From 342299abf65aab337c06ceb7c4b0f76457356667 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Fri, 5 Jun 2015 15:38:50 +0200 Subject: [PATCH 001/498] Initial commit --- spring-web-reactive/.gitignore | 8 + spring-web-reactive/build.gradle | 18 ++ spring-web-reactive/gradle.properties | 1 + .../rx/io/ByteBufPublisherInputStream.java | 125 ++++++++++++ .../rx/io/ByteBufPublisherOutputStream.java | 78 +++++++ .../rx/util/BlockingByteBufQueue.java | 193 ++++++++++++++++++ .../util/BlockingByteBufQueuePublisher.java | 110 ++++++++++ .../util/BlockingByteBufQueueSubscriber.java | 77 +++++++ .../io/ByteBufPublisherInputStreamTests.java | 106 ++++++++++ .../BlockingByteBufQueuePublisherTests.java | 178 ++++++++++++++++ .../rx/util/BlockingByteBufQueueTests.java | 81 ++++++++ 11 files changed, 975 insertions(+) create mode 100644 spring-web-reactive/.gitignore create mode 100644 spring-web-reactive/build.gradle create mode 100644 spring-web-reactive/gradle.properties create mode 100644 spring-web-reactive/src/main/java/org/springframework/rx/io/ByteBufPublisherInputStream.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/rx/io/ByteBufPublisherOutputStream.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/rx/util/BlockingByteBufQueue.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/rx/util/BlockingByteBufQueuePublisher.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/rx/util/BlockingByteBufQueueSubscriber.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/rx/io/ByteBufPublisherInputStreamTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/rx/util/BlockingByteBufQueuePublisherTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/rx/util/BlockingByteBufQueueTests.java diff --git a/spring-web-reactive/.gitignore b/spring-web-reactive/.gitignore new file mode 100644 index 0000000000..8591e43e73 --- /dev/null +++ b/spring-web-reactive/.gitignore @@ -0,0 +1,8 @@ +target +.project +.classpath +.settings +*.iml +/.idea/ +bin +.gradle diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle new file mode 100644 index 0000000000..71878c4182 --- /dev/null +++ b/spring-web-reactive/build.gradle @@ -0,0 +1,18 @@ + +apply plugin: 'java' + +repositories { + mavenCentral() +} + +dependencies { + compile "org.springframework:spring-core:4.1.2.RELEASE" + compile "org.reactivestreams:reactive-streams:1.0.0.RC3" + compile "io.netty:netty-buffer:4.0.25.Final" + compile "org.slf4j:slf4j-api:1.7.6" + compile "ch.qos.logback:logback-classic:1.1.2" + + testCompile "junit:junit:4.12" +} + + diff --git a/spring-web-reactive/gradle.properties b/spring-web-reactive/gradle.properties new file mode 100644 index 0000000000..bebfcbcfd2 --- /dev/null +++ b/spring-web-reactive/gradle.properties @@ -0,0 +1 @@ +version=1.0.0.BUILD-SNAPSHOT diff --git a/spring-web-reactive/src/main/java/org/springframework/rx/io/ByteBufPublisherInputStream.java b/spring-web-reactive/src/main/java/org/springframework/rx/io/ByteBufPublisherInputStream.java new file mode 100644 index 0000000000..43e7fa7624 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/rx/io/ByteBufPublisherInputStream.java @@ -0,0 +1,125 @@ +package org.springframework.rx.io;/* + * Copyright 2002-2015 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. + */ + +import java.io.IOException; +import java.io.InputStream; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufInputStream; +import org.reactivestreams.Publisher; + +import org.springframework.rx.util.BlockingByteBufQueue; +import org.springframework.rx.util.BlockingByteBufQueueSubscriber; +import org.springframework.util.Assert; + +/** + * @author Arjen Poutsma + */ +public class ByteBufPublisherInputStream extends InputStream { + + private final BlockingByteBufQueue queue; + + private ByteBufInputStream currentStream; + + public ByteBufPublisherInputStream(Publisher publisher) { + Assert.notNull(publisher, "'publisher' must not be null"); + + this.queue = new BlockingByteBufQueue(); + publisher.subscribe(new BlockingByteBufQueueSubscriber(this.queue)); + } + + ByteBufPublisherInputStream(BlockingByteBufQueue queue) { + Assert.notNull(queue, "'queue' must not be null"); + this.queue = queue; + } + + @Override + public int available() throws IOException { + InputStream is = currentStream(); + return is != null ? is.available() : 0; + } + + @Override + public int read() throws IOException { + InputStream is = currentStream(); + while (is != null) { + int ch = is.read(); + if (ch != -1) { + return ch; + } else { + is = currentStream(); + } + } + return -1; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + InputStream is = currentStream(); + if (is == null) { + return -1; + } + else if (b == null) { + throw new NullPointerException(); + } + else if (off < 0 || len < 0 || len > b.length - off) { + throw new IndexOutOfBoundsException(); + } + else if (len == 0) { + return 0; + } + do { + int n = is.read(b, off, len); + if (n > 0) { + return n; + } + else { + is = currentStream(); + } + } + while (is != null); + return -1; + } + + private InputStream currentStream() throws IOException { + try { + if (this.currentStream != null && this.currentStream.available() > 0) { + return this.currentStream; + } + else if (this.queue.isComplete()) { + return null; + } + else if (this.queue.isHeadBuffer()) { + ByteBuf current = this.queue.pollBuffer(); + this.currentStream = new ByteBufInputStream(current); + return this.currentStream; + } + else if (this.queue.isHeadError()) { + Throwable t = this.queue.pollError(); + throw toIOException(t); + } + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + return null; + } + + private static IOException toIOException(Throwable t) { + return t instanceof IOException ? (IOException) t : new IOException(t); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/rx/io/ByteBufPublisherOutputStream.java b/spring-web-reactive/src/main/java/org/springframework/rx/io/ByteBufPublisherOutputStream.java new file mode 100644 index 0000000000..a85125eb84 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/rx/io/ByteBufPublisherOutputStream.java @@ -0,0 +1,78 @@ +package org.springframework.rx.io; + +import java.io.IOException; +import java.io.OutputStream; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.UnpooledByteBufAllocator; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +import org.springframework.rx.util.BlockingByteBufQueue; +import org.springframework.rx.util.BlockingByteBufQueuePublisher; +import org.springframework.util.Assert; + +/** + * @author Arjen Poutsma + */ +public class ByteBufPublisherOutputStream extends OutputStream { + + private final BlockingByteBufQueue queue = new BlockingByteBufQueue(); + + private final ByteBufAllocator bufferAllocator; + + public ByteBufPublisherOutputStream() { + this(new UnpooledByteBufAllocator(false)); + } + + public ByteBufPublisherOutputStream(ByteBufAllocator bufferAllocator) { + Assert.notNull(bufferAllocator, "'bufferAllocator' must not be null"); + this.bufferAllocator = bufferAllocator; + } + + public Publisher toByteBufPublisher() { + return new BlockingByteBufQueuePublisher(this.queue); + } + + @Override + public void write(int b) throws IOException { + ByteBuf buffer = this.bufferAllocator.buffer(1, 1); + buffer.writeByte(b); + putBuffer(buffer); + } + + @Override + public void write(byte[] b) throws IOException { + ByteBuf buffer = this.bufferAllocator.buffer(b.length, b.length); + buffer.writeBytes(b); + putBuffer(buffer); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + ByteBuf buffer = this.bufferAllocator.buffer(len, len); + buffer.writeBytes(b, off, len); + putBuffer(buffer); + } + + private void putBuffer(ByteBuf buffer) { + try { + this.queue.putBuffer(buffer); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + + @Override + public void close() throws IOException { + try { + this.queue.complete(); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } +} diff --git a/spring-web-reactive/src/main/java/org/springframework/rx/util/BlockingByteBufQueue.java b/spring-web-reactive/src/main/java/org/springframework/rx/util/BlockingByteBufQueue.java new file mode 100644 index 0000000000..eda6736084 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/rx/util/BlockingByteBufQueue.java @@ -0,0 +1,193 @@ +/* + * Copyright 2002-2015 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.rx.util; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +import io.netty.buffer.ByteBuf; + +import org.springframework.util.Assert; + +/** + * A {@link BlockingQueue} aimed at working with {@code Publisher} instances. + * Mainly meant to bridge between reactive and non-reactive APIs, such as blocking + * streams. + * + *

Typically, this class will be used by two threads: one thread to put new elements on + * the stack by calling {@link #putBuffer(ByteBuf)}, possibly {@link #putError(Throwable)} + * and finally {@link #complete()}. The other thread will read elements by calling {@link + * #isHeadBuffer()} and {@link #isHeadError()}, while keeping an eye on {@link + * #isComplete()}. + * + * @author Arjen Poutsma + */ +public class BlockingByteBufQueue { + + private final BlockingQueue queue = new LinkedBlockingQueue(); + + /** + * Inserts the specified buffer into this queue, waiting if necessary for space to + * become available. + * @param buffer the buffer to add + */ + public void putBuffer(ByteBuf buffer) throws InterruptedException { + Assert.notNull(buffer, "'buffer' must not be null"); + Assert.state(!isComplete(), "Cannot put buffers in queue after complete()"); + this.queue.put(new ByteBufElement(buffer)); + } + + /** + * Inserts the specified error into this queue, waiting if necessary for space to + * become available. + * @param error the error to add + */ + public void putError(Throwable error) throws InterruptedException { + Assert.notNull(error, "'error' must not be null"); + Assert.state(!isComplete(), "Cannot put errors in queue after complete()"); + this.queue.put(new ErrorElement(error)); + } + + /** + * Marks the queue as complete. + */ + public void complete() throws InterruptedException { + this.queue.put(COMPLETE); + } + + /** + * Indicates whether the current head of this queue is a {@link ByteBuf}. + * @return {@code true} if the current head is a buffer; {@code false} otherwise + */ + public boolean isHeadBuffer() { + Element element = this.queue.peek(); + return element instanceof ByteBufElement; + } + + /** + * Indicates whether the current head of this queue is a {@link Throwable}. + * @return {@code true} if the current head is an error; {@code false} otherwise + */ + public boolean isHeadError() { + Element element = this.queue.peek(); + return element instanceof ErrorElement; + } + + /** + * Indicates whether there are more buffers or errors in this queue. + * @return {@code true} if there more elements in this queue; {@code false} otherwise + */ + public boolean isComplete() { + Element element = this.queue.peek(); + return COMPLETE == element; + } + + /** + * Retrieves and removes the buffer head of this queue. Should only be called after + * {@link #isHeadBuffer()} returns {@code true}. + * @return the head of the queue, as buffer + * @throws IllegalStateException if the current head of this queue is not a buffer + * @see #isHeadBuffer() + */ + public ByteBuf pollBuffer() throws InterruptedException { + Element element = this.queue.take(); + return element != null ? element.getBuffer() : null; + } + + /** + * Retrieves and removes the buffer error of this queue. Should only be called after + * {@link #isHeadError()} returns {@code true}. + * @return the head of the queue, as error + * @throws IllegalStateException if the current head of this queue is not a error + * @see #isHeadError() + */ + public Throwable pollError() throws InterruptedException { + Element element = this.queue.take(); + return element != null ? element.getError() : null; + } + + /** + * Removes all of the elements from this collection + */ + public void clear() { + this.queue.clear(); + } + + private interface Element { + + ByteBuf getBuffer(); + + Throwable getError(); + } + + private static class ByteBufElement implements Element { + + private final ByteBuf buffer; + + public ByteBufElement(ByteBuf buffer) { + if (buffer == null) { + throw new IllegalArgumentException("'buffer' should not be null"); + } + this.buffer = buffer; + } + + @Override + public ByteBuf getBuffer() { + return this.buffer; + } + + @Override + public Throwable getError() { + throw new IllegalStateException("No error on top of the queue"); + } + + } + + private static class ErrorElement implements Element { + + private final Throwable error; + + public ErrorElement(Throwable error) { + if (error == null) { + throw new IllegalArgumentException("'error' should not be null"); + } + this.error = error; + } + + @Override + public ByteBuf getBuffer() { + throw new IllegalStateException("No ByteBuf on top of the queue"); + } + + @Override + public Throwable getError() { + return this.error; + } + } + + private static final Element COMPLETE = new Element() { + @Override + public ByteBuf getBuffer() { + throw new IllegalStateException("No ByteBuf on top of the queue"); + } + + @Override + public Throwable getError() { + throw new IllegalStateException("No error on top of the queue"); + } + }; +} diff --git a/spring-web-reactive/src/main/java/org/springframework/rx/util/BlockingByteBufQueuePublisher.java b/spring-web-reactive/src/main/java/org/springframework/rx/util/BlockingByteBufQueuePublisher.java new file mode 100644 index 0000000000..1bbd763ba5 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/rx/util/BlockingByteBufQueuePublisher.java @@ -0,0 +1,110 @@ +/* + * Copyright 2002-2015 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.rx.util; + +import io.netty.buffer.ByteBuf; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +import org.springframework.util.Assert; + +/** + * @author Arjen Poutsma + */ +public class BlockingByteBufQueuePublisher implements Publisher { + + private final BlockingByteBufQueue queue; + + private Subscriber subscriber; + + private final Object subscriberMutex = new Object(); + + public BlockingByteBufQueuePublisher(BlockingByteBufQueue queue) { + Assert.notNull(queue, "'queue' must not be null"); + this.queue = queue; + } + + @Override + public void subscribe(Subscriber subscriber) { + synchronized (this.subscriberMutex) { + if (this.subscriber != null) { + subscriber.onError( + new IllegalStateException("Only one subscriber allowed")); + } + else { + this.subscriber = subscriber; + final SubscriptionThread thread = new SubscriptionThread(); + this.subscriber.onSubscribe(new Subscription() { + @Override + public void request(long n) { + thread.request(n); + } + + @Override + public void cancel() { + thread.cancel(); + } + }); + thread.start(); + } + } + } + + private class SubscriptionThread extends Thread { + + private volatile long requestCount = 0; + + private long l = 0; + + @Override + public void run() { + try { + while (!Thread.currentThread().isInterrupted()) + if ((l < requestCount || requestCount == Long.MAX_VALUE) && + queue.isHeadBuffer()) { + subscriber.onNext(queue.pollBuffer()); + l++; + } + else if (queue.isHeadError()) { + subscriber.onError(queue.pollError()); + break; + } + else if (queue.isComplete()) { + subscriber.onComplete(); + break; + } + } + catch (InterruptedException ex) { + // Allow thread to exit + } + } + + public void request(long n) { + if (n != Long.MAX_VALUE) { + this.requestCount += n; + } + else { + this.requestCount = Long.MAX_VALUE; + } + } + + public void cancel() { + interrupt(); + } + } +} diff --git a/spring-web-reactive/src/main/java/org/springframework/rx/util/BlockingByteBufQueueSubscriber.java b/spring-web-reactive/src/main/java/org/springframework/rx/util/BlockingByteBufQueueSubscriber.java new file mode 100644 index 0000000000..dda8668ce6 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/rx/util/BlockingByteBufQueueSubscriber.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2015 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.rx.util; + +import io.netty.buffer.ByteBuf; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +import org.springframework.util.Assert; + +/** + * @author Arjen Poutsma + */ +public class BlockingByteBufQueueSubscriber implements Subscriber { + + private final BlockingByteBufQueue queue; + + private Subscription subscription; + + public BlockingByteBufQueueSubscriber(BlockingByteBufQueue queue) { + Assert.notNull(queue, "'queue' must not be null"); + this.queue = queue; + } + + @Override + public void onSubscribe(Subscription subscription) { + this.subscription = subscription; + + this.subscription.request(1); + } + + @Override + public void onNext(ByteBuf byteBuf) { + try { + this.queue.putBuffer(byteBuf); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + this.subscription.request(1); + } + + @Override + public void onError(Throwable t) { + try { + this.queue.putError(t); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + this.subscription.request(1); + } + + @Override + public void onComplete() { + try { + this.queue.complete(); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } +} diff --git a/spring-web-reactive/src/test/java/org/springframework/rx/io/ByteBufPublisherInputStreamTests.java b/spring-web-reactive/src/test/java/org/springframework/rx/io/ByteBufPublisherInputStreamTests.java new file mode 100644 index 0000000000..24a7d9b0e9 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/rx/io/ByteBufPublisherInputStreamTests.java @@ -0,0 +1,106 @@ +/* + * Copyright 2002-2015 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.rx.io; + +import java.io.EOFException; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import org.junit.Before; +import org.junit.Test; + +import org.springframework.rx.util.BlockingByteBufQueue; +import org.springframework.rx.util.BlockingByteBufQueuePublisher; + +import static org.junit.Assert.*; + +/** + * @author Arjen Poutsma + */ +public class ByteBufPublisherInputStreamTests { + + private BlockingByteBufQueue queue; + + private ByteBufPublisherInputStream is; + + + @Before + public void setUp() throws Exception { + queue = new BlockingByteBufQueue(); + is = new ByteBufPublisherInputStream(queue); + + } + + @Test + public void readSingleByte() throws Exception { + ByteBuf abc = Unpooled.copiedBuffer(new byte[]{'a', 'b', 'c'}); + ByteBuf def = Unpooled.copiedBuffer(new byte[]{'d', 'e', 'f'}); + + queue.putBuffer(abc); + queue.putBuffer(def); + queue.complete(); + + + int ch = is.read(); + assertEquals('a', ch); + ch = is.read(); + assertEquals('b', ch); + ch = is.read(); + assertEquals('c', ch); + + ch = is.read(); + assertEquals('d', ch); + ch = is.read(); + assertEquals('e', ch); + ch = is.read(); + assertEquals('f', ch); + + ch = is.read(); + assertEquals(-1, ch); + } + + @Test + public void readBytes() throws Exception { + ByteBuf abc = Unpooled.copiedBuffer(new byte[]{'a', 'b', 'c'}); + ByteBuf def = Unpooled.copiedBuffer(new byte[]{'d', 'e', 'f'}); + + queue.putBuffer(abc); + queue.putBuffer(def); + queue.complete(); + + byte[] buf = new byte[2]; + int read = this.is.read(buf); + assertEquals(2, read); + assertArrayEquals(new byte[] { 'a', 'b'}, buf); + + read = this.is.read(buf); + assertEquals(1, read); + assertEquals('c', buf[0]); + + read = this.is.read(buf); + assertEquals(2, read); + assertArrayEquals(new byte[] { 'd', 'e'}, buf); + + read = this.is.read(buf); + assertEquals(1, read); + assertEquals('f', buf[0]); + + read = this.is.read(buf); + assertEquals(-1, read); + } + +} \ No newline at end of file diff --git a/spring-web-reactive/src/test/java/org/springframework/rx/util/BlockingByteBufQueuePublisherTests.java b/spring-web-reactive/src/test/java/org/springframework/rx/util/BlockingByteBufQueuePublisherTests.java new file mode 100644 index 0000000000..719324f4b2 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/rx/util/BlockingByteBufQueuePublisherTests.java @@ -0,0 +1,178 @@ +/* + * Copyright 2002-2015 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.rx.util; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import static org.junit.Assert.*; +import org.junit.Before; +import org.junit.Test; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +/** + * @author Arjen Poutsma + */ +public class BlockingByteBufQueuePublisherTests { + + private BlockingByteBufQueue queue; + + private BlockingByteBufQueuePublisher publisher; + + @Before + public void setUp() throws Exception { + queue = new BlockingByteBufQueue(); + publisher = new BlockingByteBufQueuePublisher(queue); + } + + @Test + public void normal() throws Exception { + ByteBuf abc = Unpooled.copiedBuffer(new byte[]{'a', 'b', 'c'}); + ByteBuf def = Unpooled.copiedBuffer(new byte[]{'d', 'e', 'f'}); + + queue.putBuffer(abc); + queue.putBuffer(def); + queue.complete(); + + final AtomicBoolean complete = new AtomicBoolean(false); + final List received = new ArrayList(2); + + publisher.subscribe(new Subscriber() { + @Override + public void onSubscribe(Subscription s) { + s.request(2); + } + + @Override + public void onNext(ByteBuf byteBuf) { + received.add(byteBuf); + } + + @Override + public void onError(Throwable t) { + fail("onError not expected"); + } + + @Override + public void onComplete() { + complete.set(true); + } + }); + + while (!complete.get()) { + } + + assertEquals(2, received.size()); + assertSame(abc, received.get(0)); + assertSame(def, received.get(1)); + } + + @Test + public void unbounded() throws Exception { + ByteBuf abc = Unpooled.copiedBuffer(new byte[]{'a', 'b', 'c'}); + ByteBuf def = Unpooled.copiedBuffer(new byte[]{'d', 'e', 'f'}); + + queue.putBuffer(abc); + queue.putBuffer(def); + queue.complete(); + + final AtomicBoolean complete = new AtomicBoolean(false); + final List received = new ArrayList(2); + + publisher.subscribe(new Subscriber() { + @Override + public void onSubscribe(Subscription s) { + s.request(Long.MAX_VALUE); + } + + @Override + public void onNext(ByteBuf byteBuf) { + received.add(byteBuf); + } + + @Override + public void onError(Throwable t) { + fail("onError not expected"); + } + + @Override + public void onComplete() { + complete.set(true); + } + }); + + while (!complete.get()) { + } + + assertEquals(2, received.size()); + assertSame(abc, received.get(0)); + assertSame(def, received.get(1)); + } + + @Test + public void multipleSubscribe() throws Exception { + publisher.subscribe(new Subscriber() { + @Override + public void onSubscribe(Subscription s) { + + } + + @Override + public void onNext(ByteBuf byteBuf) { + + } + + @Override + public void onError(Throwable t) { + + } + + @Override + public void onComplete() { + + } + }); + publisher.subscribe(new Subscriber() { + @Override + public void onSubscribe(Subscription s) { + fail("onSubscribe not expected"); + } + + @Override + public void onNext(ByteBuf byteBuf) { + fail("onNext not expected"); + } + + @Override + public void onError(Throwable t) { + assertTrue(t instanceof IllegalStateException); + } + + @Override + public void onComplete() { + fail("onComplete not expected"); + } + }); + + } + + +} \ No newline at end of file diff --git a/spring-web-reactive/src/test/java/org/springframework/rx/util/BlockingByteBufQueueTests.java b/spring-web-reactive/src/test/java/org/springframework/rx/util/BlockingByteBufQueueTests.java new file mode 100644 index 0000000000..5f16234fcc --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/rx/util/BlockingByteBufQueueTests.java @@ -0,0 +1,81 @@ +/* + * Copyright 2002-2015 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.rx.util; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import static org.junit.Assert.*; +import org.junit.Before; +import org.junit.Test; + +/** + * @author Arjen Poutsma + */ +public class BlockingByteBufQueueTests { + + private BlockingByteBufQueue queue; + + @Before + public void setUp() throws Exception { + queue = new BlockingByteBufQueue(); + } + + @Test + public void normal() throws Exception { + ByteBuf abc = Unpooled.copiedBuffer(new byte[]{'a', 'b', 'c'}); + ByteBuf def = Unpooled.copiedBuffer(new byte[]{'d', 'e', 'f'}); + + queue.putBuffer(abc); + queue.putBuffer(def); + queue.complete(); + + assertTrue(queue.isHeadBuffer()); + assertFalse(queue.isHeadError()); + assertSame(abc, queue.pollBuffer()); + + assertTrue(queue.isHeadBuffer()); + assertFalse(queue.isHeadError()); + assertSame(def, queue.pollBuffer()); + + assertTrue(queue.isComplete()); + } + + @Test + public void empty() throws Exception { + assertNull(queue.pollBuffer()); + } + + @Test + public void error() throws Exception { + ByteBuf abc = Unpooled.copiedBuffer(new byte[]{'a', 'b', 'c'}); + Throwable error = new IllegalStateException(); + + queue.putBuffer(abc); + queue.putError(error); + queue.complete(); + + assertTrue(queue.isHeadBuffer()); + assertFalse(queue.isHeadError()); + assertSame(abc, queue.pollBuffer()); + + assertTrue(queue.isHeadError()); + assertFalse(queue.isHeadBuffer()); + assertSame(error, queue.pollError()); + + assertTrue(queue.isComplete()); + } +} \ No newline at end of file From c552aaa6f1cd83582a61c7f1fd9e146da19c3c8a Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Tue, 30 Jun 2015 13:45:54 +0200 Subject: [PATCH 002/498] work, work. --- spring-web-reactive/build.gradle | 14 ++ ...ava => ByteArrayPublisherInputStream.java} | 41 ++-- .../rx/io/ByteArrayPublisherOutputStream.java | 54 +++++ .../rx/io/ByteBufPublisherOutputStream.java | 78 ------- .../util/AbstractUnicastAsyncSubscriber.java | 214 ++++++++++++++++++ .../util/AbstractUnicastSyncSubscriber.java | 76 +++++++ .../rx/util/BlockingByteBufQueue.java | 193 ---------------- .../util/BlockingByteBufQueueSubscriber.java | 77 ------- .../rx/util/BlockingSignalQueue.java | 123 ++++++++++ ...java => BlockingSignalQueuePublisher.java} | 18 +- .../util/BlockingSignalQueueSubscriber.java | 113 +++++++++ .../springframework/rx/util/OnComplete.java | 65 ++++++ .../org/springframework/rx/util/OnError.java | 69 ++++++ .../org/springframework/rx/util/OnNext.java | 70 ++++++ .../springframework/rx/util/OnSubscribe.java | 70 ++++++ .../org/springframework/rx/util/Signal.java | 39 ++++ .../web/servlet/AsyncContextSynchronizer.java | 69 ++++++ .../io/ByteBufPublisherInputStreamTests.java | 29 +-- .../BlockingByteBufQueuePublisherTests.java | 16 +- .../rx/util/BlockingByteBufQueueTests.java | 28 +-- 20 files changed, 1036 insertions(+), 420 deletions(-) rename spring-web-reactive/src/main/java/org/springframework/rx/io/{ByteBufPublisherInputStream.java => ByteArrayPublisherInputStream.java} (69%) create mode 100644 spring-web-reactive/src/main/java/org/springframework/rx/io/ByteArrayPublisherOutputStream.java delete mode 100644 spring-web-reactive/src/main/java/org/springframework/rx/io/ByteBufPublisherOutputStream.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/rx/util/AbstractUnicastAsyncSubscriber.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/rx/util/AbstractUnicastSyncSubscriber.java delete mode 100644 spring-web-reactive/src/main/java/org/springframework/rx/util/BlockingByteBufQueue.java delete mode 100644 spring-web-reactive/src/main/java/org/springframework/rx/util/BlockingByteBufQueueSubscriber.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/rx/util/BlockingSignalQueue.java rename spring-web-reactive/src/main/java/org/springframework/rx/util/{BlockingByteBufQueuePublisher.java => BlockingSignalQueuePublisher.java} (82%) create mode 100644 spring-web-reactive/src/main/java/org/springframework/rx/util/BlockingSignalQueueSubscriber.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/rx/util/OnComplete.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/rx/util/OnError.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/rx/util/OnNext.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/rx/util/OnSubscribe.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/rx/util/Signal.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/rx/web/servlet/AsyncContextSynchronizer.java diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index 71878c4182..c9c4c7150b 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -1,5 +1,17 @@ +buildscript { + repositories { + maven { url 'http://repo.springsource.org/plugins-release' } + } + dependencies { + classpath 'org.springframework.build.gradle:propdeps-plugin:0.0.7' + } +} apply plugin: 'java' +apply plugin: 'propdeps' +apply plugin: 'propdeps-idea' +apply plugin: 'propdeps-maven' + repositories { mavenCentral() @@ -12,6 +24,8 @@ dependencies { compile "org.slf4j:slf4j-api:1.7.6" compile "ch.qos.logback:logback-classic:1.1.2" + provided "javax.servlet:javax.servlet-api:3.1.0" + testCompile "junit:junit:4.12" } diff --git a/spring-web-reactive/src/main/java/org/springframework/rx/io/ByteBufPublisherInputStream.java b/spring-web-reactive/src/main/java/org/springframework/rx/io/ByteArrayPublisherInputStream.java similarity index 69% rename from spring-web-reactive/src/main/java/org/springframework/rx/io/ByteBufPublisherInputStream.java rename to spring-web-reactive/src/main/java/org/springframework/rx/io/ByteArrayPublisherInputStream.java index 43e7fa7624..d3251c0451 100644 --- a/spring-web-reactive/src/main/java/org/springframework/rx/io/ByteBufPublisherInputStream.java +++ b/spring-web-reactive/src/main/java/org/springframework/rx/io/ByteArrayPublisherInputStream.java @@ -14,34 +14,40 @@ package org.springframework.rx.io;/* * limitations under the License. */ +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufInputStream; import org.reactivestreams.Publisher; -import org.springframework.rx.util.BlockingByteBufQueue; -import org.springframework.rx.util.BlockingByteBufQueueSubscriber; +import org.springframework.rx.util.BlockingSignalQueue; +import org.springframework.rx.util.BlockingSignalQueueSubscriber; import org.springframework.util.Assert; /** + * {@code InputStream} implementation based on a byte array {@link Publisher}. + * * @author Arjen Poutsma */ -public class ByteBufPublisherInputStream extends InputStream { +public class ByteArrayPublisherInputStream extends InputStream { - private final BlockingByteBufQueue queue; + private final BlockingSignalQueue queue; - private ByteBufInputStream currentStream; + private ByteArrayInputStream currentStream; - public ByteBufPublisherInputStream(Publisher publisher) { + + /** + * Creates a new {@code ByteArrayPublisherInputStream} based on the given publisher. + * @param publisher the publisher to use + */ + public ByteArrayPublisherInputStream(Publisher publisher) { Assert.notNull(publisher, "'publisher' must not be null"); - this.queue = new BlockingByteBufQueue(); - publisher.subscribe(new BlockingByteBufQueueSubscriber(this.queue)); + this.queue = new BlockingSignalQueue(); + publisher.subscribe(new BlockingSignalQueueSubscriber(this.queue)); } - ByteBufPublisherInputStream(BlockingByteBufQueue queue) { + ByteArrayPublisherInputStream(BlockingSignalQueue queue) { Assert.notNull(queue, "'queue' must not be null"); this.queue = queue; } @@ -91,6 +97,7 @@ public class ByteBufPublisherInputStream extends InputStream { } } while (is != null); + return -1; } @@ -102,14 +109,14 @@ public class ByteBufPublisherInputStream extends InputStream { else if (this.queue.isComplete()) { return null; } - else if (this.queue.isHeadBuffer()) { - ByteBuf current = this.queue.pollBuffer(); - this.currentStream = new ByteBufInputStream(current); + else if (this.queue.isHeadSignal()) { + byte[] current = this.queue.pollSignal(); + this.currentStream = new ByteArrayInputStream(current); return this.currentStream; } else if (this.queue.isHeadError()) { Throwable t = this.queue.pollError(); - throw toIOException(t); + throw t instanceof IOException ? (IOException) t : new IOException(t); } } catch (InterruptedException ex) { @@ -118,8 +125,4 @@ public class ByteBufPublisherInputStream extends InputStream { return null; } - private static IOException toIOException(Throwable t) { - return t instanceof IOException ? (IOException) t : new IOException(t); - } - } diff --git a/spring-web-reactive/src/main/java/org/springframework/rx/io/ByteArrayPublisherOutputStream.java b/spring-web-reactive/src/main/java/org/springframework/rx/io/ByteArrayPublisherOutputStream.java new file mode 100644 index 0000000000..a89b9daee8 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/rx/io/ByteArrayPublisherOutputStream.java @@ -0,0 +1,54 @@ +package org.springframework.rx.io; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Arrays; + +import org.reactivestreams.Publisher; + +import org.springframework.rx.util.BlockingSignalQueue; +import org.springframework.rx.util.BlockingSignalQueuePublisher; + +/** + * {@code OutputStream} implementation that stores all written bytes, to be retrieved + * using {@link #toByteBufPublisher()}. + * @author Arjen Poutsma + */ +public class ByteArrayPublisherOutputStream extends OutputStream { + + private final BlockingSignalQueue queue = new BlockingSignalQueue(); + + /** + * Returns the written data as a {@code Publisher}. + * @return a publisher for the written bytes + */ + public Publisher toByteBufPublisher() { + return new BlockingSignalQueuePublisher(this.queue); + } + + @Override + public void write(int b) throws IOException { + write(new byte[]{(byte) b}); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + byte[] copy = Arrays.copyOf(b, len); + try { + this.queue.putSignal(copy); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + + @Override + public void close() throws IOException { + try { + this.queue.complete(); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } +} diff --git a/spring-web-reactive/src/main/java/org/springframework/rx/io/ByteBufPublisherOutputStream.java b/spring-web-reactive/src/main/java/org/springframework/rx/io/ByteBufPublisherOutputStream.java deleted file mode 100644 index a85125eb84..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/rx/io/ByteBufPublisherOutputStream.java +++ /dev/null @@ -1,78 +0,0 @@ -package org.springframework.rx.io; - -import java.io.IOException; -import java.io.OutputStream; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.UnpooledByteBufAllocator; -import org.reactivestreams.Publisher; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; - -import org.springframework.rx.util.BlockingByteBufQueue; -import org.springframework.rx.util.BlockingByteBufQueuePublisher; -import org.springframework.util.Assert; - -/** - * @author Arjen Poutsma - */ -public class ByteBufPublisherOutputStream extends OutputStream { - - private final BlockingByteBufQueue queue = new BlockingByteBufQueue(); - - private final ByteBufAllocator bufferAllocator; - - public ByteBufPublisherOutputStream() { - this(new UnpooledByteBufAllocator(false)); - } - - public ByteBufPublisherOutputStream(ByteBufAllocator bufferAllocator) { - Assert.notNull(bufferAllocator, "'bufferAllocator' must not be null"); - this.bufferAllocator = bufferAllocator; - } - - public Publisher toByteBufPublisher() { - return new BlockingByteBufQueuePublisher(this.queue); - } - - @Override - public void write(int b) throws IOException { - ByteBuf buffer = this.bufferAllocator.buffer(1, 1); - buffer.writeByte(b); - putBuffer(buffer); - } - - @Override - public void write(byte[] b) throws IOException { - ByteBuf buffer = this.bufferAllocator.buffer(b.length, b.length); - buffer.writeBytes(b); - putBuffer(buffer); - } - - @Override - public void write(byte[] b, int off, int len) throws IOException { - ByteBuf buffer = this.bufferAllocator.buffer(len, len); - buffer.writeBytes(b, off, len); - putBuffer(buffer); - } - - private void putBuffer(ByteBuf buffer) { - try { - this.queue.putBuffer(buffer); - } - catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - } - } - - @Override - public void close() throws IOException { - try { - this.queue.complete(); - } - catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - } - } -} diff --git a/spring-web-reactive/src/main/java/org/springframework/rx/util/AbstractUnicastAsyncSubscriber.java b/spring-web-reactive/src/main/java/org/springframework/rx/util/AbstractUnicastAsyncSubscriber.java new file mode 100644 index 0000000000..aece160be8 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/rx/util/AbstractUnicastAsyncSubscriber.java @@ -0,0 +1,214 @@ +/* + * Copyright 2002-2015 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.rx.util; + +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +import org.springframework.util.Assert; + +/** + * @author Arjen Poutsma + */ +public abstract class AbstractUnicastAsyncSubscriber implements Subscriber { + + private final Executor executor; + + private Subscription subscription; + + private boolean done; + + protected AbstractUnicastAsyncSubscriber(Executor executor) { + Assert.notNull(executor, "'executor' must not be null"); + + this.executor = executor; + } + + private void done() { + done = true; + + if (subscription != null) { + subscription.cancel(); + } + } + + // This method is invoked when the OnNext signals arrive + // Returns whether more elements are desired or not, and if no more elements are desired, + // for convenience. + protected abstract boolean whenNext(final T element); + + // This method is invoked when the OnComplete signal arrives + // override this method to implement your own custom onComplete logic. + protected void whenComplete() { + } + + // This method is invoked if the OnError signal arrives + // override this method to implement your own custom onError logic. + protected void whenError(Throwable error) { + } + + private void handleOnSubscribe(Subscription subscription) { + if (subscription == null) { + return; + } + if (this.subscription != null) { + subscription.cancel(); + } + else { + this.subscription = subscription; + this.subscription.request(1); + } + } + + private void handleOnNext(final T element) { + if (!done) { + try { + if (whenNext(element)) { + subscription.request(1); + } + else { + done(); + } + } + catch (final Throwable t) { + done(); + onError(t); + } + } + } + + private void handleOnComplete() { + done = true; + whenComplete(); + } + + private void handleOnError(final Throwable error) { + done = true; + whenError(error); + } + + // We implement the OnX methods on `Subscriber` to send Signals that we will process asycnhronously, but only one at a time + + @Override + public final void onSubscribe(final Subscription s) { + // As per rule 2.13, we need to throw a `java.lang.NullPointerException` if the `Subscription` is `null` + if (s == null) { + throw null; + } + + signal(new OnSubscribe(s)); + } + + @Override + public final void onNext(final T element) { + // As per rule 2.13, we need to throw a `java.lang.NullPointerException` if the `element` is `null` + if (element == null) { + throw null; + } + + signal(new OnNext(element)); + } + + @Override + public final void onError(final Throwable t) { + // As per rule 2.13, we need to throw a `java.lang.NullPointerException` if the `Throwable` is `null` + if (t == null) { + throw null; + } + + signal(new OnError(t)); + } + + @Override + public final void onComplete() { + signal(OnComplete.INSTANCE); + } + + private final ConcurrentLinkedQueue> inboundSignals = + new ConcurrentLinkedQueue>(); + + private final AtomicBoolean enabled = new AtomicBoolean(false); + + // What `signal` does is that it sends signals to the `Subscription` asynchronously + private void signal(final Signal signal) { + if (inboundSignals + .offer(signal)) // No need to null-check here as ConcurrentLinkedQueue does this for us + { + tryScheduleToExecute(); // Then we try to schedule it for execution, if it isn't already + } + } + + // This method makes sure that this `Subscriber` is only executing on one Thread at a time + private void tryScheduleToExecute() { + if (enabled.compareAndSet(false, true)) { + try { + executor.execute(new SignalRunnable()); + } + catch (Throwable t) { // If we can't run on the `Executor`, we need to fail gracefully and not violate rule 2.13 + if (!done) { + try { + done(); // First of all, this failure is not recoverable, so we need to cancel our subscription + } + finally { + inboundSignals.clear(); // We're not going to need these anymore + // This subscription is cancelled by now, but letting the Subscriber become schedulable again means + // that we can drain the inboundSignals queue if anything arrives after clearing + enabled.set(false); + } + } + } + } + } + + private class SignalRunnable implements Runnable { + + @Override + public void run() { + if (enabled.get()) { + try { + Signal s = inboundSignals.poll(); + if (!done) { + if (s.isOnNext()) { + handleOnNext(s.next()); + } + else if (s.isOnSubscribe()) { + handleOnSubscribe(s.subscription()); + } + else if (s.isOnError()) { + handleOnError(s.error()); + } + else if (s.isComplete()) { + handleOnComplete(); + } + } + } + finally { + enabled.set(false); + + if (!inboundSignals.isEmpty()) { + tryScheduleToExecute(); + } + } + } + + } + } +} \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/rx/util/AbstractUnicastSyncSubscriber.java b/spring-web-reactive/src/main/java/org/springframework/rx/util/AbstractUnicastSyncSubscriber.java new file mode 100644 index 0000000000..85e4764d50 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/rx/util/AbstractUnicastSyncSubscriber.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2015 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.rx.util; + +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +/** + * @author Arjen Poutsma + */ +public abstract class AbstractUnicastSyncSubscriber implements Subscriber { + + private Subscription subscription; + + private boolean done = false; + + @Override + public final void onSubscribe(Subscription subscription) { + if (subscription == null) { + throw new NullPointerException(); + } + + if (this.subscription != null) { + subscription.cancel(); + } + else { + this.subscription = subscription; + this.subscription.request(1); + } + } + + @Override + public final void onNext(T element) { + if (element == null) { + throw new NullPointerException(); + } + + if (!done) { + try { + if (onNextInternal(element)) { + subscription.request(1); + } + else { + done(); + } + } + catch (Throwable t) { + done(); + onError(t); + } + } + } + + private void done() { + done = true; + subscription.cancel(); + } + + protected abstract boolean onNextInternal(final T element) throws Exception; + + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/rx/util/BlockingByteBufQueue.java b/spring-web-reactive/src/main/java/org/springframework/rx/util/BlockingByteBufQueue.java deleted file mode 100644 index eda6736084..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/rx/util/BlockingByteBufQueue.java +++ /dev/null @@ -1,193 +0,0 @@ -/* - * Copyright 2002-2015 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.rx.util; - -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; - -import io.netty.buffer.ByteBuf; - -import org.springframework.util.Assert; - -/** - * A {@link BlockingQueue} aimed at working with {@code Publisher} instances. - * Mainly meant to bridge between reactive and non-reactive APIs, such as blocking - * streams. - * - *

Typically, this class will be used by two threads: one thread to put new elements on - * the stack by calling {@link #putBuffer(ByteBuf)}, possibly {@link #putError(Throwable)} - * and finally {@link #complete()}. The other thread will read elements by calling {@link - * #isHeadBuffer()} and {@link #isHeadError()}, while keeping an eye on {@link - * #isComplete()}. - * - * @author Arjen Poutsma - */ -public class BlockingByteBufQueue { - - private final BlockingQueue queue = new LinkedBlockingQueue(); - - /** - * Inserts the specified buffer into this queue, waiting if necessary for space to - * become available. - * @param buffer the buffer to add - */ - public void putBuffer(ByteBuf buffer) throws InterruptedException { - Assert.notNull(buffer, "'buffer' must not be null"); - Assert.state(!isComplete(), "Cannot put buffers in queue after complete()"); - this.queue.put(new ByteBufElement(buffer)); - } - - /** - * Inserts the specified error into this queue, waiting if necessary for space to - * become available. - * @param error the error to add - */ - public void putError(Throwable error) throws InterruptedException { - Assert.notNull(error, "'error' must not be null"); - Assert.state(!isComplete(), "Cannot put errors in queue after complete()"); - this.queue.put(new ErrorElement(error)); - } - - /** - * Marks the queue as complete. - */ - public void complete() throws InterruptedException { - this.queue.put(COMPLETE); - } - - /** - * Indicates whether the current head of this queue is a {@link ByteBuf}. - * @return {@code true} if the current head is a buffer; {@code false} otherwise - */ - public boolean isHeadBuffer() { - Element element = this.queue.peek(); - return element instanceof ByteBufElement; - } - - /** - * Indicates whether the current head of this queue is a {@link Throwable}. - * @return {@code true} if the current head is an error; {@code false} otherwise - */ - public boolean isHeadError() { - Element element = this.queue.peek(); - return element instanceof ErrorElement; - } - - /** - * Indicates whether there are more buffers or errors in this queue. - * @return {@code true} if there more elements in this queue; {@code false} otherwise - */ - public boolean isComplete() { - Element element = this.queue.peek(); - return COMPLETE == element; - } - - /** - * Retrieves and removes the buffer head of this queue. Should only be called after - * {@link #isHeadBuffer()} returns {@code true}. - * @return the head of the queue, as buffer - * @throws IllegalStateException if the current head of this queue is not a buffer - * @see #isHeadBuffer() - */ - public ByteBuf pollBuffer() throws InterruptedException { - Element element = this.queue.take(); - return element != null ? element.getBuffer() : null; - } - - /** - * Retrieves and removes the buffer error of this queue. Should only be called after - * {@link #isHeadError()} returns {@code true}. - * @return the head of the queue, as error - * @throws IllegalStateException if the current head of this queue is not a error - * @see #isHeadError() - */ - public Throwable pollError() throws InterruptedException { - Element element = this.queue.take(); - return element != null ? element.getError() : null; - } - - /** - * Removes all of the elements from this collection - */ - public void clear() { - this.queue.clear(); - } - - private interface Element { - - ByteBuf getBuffer(); - - Throwable getError(); - } - - private static class ByteBufElement implements Element { - - private final ByteBuf buffer; - - public ByteBufElement(ByteBuf buffer) { - if (buffer == null) { - throw new IllegalArgumentException("'buffer' should not be null"); - } - this.buffer = buffer; - } - - @Override - public ByteBuf getBuffer() { - return this.buffer; - } - - @Override - public Throwable getError() { - throw new IllegalStateException("No error on top of the queue"); - } - - } - - private static class ErrorElement implements Element { - - private final Throwable error; - - public ErrorElement(Throwable error) { - if (error == null) { - throw new IllegalArgumentException("'error' should not be null"); - } - this.error = error; - } - - @Override - public ByteBuf getBuffer() { - throw new IllegalStateException("No ByteBuf on top of the queue"); - } - - @Override - public Throwable getError() { - return this.error; - } - } - - private static final Element COMPLETE = new Element() { - @Override - public ByteBuf getBuffer() { - throw new IllegalStateException("No ByteBuf on top of the queue"); - } - - @Override - public Throwable getError() { - throw new IllegalStateException("No error on top of the queue"); - } - }; -} diff --git a/spring-web-reactive/src/main/java/org/springframework/rx/util/BlockingByteBufQueueSubscriber.java b/spring-web-reactive/src/main/java/org/springframework/rx/util/BlockingByteBufQueueSubscriber.java deleted file mode 100644 index dda8668ce6..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/rx/util/BlockingByteBufQueueSubscriber.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2002-2015 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.rx.util; - -import io.netty.buffer.ByteBuf; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; - -import org.springframework.util.Assert; - -/** - * @author Arjen Poutsma - */ -public class BlockingByteBufQueueSubscriber implements Subscriber { - - private final BlockingByteBufQueue queue; - - private Subscription subscription; - - public BlockingByteBufQueueSubscriber(BlockingByteBufQueue queue) { - Assert.notNull(queue, "'queue' must not be null"); - this.queue = queue; - } - - @Override - public void onSubscribe(Subscription subscription) { - this.subscription = subscription; - - this.subscription.request(1); - } - - @Override - public void onNext(ByteBuf byteBuf) { - try { - this.queue.putBuffer(byteBuf); - } - catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - } - this.subscription.request(1); - } - - @Override - public void onError(Throwable t) { - try { - this.queue.putError(t); - } - catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - } - this.subscription.request(1); - } - - @Override - public void onComplete() { - try { - this.queue.complete(); - } - catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - } - } -} diff --git a/spring-web-reactive/src/main/java/org/springframework/rx/util/BlockingSignalQueue.java b/spring-web-reactive/src/main/java/org/springframework/rx/util/BlockingSignalQueue.java new file mode 100644 index 0000000000..f30f7470d4 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/rx/util/BlockingSignalQueue.java @@ -0,0 +1,123 @@ +/* + * Copyright 2002-2015 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.rx.util; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +import io.netty.buffer.ByteBuf; + +import org.springframework.util.Assert; + +/** + * A {@link BlockingQueue} aimed at working with {@code Publisher} instances. + * Mainly meant to bridge between reactive and non-reactive APIs, such as blocking + * streams. + * + *

Typically, this class will be used by two threads: one thread to put new elements on + * the stack by calling {@link #put(ByteBuf)}, possibly {@link #putError(Throwable)} and + * finally {@link #complete()}. The other thread will read elements by calling {@link + * #isHeadSignal()}/{@link #pollSignal()} and {@link #isHeadError()}/{@link #pollError()}, + * while keeping an eye on {@link #isComplete()}. + * @author Arjen Poutsma + */ +public class BlockingSignalQueue { + + private final BlockingQueue> queue = new LinkedBlockingQueue>(); + + + /** + * Inserts the specified signal into this queue, waiting if necessary for space to + * become available. + * @param t the signal to add + */ + public void putSignal(T t) throws InterruptedException { + Assert.notNull(t, "'t' must not be null"); + Assert.state(!isComplete(), "Cannot put signal in queue after complete()"); + this.queue.put(new OnNext(t)); + } + + /** + * Inserts the specified error into this queue, waiting if necessary for space to + * become available. + * @param error the error to add + */ + public void putError(Throwable error) throws InterruptedException { + Assert.notNull(error, "'error' must not be null"); + Assert.state(!isComplete(), "Cannot putSignal errors in queue after complete()"); + this.queue.put(new OnError(error)); + } + + /** + * Marks the queue as complete. + */ + public void complete() throws InterruptedException { + this.queue.put(OnComplete.INSTANCE); + } + + /** + * Indicates whether the current head of this queue is a signal. + * @return {@code true} if the current head is a signal; {@code false} otherwise + */ + public boolean isHeadSignal() { + Signal signal = this.queue.peek(); + return signal instanceof OnNext; + } + + /** + * Indicates whether the current head of this queue is a {@link Throwable}. + * @return {@code true} if the current head is an error; {@code false} otherwise + */ + public boolean isHeadError() { + Signal signal = this.queue.peek(); + return signal instanceof OnError; + } + + /** + * Indicates whether there are more buffers or errors in this queue. + * @return {@code true} if there more elements in this queue; {@code false} otherwise + */ + public boolean isComplete() { + Signal signal = this.queue.peek(); + return OnComplete.INSTANCE == signal; + } + + /** + * Retrieves and removes the signal head of this queue. Should only be called after + * {@link #isHeadSignal()} returns {@code true}. + * @return the head of the queue + * @throws IllegalStateException if the current head of this queue is not a buffer + * @see #isHeadSignal() + */ + public T pollSignal() throws InterruptedException { + Signal signal = this.queue.take(); + return signal != null ? signal.next() : null; + } + + /** + * Retrieves and removes the buffer error of this queue. Should only be called after + * {@link #isHeadError()} returns {@code true}. + * @return the head of the queue, as error + * @throws IllegalStateException if the current head of this queue is not a error + * @see #isHeadError() + */ + public Throwable pollError() throws InterruptedException { + Signal signal = this.queue.take(); + return signal != null ? signal.error() : null; + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/rx/util/BlockingByteBufQueuePublisher.java b/spring-web-reactive/src/main/java/org/springframework/rx/util/BlockingSignalQueuePublisher.java similarity index 82% rename from spring-web-reactive/src/main/java/org/springframework/rx/util/BlockingByteBufQueuePublisher.java rename to spring-web-reactive/src/main/java/org/springframework/rx/util/BlockingSignalQueuePublisher.java index 1bbd763ba5..a542cd8dbf 100644 --- a/spring-web-reactive/src/main/java/org/springframework/rx/util/BlockingByteBufQueuePublisher.java +++ b/spring-web-reactive/src/main/java/org/springframework/rx/util/BlockingSignalQueuePublisher.java @@ -16,7 +16,6 @@ package org.springframework.rx.util; -import io.netty.buffer.ByteBuf; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; @@ -26,21 +25,21 @@ import org.springframework.util.Assert; /** * @author Arjen Poutsma */ -public class BlockingByteBufQueuePublisher implements Publisher { +public class BlockingSignalQueuePublisher implements Publisher { - private final BlockingByteBufQueue queue; + private final BlockingSignalQueue queue; - private Subscriber subscriber; + private Subscriber subscriber; private final Object subscriberMutex = new Object(); - public BlockingByteBufQueuePublisher(BlockingByteBufQueue queue) { + public BlockingSignalQueuePublisher(BlockingSignalQueue queue) { Assert.notNull(queue, "'queue' must not be null"); this.queue = queue; } @Override - public void subscribe(Subscriber subscriber) { + public void subscribe(Subscriber subscriber) { synchronized (this.subscriberMutex) { if (this.subscriber != null) { subscriber.onError( @@ -74,10 +73,10 @@ public class BlockingByteBufQueuePublisher implements Publisher { @Override public void run() { try { - while (!Thread.currentThread().isInterrupted()) + while (!Thread.currentThread().isInterrupted()) { if ((l < requestCount || requestCount == Long.MAX_VALUE) && - queue.isHeadBuffer()) { - subscriber.onNext(queue.pollBuffer()); + queue.isHeadSignal()) { + subscriber.onNext(queue.pollSignal()); l++; } else if (queue.isHeadError()) { @@ -88,6 +87,7 @@ public class BlockingByteBufQueuePublisher implements Publisher { subscriber.onComplete(); break; } + } } catch (InterruptedException ex) { // Allow thread to exit diff --git a/spring-web-reactive/src/main/java/org/springframework/rx/util/BlockingSignalQueueSubscriber.java b/spring-web-reactive/src/main/java/org/springframework/rx/util/BlockingSignalQueueSubscriber.java new file mode 100644 index 0000000000..b17c0d709c --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/rx/util/BlockingSignalQueueSubscriber.java @@ -0,0 +1,113 @@ +/* + * Copyright 2002-2015 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.rx.util; + +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +import org.springframework.util.Assert; + +/** + * A simple byte array {@link Subscriber} that puts all published bytes on a + * {@link @BlockingSignalQueue}. + * + * @author Arjen Poutsma + */ +public class BlockingSignalQueueSubscriber implements Subscriber { + + /** + * The default request size to use. + */ + public static final int DEFAULT_REQUEST_SIZE = 1; + + private final BlockingSignalQueue queue; + + private Subscription subscription; + + private int initialRequestSize = DEFAULT_REQUEST_SIZE; + + private int requestSize = DEFAULT_REQUEST_SIZE; + + + /** + * Creates a new {@code BlockingSignalQueueSubscriber} using the given queue. + * @param queue the queue to use + */ + public BlockingSignalQueueSubscriber(BlockingSignalQueue queue) { + Assert.notNull(queue, "'queue' must not be null"); + this.queue = queue; + } + + /** + * Sets the request size used when subscribing, in {@link #onSubscribe(Subscription)}. + * Defaults to {@link #DEFAULT_REQUEST_SIZE}. + * @param initialRequestSize the initial request size + * @see Subscription#request(long) + */ + public void setInitialRequestSize(int initialRequestSize) { + this.initialRequestSize = initialRequestSize; + } + + /** + * Sets the request size used after data or an error comes in, in {@link + * #onNext(Object)} and {@link #onError(Throwable)}. Defaults to {@link + * #DEFAULT_REQUEST_SIZE}. + * @see Subscription#request(long) + */ + public void setRequestSize(int requestSize) { + this.requestSize = requestSize; + } + + @Override + public void onSubscribe(Subscription subscription) { + this.subscription = subscription; + + this.subscription.request(this.initialRequestSize); + } + + @Override + public void onNext(T t) { + try { + this.queue.putSignal(t); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + this.subscription.request(requestSize); + } + + @Override + public void onError(Throwable t) { + try { + this.queue.putError(t); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + this.subscription.request(requestSize); + } + + @Override + public void onComplete() { + try { + this.queue.complete(); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } +} diff --git a/spring-web-reactive/src/main/java/org/springframework/rx/util/OnComplete.java b/spring-web-reactive/src/main/java/org/springframework/rx/util/OnComplete.java new file mode 100644 index 0000000000..c306e584cf --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/rx/util/OnComplete.java @@ -0,0 +1,65 @@ +/* + * Copyright 2002-2015 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.rx.util; + +import org.reactivestreams.Subscription; + +/** + * @author Arjen Poutsma + */ +class OnComplete implements Signal { + + public static final OnComplete INSTANCE = new OnComplete(); + + private OnComplete() { + } + + @Override + public boolean isComplete() { + return true; + } + + @Override + public boolean isOnNext() { + return false; + } + + @Override + public Object next() { + throw new IllegalStateException(); + } + + @Override + public boolean isOnError() { + return false; + } + + @Override + public Throwable error() { + throw new IllegalStateException(); + } + + @Override + public boolean isOnSubscribe() { + return false; + } + + @Override + public Subscription subscription() { + throw new IllegalStateException(); + } +} diff --git a/spring-web-reactive/src/main/java/org/springframework/rx/util/OnError.java b/spring-web-reactive/src/main/java/org/springframework/rx/util/OnError.java new file mode 100644 index 0000000000..0135485932 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/rx/util/OnError.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2015 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.rx.util; + +import org.reactivestreams.Subscription; + +import org.springframework.util.Assert; + +/** + * @author Arjen Poutsma + */ +final class OnError implements Signal { + + private final Throwable error; + + public OnError(Throwable error) { + Assert.notNull(error, "'error' must not be null"); + this.error = error; + } + + @Override + public boolean isOnError() { + return true; + } + + @Override + public Throwable error() { + return error; + } + + @Override + public boolean isOnNext() { + return false; + } + + @Override + public Object next() { + throw new IllegalStateException(); + } + + @Override + public boolean isOnSubscribe() { + return false; + } + + @Override + public Subscription subscription() { + throw new IllegalStateException(); + } + + @Override + public boolean isComplete() { + return false; + } +} diff --git a/spring-web-reactive/src/main/java/org/springframework/rx/util/OnNext.java b/spring-web-reactive/src/main/java/org/springframework/rx/util/OnNext.java new file mode 100644 index 0000000000..56e052e1aa --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/rx/util/OnNext.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2015 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.rx.util; + +import org.reactivestreams.Subscription; + +import org.springframework.util.Assert; + +/** + * @author Arjen Poutsma + */ +class OnNext implements Signal { + + private final T next; + + public OnNext(T next) { + Assert.notNull(next, "'next' must not be null"); + this.next = next; + } + + @Override + public boolean isOnNext() { + return true; + } + + @Override + public T next() { + return next; + } + + + @Override + public boolean isOnError() { + return false; + } + + @Override + public Throwable error() { + throw new IllegalStateException(); + } + + @Override + public boolean isOnSubscribe() { + return false; + } + + @Override + public Subscription subscription() { + throw new IllegalStateException(); + } + + @Override + public boolean isComplete() { + return false; + } +} diff --git a/spring-web-reactive/src/main/java/org/springframework/rx/util/OnSubscribe.java b/spring-web-reactive/src/main/java/org/springframework/rx/util/OnSubscribe.java new file mode 100644 index 0000000000..d4731a403b --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/rx/util/OnSubscribe.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2015 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.rx.util; + +import org.reactivestreams.Subscription; + +import org.springframework.util.Assert; + +/** + * @author Arjen Poutsma + */ +class OnSubscribe implements Signal { + + private final Subscription subscription; + + public OnSubscribe(Subscription subscription) { + Assert.notNull(subscription, "'subscription' must not be null"); + this.subscription = subscription; + } + + @Override + public boolean isOnSubscribe() { + return true; + } + + @Override + public Subscription subscription() { + return null; + } + + + @Override + public boolean isOnNext() { + return false; + } + + @Override + public Object next() { + throw new IllegalStateException(); + } + + @Override + public boolean isOnError() { + return false; + } + + @Override + public Throwable error() { + throw new IllegalStateException(); + } + + @Override + public boolean isComplete() { + return false; + } +} diff --git a/spring-web-reactive/src/main/java/org/springframework/rx/util/Signal.java b/spring-web-reactive/src/main/java/org/springframework/rx/util/Signal.java new file mode 100644 index 0000000000..1833ef210e --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/rx/util/Signal.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2015 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.rx.util; + +import org.reactivestreams.Subscription; + +/** + * @author Arjen Poutsma + */ +interface Signal { + + boolean isOnNext(); + + T next(); + + boolean isOnError(); + + Throwable error(); + + boolean isOnSubscribe(); + + Subscription subscription(); + + boolean isComplete(); +} diff --git a/spring-web-reactive/src/main/java/org/springframework/rx/web/servlet/AsyncContextSynchronizer.java b/spring-web-reactive/src/main/java/org/springframework/rx/web/servlet/AsyncContextSynchronizer.java new file mode 100644 index 0000000000..9d2b8d0880 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/rx/web/servlet/AsyncContextSynchronizer.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2015 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.rx.web.servlet; + +import java.io.IOException; +import java.util.concurrent.atomic.AtomicInteger; +import javax.servlet.AsyncContext; +import javax.servlet.ServletInputStream; +import javax.servlet.ServletOutputStream; + +/** + * @author Arjen Poutsma + */ +class AsyncContextSynchronizer { + + private static final int READ_COMPLETE = 1; + + private static final int WRITE_COMPLETE = 1 << 1; + + private static final int COMPLETE = READ_COMPLETE | WRITE_COMPLETE; + + private final AsyncContext asyncContext; + + private final AtomicInteger complete = new AtomicInteger(0); + + public AsyncContextSynchronizer(AsyncContext asyncContext) { + this.asyncContext = asyncContext; + } + + public ServletInputStream getInputStream() throws IOException { + return this.asyncContext.getRequest().getInputStream(); + } + + public ServletOutputStream getOutputStream() throws IOException { + return this.asyncContext.getResponse().getOutputStream(); + } + + public void readComplete() { + if (complete.compareAndSet(WRITE_COMPLETE, COMPLETE)) { + this.asyncContext.complete(); + } + else { + this.complete.compareAndSet(0, READ_COMPLETE); + } + } + + public void writeComplete() { + if (complete.compareAndSet(READ_COMPLETE, COMPLETE)) { + this.asyncContext.complete(); + } + else { + this.complete.compareAndSet(0, WRITE_COMPLETE); + } + } +} diff --git a/spring-web-reactive/src/test/java/org/springframework/rx/io/ByteBufPublisherInputStreamTests.java b/spring-web-reactive/src/test/java/org/springframework/rx/io/ByteBufPublisherInputStreamTests.java index 24a7d9b0e9..cd8fa33ddf 100644 --- a/spring-web-reactive/src/test/java/org/springframework/rx/io/ByteBufPublisherInputStreamTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/rx/io/ByteBufPublisherInputStreamTests.java @@ -16,15 +16,10 @@ package org.springframework.rx.io; -import java.io.EOFException; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; import org.junit.Before; import org.junit.Test; -import org.springframework.rx.util.BlockingByteBufQueue; -import org.springframework.rx.util.BlockingByteBufQueuePublisher; +import org.springframework.rx.util.BlockingSignalQueue; import static org.junit.Assert.*; @@ -33,25 +28,22 @@ import static org.junit.Assert.*; */ public class ByteBufPublisherInputStreamTests { - private BlockingByteBufQueue queue; + private BlockingSignalQueue queue; - private ByteBufPublisherInputStream is; + private ByteArrayPublisherInputStream is; @Before public void setUp() throws Exception { - queue = new BlockingByteBufQueue(); - is = new ByteBufPublisherInputStream(queue); + queue = new BlockingSignalQueue(); + is = new ByteArrayPublisherInputStream(queue); } @Test public void readSingleByte() throws Exception { - ByteBuf abc = Unpooled.copiedBuffer(new byte[]{'a', 'b', 'c'}); - ByteBuf def = Unpooled.copiedBuffer(new byte[]{'d', 'e', 'f'}); - - queue.putBuffer(abc); - queue.putBuffer(def); + queue.putSignal(new byte[]{'a', 'b', 'c'}); + queue.putSignal(new byte[]{'d', 'e', 'f'}); queue.complete(); @@ -75,11 +67,8 @@ public class ByteBufPublisherInputStreamTests { @Test public void readBytes() throws Exception { - ByteBuf abc = Unpooled.copiedBuffer(new byte[]{'a', 'b', 'c'}); - ByteBuf def = Unpooled.copiedBuffer(new byte[]{'d', 'e', 'f'}); - - queue.putBuffer(abc); - queue.putBuffer(def); + queue.putSignal(new byte[]{'a', 'b', 'c'}); + queue.putSignal(new byte[]{'d', 'e', 'f'}); queue.complete(); byte[] buf = new byte[2]; diff --git a/spring-web-reactive/src/test/java/org/springframework/rx/util/BlockingByteBufQueuePublisherTests.java b/spring-web-reactive/src/test/java/org/springframework/rx/util/BlockingByteBufQueuePublisherTests.java index 719324f4b2..5c14966818 100644 --- a/spring-web-reactive/src/test/java/org/springframework/rx/util/BlockingByteBufQueuePublisherTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/rx/util/BlockingByteBufQueuePublisherTests.java @@ -33,14 +33,14 @@ import org.reactivestreams.Subscription; */ public class BlockingByteBufQueuePublisherTests { - private BlockingByteBufQueue queue; + private BlockingSignalQueue queue; - private BlockingByteBufQueuePublisher publisher; + private BlockingSignalQueuePublisher publisher; @Before public void setUp() throws Exception { - queue = new BlockingByteBufQueue(); - publisher = new BlockingByteBufQueuePublisher(queue); + queue = new BlockingSignalQueue(); + publisher = new BlockingSignalQueuePublisher(queue); } @Test @@ -48,8 +48,8 @@ public class BlockingByteBufQueuePublisherTests { ByteBuf abc = Unpooled.copiedBuffer(new byte[]{'a', 'b', 'c'}); ByteBuf def = Unpooled.copiedBuffer(new byte[]{'d', 'e', 'f'}); - queue.putBuffer(abc); - queue.putBuffer(def); + queue.putSignal(abc); + queue.putSignal(def); queue.complete(); final AtomicBoolean complete = new AtomicBoolean(false); @@ -90,8 +90,8 @@ public class BlockingByteBufQueuePublisherTests { ByteBuf abc = Unpooled.copiedBuffer(new byte[]{'a', 'b', 'c'}); ByteBuf def = Unpooled.copiedBuffer(new byte[]{'d', 'e', 'f'}); - queue.putBuffer(abc); - queue.putBuffer(def); + queue.putSignal(abc); + queue.putSignal(def); queue.complete(); final AtomicBoolean complete = new AtomicBoolean(false); diff --git a/spring-web-reactive/src/test/java/org/springframework/rx/util/BlockingByteBufQueueTests.java b/spring-web-reactive/src/test/java/org/springframework/rx/util/BlockingByteBufQueueTests.java index 5f16234fcc..882c6a13c2 100644 --- a/spring-web-reactive/src/test/java/org/springframework/rx/util/BlockingByteBufQueueTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/rx/util/BlockingByteBufQueueTests.java @@ -27,11 +27,11 @@ import org.junit.Test; */ public class BlockingByteBufQueueTests { - private BlockingByteBufQueue queue; + private BlockingSignalQueue queue; @Before public void setUp() throws Exception { - queue = new BlockingByteBufQueue(); + queue = new BlockingSignalQueue(); } @Test @@ -39,41 +39,37 @@ public class BlockingByteBufQueueTests { ByteBuf abc = Unpooled.copiedBuffer(new byte[]{'a', 'b', 'c'}); ByteBuf def = Unpooled.copiedBuffer(new byte[]{'d', 'e', 'f'}); - queue.putBuffer(abc); - queue.putBuffer(def); + queue.putSignal(abc); + queue.putSignal(def); queue.complete(); - assertTrue(queue.isHeadBuffer()); + assertTrue(queue.isHeadSignal()); assertFalse(queue.isHeadError()); - assertSame(abc, queue.pollBuffer()); + assertSame(abc, queue.pollSignal()); - assertTrue(queue.isHeadBuffer()); + assertTrue(queue.isHeadSignal()); assertFalse(queue.isHeadError()); - assertSame(def, queue.pollBuffer()); + assertSame(def, queue.pollSignal()); assertTrue(queue.isComplete()); } - @Test - public void empty() throws Exception { - assertNull(queue.pollBuffer()); - } @Test public void error() throws Exception { ByteBuf abc = Unpooled.copiedBuffer(new byte[]{'a', 'b', 'c'}); Throwable error = new IllegalStateException(); - queue.putBuffer(abc); + queue.putSignal(abc); queue.putError(error); queue.complete(); - assertTrue(queue.isHeadBuffer()); + assertTrue(queue.isHeadSignal()); assertFalse(queue.isHeadError()); - assertSame(abc, queue.pollBuffer()); + assertSame(abc, queue.pollSignal()); assertTrue(queue.isHeadError()); - assertFalse(queue.isHeadBuffer()); + assertFalse(queue.isHeadSignal()); assertSame(error, queue.pollError()); assertTrue(queue.isComplete()); From 3b45087c87a3386f74be88cb918a4d2055ae5400 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Wed, 1 Jul 2015 15:03:14 +0200 Subject: [PATCH 003/498] work, work. --- .../util/AbstractUnicastAsyncSubscriber.java | 214 ------------------ .../util/AbstractUnicastSyncSubscriber.java | 76 ------- .../rx/util/BlockingSignalQueue.java | 134 ++++++++++- .../springframework/rx/util/OnComplete.java | 65 ------ .../org/springframework/rx/util/OnError.java | 69 ------ .../org/springframework/rx/util/OnNext.java | 70 ------ .../springframework/rx/util/OnSubscribe.java | 70 ------ .../org/springframework/rx/util/Signal.java | 39 ---- .../BlockingByteBufQueuePublisherTests.java | 97 ++++++-- .../rx/util/BlockingByteBufQueueTests.java | 15 +- 10 files changed, 205 insertions(+), 644 deletions(-) delete mode 100644 spring-web-reactive/src/main/java/org/springframework/rx/util/AbstractUnicastAsyncSubscriber.java delete mode 100644 spring-web-reactive/src/main/java/org/springframework/rx/util/AbstractUnicastSyncSubscriber.java delete mode 100644 spring-web-reactive/src/main/java/org/springframework/rx/util/OnComplete.java delete mode 100644 spring-web-reactive/src/main/java/org/springframework/rx/util/OnError.java delete mode 100644 spring-web-reactive/src/main/java/org/springframework/rx/util/OnNext.java delete mode 100644 spring-web-reactive/src/main/java/org/springframework/rx/util/OnSubscribe.java delete mode 100644 spring-web-reactive/src/main/java/org/springframework/rx/util/Signal.java diff --git a/spring-web-reactive/src/main/java/org/springframework/rx/util/AbstractUnicastAsyncSubscriber.java b/spring-web-reactive/src/main/java/org/springframework/rx/util/AbstractUnicastAsyncSubscriber.java deleted file mode 100644 index aece160be8..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/rx/util/AbstractUnicastAsyncSubscriber.java +++ /dev/null @@ -1,214 +0,0 @@ -/* - * Copyright 2002-2015 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.rx.util; - -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.Executor; -import java.util.concurrent.atomic.AtomicBoolean; - -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; - -import org.springframework.util.Assert; - -/** - * @author Arjen Poutsma - */ -public abstract class AbstractUnicastAsyncSubscriber implements Subscriber { - - private final Executor executor; - - private Subscription subscription; - - private boolean done; - - protected AbstractUnicastAsyncSubscriber(Executor executor) { - Assert.notNull(executor, "'executor' must not be null"); - - this.executor = executor; - } - - private void done() { - done = true; - - if (subscription != null) { - subscription.cancel(); - } - } - - // This method is invoked when the OnNext signals arrive - // Returns whether more elements are desired or not, and if no more elements are desired, - // for convenience. - protected abstract boolean whenNext(final T element); - - // This method is invoked when the OnComplete signal arrives - // override this method to implement your own custom onComplete logic. - protected void whenComplete() { - } - - // This method is invoked if the OnError signal arrives - // override this method to implement your own custom onError logic. - protected void whenError(Throwable error) { - } - - private void handleOnSubscribe(Subscription subscription) { - if (subscription == null) { - return; - } - if (this.subscription != null) { - subscription.cancel(); - } - else { - this.subscription = subscription; - this.subscription.request(1); - } - } - - private void handleOnNext(final T element) { - if (!done) { - try { - if (whenNext(element)) { - subscription.request(1); - } - else { - done(); - } - } - catch (final Throwable t) { - done(); - onError(t); - } - } - } - - private void handleOnComplete() { - done = true; - whenComplete(); - } - - private void handleOnError(final Throwable error) { - done = true; - whenError(error); - } - - // We implement the OnX methods on `Subscriber` to send Signals that we will process asycnhronously, but only one at a time - - @Override - public final void onSubscribe(final Subscription s) { - // As per rule 2.13, we need to throw a `java.lang.NullPointerException` if the `Subscription` is `null` - if (s == null) { - throw null; - } - - signal(new OnSubscribe(s)); - } - - @Override - public final void onNext(final T element) { - // As per rule 2.13, we need to throw a `java.lang.NullPointerException` if the `element` is `null` - if (element == null) { - throw null; - } - - signal(new OnNext(element)); - } - - @Override - public final void onError(final Throwable t) { - // As per rule 2.13, we need to throw a `java.lang.NullPointerException` if the `Throwable` is `null` - if (t == null) { - throw null; - } - - signal(new OnError(t)); - } - - @Override - public final void onComplete() { - signal(OnComplete.INSTANCE); - } - - private final ConcurrentLinkedQueue> inboundSignals = - new ConcurrentLinkedQueue>(); - - private final AtomicBoolean enabled = new AtomicBoolean(false); - - // What `signal` does is that it sends signals to the `Subscription` asynchronously - private void signal(final Signal signal) { - if (inboundSignals - .offer(signal)) // No need to null-check here as ConcurrentLinkedQueue does this for us - { - tryScheduleToExecute(); // Then we try to schedule it for execution, if it isn't already - } - } - - // This method makes sure that this `Subscriber` is only executing on one Thread at a time - private void tryScheduleToExecute() { - if (enabled.compareAndSet(false, true)) { - try { - executor.execute(new SignalRunnable()); - } - catch (Throwable t) { // If we can't run on the `Executor`, we need to fail gracefully and not violate rule 2.13 - if (!done) { - try { - done(); // First of all, this failure is not recoverable, so we need to cancel our subscription - } - finally { - inboundSignals.clear(); // We're not going to need these anymore - // This subscription is cancelled by now, but letting the Subscriber become schedulable again means - // that we can drain the inboundSignals queue if anything arrives after clearing - enabled.set(false); - } - } - } - } - } - - private class SignalRunnable implements Runnable { - - @Override - public void run() { - if (enabled.get()) { - try { - Signal s = inboundSignals.poll(); - if (!done) { - if (s.isOnNext()) { - handleOnNext(s.next()); - } - else if (s.isOnSubscribe()) { - handleOnSubscribe(s.subscription()); - } - else if (s.isOnError()) { - handleOnError(s.error()); - } - else if (s.isComplete()) { - handleOnComplete(); - } - } - } - finally { - enabled.set(false); - - if (!inboundSignals.isEmpty()) { - tryScheduleToExecute(); - } - } - } - - } - } -} \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/rx/util/AbstractUnicastSyncSubscriber.java b/spring-web-reactive/src/main/java/org/springframework/rx/util/AbstractUnicastSyncSubscriber.java deleted file mode 100644 index 85e4764d50..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/rx/util/AbstractUnicastSyncSubscriber.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2002-2015 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.rx.util; - -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; - -/** - * @author Arjen Poutsma - */ -public abstract class AbstractUnicastSyncSubscriber implements Subscriber { - - private Subscription subscription; - - private boolean done = false; - - @Override - public final void onSubscribe(Subscription subscription) { - if (subscription == null) { - throw new NullPointerException(); - } - - if (this.subscription != null) { - subscription.cancel(); - } - else { - this.subscription = subscription; - this.subscription.request(1); - } - } - - @Override - public final void onNext(T element) { - if (element == null) { - throw new NullPointerException(); - } - - if (!done) { - try { - if (onNextInternal(element)) { - subscription.request(1); - } - else { - done(); - } - } - catch (Throwable t) { - done(); - onError(t); - } - } - } - - private void done() { - done = true; - subscription.cancel(); - } - - protected abstract boolean onNextInternal(final T element) throws Exception; - - -} diff --git a/spring-web-reactive/src/main/java/org/springframework/rx/util/BlockingSignalQueue.java b/spring-web-reactive/src/main/java/org/springframework/rx/util/BlockingSignalQueue.java index f30f7470d4..476ab16eb4 100644 --- a/spring-web-reactive/src/main/java/org/springframework/rx/util/BlockingSignalQueue.java +++ b/spring-web-reactive/src/main/java/org/springframework/rx/util/BlockingSignalQueue.java @@ -19,8 +19,6 @@ package org.springframework.rx.util; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; -import io.netty.buffer.ByteBuf; - import org.springframework.util.Assert; /** @@ -29,8 +27,8 @@ import org.springframework.util.Assert; * streams. * *

Typically, this class will be used by two threads: one thread to put new elements on - * the stack by calling {@link #put(ByteBuf)}, possibly {@link #putError(Throwable)} and - * finally {@link #complete()}. The other thread will read elements by calling {@link + * the stack by calling {@link #putSignal(Object)}, possibly {@link #putError(Throwable)} + * and finally {@link #complete()}. The other thread will read elements by calling {@link * #isHeadSignal()}/{@link #pollSignal()} and {@link #isHeadError()}/{@link #pollError()}, * while keeping an eye on {@link #isComplete()}. * @author Arjen Poutsma @@ -48,7 +46,7 @@ public class BlockingSignalQueue { public void putSignal(T t) throws InterruptedException { Assert.notNull(t, "'t' must not be null"); Assert.state(!isComplete(), "Cannot put signal in queue after complete()"); - this.queue.put(new OnNext(t)); + this.queue.put(new OnNext(t)); } /** @@ -59,12 +57,13 @@ public class BlockingSignalQueue { public void putError(Throwable error) throws InterruptedException { Assert.notNull(error, "'error' must not be null"); Assert.state(!isComplete(), "Cannot putSignal errors in queue after complete()"); - this.queue.put(new OnError(error)); + this.queue.put(new OnError(error)); } /** * Marks the queue as complete. */ + @SuppressWarnings("unchecked") public void complete() throws InterruptedException { this.queue.put(OnComplete.INSTANCE); } @@ -75,7 +74,7 @@ public class BlockingSignalQueue { */ public boolean isHeadSignal() { Signal signal = this.queue.peek(); - return signal instanceof OnNext; + return signal != null && signal.isOnNext(); } /** @@ -84,7 +83,7 @@ public class BlockingSignalQueue { */ public boolean isHeadError() { Signal signal = this.queue.peek(); - return signal instanceof OnError; + return signal != null && signal.isOnError(); } /** @@ -93,7 +92,7 @@ public class BlockingSignalQueue { */ public boolean isComplete() { Signal signal = this.queue.peek(); - return OnComplete.INSTANCE == signal; + return signal != null && signal.isComplete(); } /** @@ -120,4 +119,121 @@ public class BlockingSignalQueue { return signal != null ? signal.error() : null; } + private interface Signal { + + boolean isOnNext(); + + T next(); + + boolean isOnError(); + + Throwable error(); + + boolean isComplete(); + } + + private static class OnNext implements Signal { + + private final T next; + + public OnNext(T next) { + Assert.notNull(next, "'next' must not be null"); + this.next = next; + } + + @Override + public boolean isOnNext() { + return true; + } + + @Override + public T next() { + return next; + } + + @Override + public boolean isOnError() { + return false; + } + + @Override + public Throwable error() { + throw new IllegalStateException(); + } + + @Override + public boolean isComplete() { + return false; + } + } + + private static final class OnError implements Signal { + + private final Throwable error; + + public OnError(Throwable error) { + Assert.notNull(error, "'error' must not be null"); + this.error = error; + } + + @Override + public boolean isOnError() { + return true; + } + + @Override + public Throwable error() { + return error; + } + + @Override + public boolean isOnNext() { + return false; + } + + @Override + public T next() { + throw new IllegalStateException(); + } + + @Override + public boolean isComplete() { + return false; + } + } + + private static class OnComplete implements Signal { + + private static final OnComplete INSTANCE = new OnComplete(); + + private OnComplete() { + } + + @Override + public boolean isComplete() { + return true; + } + + @Override + public boolean isOnNext() { + return false; + } + + @Override + public T next() { + throw new IllegalStateException(); + } + + @Override + public boolean isOnError() { + return false; + } + + @Override + public Throwable error() { + throw new IllegalStateException(); + } + + } + } diff --git a/spring-web-reactive/src/main/java/org/springframework/rx/util/OnComplete.java b/spring-web-reactive/src/main/java/org/springframework/rx/util/OnComplete.java deleted file mode 100644 index c306e584cf..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/rx/util/OnComplete.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2002-2015 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.rx.util; - -import org.reactivestreams.Subscription; - -/** - * @author Arjen Poutsma - */ -class OnComplete implements Signal { - - public static final OnComplete INSTANCE = new OnComplete(); - - private OnComplete() { - } - - @Override - public boolean isComplete() { - return true; - } - - @Override - public boolean isOnNext() { - return false; - } - - @Override - public Object next() { - throw new IllegalStateException(); - } - - @Override - public boolean isOnError() { - return false; - } - - @Override - public Throwable error() { - throw new IllegalStateException(); - } - - @Override - public boolean isOnSubscribe() { - return false; - } - - @Override - public Subscription subscription() { - throw new IllegalStateException(); - } -} diff --git a/spring-web-reactive/src/main/java/org/springframework/rx/util/OnError.java b/spring-web-reactive/src/main/java/org/springframework/rx/util/OnError.java deleted file mode 100644 index 0135485932..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/rx/util/OnError.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2002-2015 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.rx.util; - -import org.reactivestreams.Subscription; - -import org.springframework.util.Assert; - -/** - * @author Arjen Poutsma - */ -final class OnError implements Signal { - - private final Throwable error; - - public OnError(Throwable error) { - Assert.notNull(error, "'error' must not be null"); - this.error = error; - } - - @Override - public boolean isOnError() { - return true; - } - - @Override - public Throwable error() { - return error; - } - - @Override - public boolean isOnNext() { - return false; - } - - @Override - public Object next() { - throw new IllegalStateException(); - } - - @Override - public boolean isOnSubscribe() { - return false; - } - - @Override - public Subscription subscription() { - throw new IllegalStateException(); - } - - @Override - public boolean isComplete() { - return false; - } -} diff --git a/spring-web-reactive/src/main/java/org/springframework/rx/util/OnNext.java b/spring-web-reactive/src/main/java/org/springframework/rx/util/OnNext.java deleted file mode 100644 index 56e052e1aa..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/rx/util/OnNext.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2002-2015 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.rx.util; - -import org.reactivestreams.Subscription; - -import org.springframework.util.Assert; - -/** - * @author Arjen Poutsma - */ -class OnNext implements Signal { - - private final T next; - - public OnNext(T next) { - Assert.notNull(next, "'next' must not be null"); - this.next = next; - } - - @Override - public boolean isOnNext() { - return true; - } - - @Override - public T next() { - return next; - } - - - @Override - public boolean isOnError() { - return false; - } - - @Override - public Throwable error() { - throw new IllegalStateException(); - } - - @Override - public boolean isOnSubscribe() { - return false; - } - - @Override - public Subscription subscription() { - throw new IllegalStateException(); - } - - @Override - public boolean isComplete() { - return false; - } -} diff --git a/spring-web-reactive/src/main/java/org/springframework/rx/util/OnSubscribe.java b/spring-web-reactive/src/main/java/org/springframework/rx/util/OnSubscribe.java deleted file mode 100644 index d4731a403b..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/rx/util/OnSubscribe.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2002-2015 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.rx.util; - -import org.reactivestreams.Subscription; - -import org.springframework.util.Assert; - -/** - * @author Arjen Poutsma - */ -class OnSubscribe implements Signal { - - private final Subscription subscription; - - public OnSubscribe(Subscription subscription) { - Assert.notNull(subscription, "'subscription' must not be null"); - this.subscription = subscription; - } - - @Override - public boolean isOnSubscribe() { - return true; - } - - @Override - public Subscription subscription() { - return null; - } - - - @Override - public boolean isOnNext() { - return false; - } - - @Override - public Object next() { - throw new IllegalStateException(); - } - - @Override - public boolean isOnError() { - return false; - } - - @Override - public Throwable error() { - throw new IllegalStateException(); - } - - @Override - public boolean isComplete() { - return false; - } -} diff --git a/spring-web-reactive/src/main/java/org/springframework/rx/util/Signal.java b/spring-web-reactive/src/main/java/org/springframework/rx/util/Signal.java deleted file mode 100644 index 1833ef210e..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/rx/util/Signal.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2002-2015 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.rx.util; - -import org.reactivestreams.Subscription; - -/** - * @author Arjen Poutsma - */ -interface Signal { - - boolean isOnNext(); - - T next(); - - boolean isOnError(); - - Throwable error(); - - boolean isOnSubscribe(); - - Subscription subscription(); - - boolean isComplete(); -} diff --git a/spring-web-reactive/src/test/java/org/springframework/rx/util/BlockingByteBufQueuePublisherTests.java b/spring-web-reactive/src/test/java/org/springframework/rx/util/BlockingByteBufQueuePublisherTests.java index 5c14966818..ea06be0397 100644 --- a/spring-web-reactive/src/test/java/org/springframework/rx/util/BlockingByteBufQueuePublisherTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/rx/util/BlockingByteBufQueuePublisherTests.java @@ -20,50 +20,53 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; -import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; -import static org.junit.Assert.*; import org.junit.Before; import org.junit.Test; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; +import static org.junit.Assert.*; + /** * @author Arjen Poutsma */ public class BlockingByteBufQueuePublisherTests { - private BlockingSignalQueue queue; + private BlockingSignalQueue queue; - private BlockingSignalQueuePublisher publisher; + private BlockingSignalQueuePublisher publisher; @Before public void setUp() throws Exception { - queue = new BlockingSignalQueue(); - publisher = new BlockingSignalQueuePublisher(queue); + queue = new BlockingSignalQueue(); + publisher = new BlockingSignalQueuePublisher(queue); } @Test public void normal() throws Exception { - ByteBuf abc = Unpooled.copiedBuffer(new byte[]{'a', 'b', 'c'}); - ByteBuf def = Unpooled.copiedBuffer(new byte[]{'d', 'e', 'f'}); + byte[] abc = new byte[]{'a', 'b', 'c'}; + byte[] def = new byte[]{'d', 'e', 'f'}; queue.putSignal(abc); queue.putSignal(def); queue.complete(); final AtomicBoolean complete = new AtomicBoolean(false); - final List received = new ArrayList(2); + final List received = new ArrayList(2); + + publisher.subscribe(new Subscriber() { + private Subscription subscription; - publisher.subscribe(new Subscriber() { @Override public void onSubscribe(Subscription s) { - s.request(2); + s.request(1); + this.subscription = s; } @Override - public void onNext(ByteBuf byteBuf) { - received.add(byteBuf); + public void onNext(byte[] bytes) { + received.add(bytes); + this.subscription.request(1); } @Override @@ -87,25 +90,25 @@ public class BlockingByteBufQueuePublisherTests { @Test public void unbounded() throws Exception { - ByteBuf abc = Unpooled.copiedBuffer(new byte[]{'a', 'b', 'c'}); - ByteBuf def = Unpooled.copiedBuffer(new byte[]{'d', 'e', 'f'}); + byte[] abc = new byte[]{'a', 'b', 'c'}; + byte[] def = new byte[]{'d', 'e', 'f'}; queue.putSignal(abc); queue.putSignal(def); queue.complete(); final AtomicBoolean complete = new AtomicBoolean(false); - final List received = new ArrayList(2); + final List received = new ArrayList(2); - publisher.subscribe(new Subscriber() { + publisher.subscribe(new Subscriber() { @Override public void onSubscribe(Subscription s) { s.request(Long.MAX_VALUE); } @Override - public void onNext(ByteBuf byteBuf) { - received.add(byteBuf); + public void onNext(byte[] bytes) { + received.add(bytes); } @Override @@ -129,14 +132,14 @@ public class BlockingByteBufQueuePublisherTests { @Test public void multipleSubscribe() throws Exception { - publisher.subscribe(new Subscriber() { + publisher.subscribe(new Subscriber() { @Override public void onSubscribe(Subscription s) { } @Override - public void onNext(ByteBuf byteBuf) { + public void onNext(byte[] bytes) { } @@ -150,14 +153,14 @@ public class BlockingByteBufQueuePublisherTests { } }); - publisher.subscribe(new Subscriber() { + publisher.subscribe(new Subscriber() { @Override public void onSubscribe(Subscription s) { fail("onSubscribe not expected"); } @Override - public void onNext(ByteBuf byteBuf) { + public void onNext(byte[] bytes) { fail("onNext not expected"); } @@ -171,8 +174,54 @@ public class BlockingByteBufQueuePublisherTests { fail("onComplete not expected"); } }); + } + @Test + public void cancel() throws Exception { + byte[] abc = new byte[]{'a', 'b', 'c'}; + byte[] def = new byte[]{'d', 'e', 'f'}; + + queue.putSignal(abc); + queue.putSignal(def); + queue.complete(); + + final AtomicBoolean complete = new AtomicBoolean(false); + final List received = new ArrayList(1); + + publisher.subscribe(new Subscriber() { + + private Subscription subscription; + + @Override + public void onSubscribe(Subscription s) { + s.request(1); + this.subscription = s; + } + + @Override + public void onNext(byte[] bytes) { + received.add(bytes); + this.subscription.cancel(); + complete.set(true); + } + + @Override + public void onError(Throwable t) { + fail("onError not expected"); + } + + @Override + public void onComplete() { + } + }); + + while (!complete.get()) { + } + + assertEquals(1, received.size()); + assertSame(abc, received.get(0)); } + } \ No newline at end of file diff --git a/spring-web-reactive/src/test/java/org/springframework/rx/util/BlockingByteBufQueueTests.java b/spring-web-reactive/src/test/java/org/springframework/rx/util/BlockingByteBufQueueTests.java index 882c6a13c2..3148d7df56 100644 --- a/spring-web-reactive/src/test/java/org/springframework/rx/util/BlockingByteBufQueueTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/rx/util/BlockingByteBufQueueTests.java @@ -16,28 +16,27 @@ package org.springframework.rx.util; -import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; -import static org.junit.Assert.*; import org.junit.Before; import org.junit.Test; +import static org.junit.Assert.*; + /** * @author Arjen Poutsma */ public class BlockingByteBufQueueTests { - private BlockingSignalQueue queue; + private BlockingSignalQueue queue; @Before public void setUp() throws Exception { - queue = new BlockingSignalQueue(); + queue = new BlockingSignalQueue(); } @Test public void normal() throws Exception { - ByteBuf abc = Unpooled.copiedBuffer(new byte[]{'a', 'b', 'c'}); - ByteBuf def = Unpooled.copiedBuffer(new byte[]{'d', 'e', 'f'}); + byte[] abc = new byte[]{'a', 'b', 'c'}; + byte[] def = new byte[]{'d', 'e', 'f'}; queue.putSignal(abc); queue.putSignal(def); @@ -57,7 +56,7 @@ public class BlockingByteBufQueueTests { @Test public void error() throws Exception { - ByteBuf abc = Unpooled.copiedBuffer(new byte[]{'a', 'b', 'c'}); + byte[] abc = new byte[]{'a', 'b', 'c'}; Throwable error = new IllegalStateException(); queue.putSignal(abc); From f518d76a771def7289ac4f85ddc66812f428a16f Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Tue, 7 Jul 2015 12:18:52 +0200 Subject: [PATCH 004/498] Working version of Servlet 3.1 <-> RS bridge. --- spring-web-reactive/build.gradle | 9 + .../rx/io/ByteArrayPublisherInputStream.java | 3 +- .../rx/io/ByteArrayPublisherOutputStream.java | 3 +- .../rx/util/BlockingSignalQueue.java | 210 ++++++++++-------- .../rx/util/BlockingSignalQueuePublisher.java | 110 --------- .../util/BlockingSignalQueueSubscriber.java | 113 ---------- .../springframework/rx/util/OnComplete.java | 54 +++++ .../org/springframework/rx/util/OnError.java | 57 +++++ .../org/springframework/rx/util/OnNext.java | 57 +++++ .../org/springframework/rx/util/Signal.java | 33 +++ .../web/servlet/AsyncContextSynchronizer.java | 9 + .../rx/web/servlet/HttpHandler.java | 28 +++ .../rx/web/servlet/HttpHandlerServlet.java | 62 ++++++ .../rx/web/servlet/RequestBodyPublisher.java | 142 ++++++++++++ .../web/servlet/ResponseBodySubscriber.java | 111 +++++++++ .../src/main/resources/log4j.properties | 6 + .../BlockingByteBufQueuePublisherTests.java | 5 +- ...HttpHandlerServletIntegrationTestCase.java | 96 ++++++++ .../rx/web/servlet/CountingHttpHandler.java | 64 ++++++ .../rx/web/servlet/EchoHandler.java | 30 +++ ...tpHandlerServletJettyIntegrationTests.java | 56 +++++ ...pHandlerServletTomcatIntegrationTests.java | 55 +++++ 22 files changed, 997 insertions(+), 316 deletions(-) delete mode 100644 spring-web-reactive/src/main/java/org/springframework/rx/util/BlockingSignalQueuePublisher.java delete mode 100644 spring-web-reactive/src/main/java/org/springframework/rx/util/BlockingSignalQueueSubscriber.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/rx/util/OnComplete.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/rx/util/OnError.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/rx/util/OnNext.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/rx/util/Signal.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/rx/web/servlet/HttpHandler.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/rx/web/servlet/HttpHandlerServlet.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/rx/web/servlet/RequestBodyPublisher.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/rx/web/servlet/ResponseBodySubscriber.java create mode 100644 spring-web-reactive/src/main/resources/log4j.properties create mode 100644 spring-web-reactive/src/test/java/org/springframework/rx/web/servlet/AbstractHttpHandlerServletIntegrationTestCase.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/rx/web/servlet/CountingHttpHandler.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/rx/web/servlet/EchoHandler.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/rx/web/servlet/HttpHandlerServletJettyIntegrationTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/rx/web/servlet/HttpHandlerServletTomcatIntegrationTests.java diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index c9c4c7150b..0807ee5280 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -27,6 +27,15 @@ dependencies { provided "javax.servlet:javax.servlet-api:3.1.0" testCompile "junit:junit:4.12" + testCompile "org.springframework:spring-web:4.1.2.RELEASE" + testCompile 'org.apache.tomcat:tomcat-util:8.0.23' + testCompile 'org.apache.tomcat.embed:tomcat-embed-core:8.0.23' + + testCompile 'org.eclipse.jetty:jetty-server:9.3.0.v20150612' + testCompile 'org.eclipse.jetty:jetty-servlet:9.3.0.v20150612' + + testCompile("log4j:log4j:1.2.16") + } diff --git a/spring-web-reactive/src/main/java/org/springframework/rx/io/ByteArrayPublisherInputStream.java b/spring-web-reactive/src/main/java/org/springframework/rx/io/ByteArrayPublisherInputStream.java index d3251c0451..3f5b882f44 100644 --- a/spring-web-reactive/src/main/java/org/springframework/rx/io/ByteArrayPublisherInputStream.java +++ b/spring-web-reactive/src/main/java/org/springframework/rx/io/ByteArrayPublisherInputStream.java @@ -21,7 +21,6 @@ import java.io.InputStream; import org.reactivestreams.Publisher; import org.springframework.rx.util.BlockingSignalQueue; -import org.springframework.rx.util.BlockingSignalQueueSubscriber; import org.springframework.util.Assert; /** @@ -44,7 +43,7 @@ public class ByteArrayPublisherInputStream extends InputStream { Assert.notNull(publisher, "'publisher' must not be null"); this.queue = new BlockingSignalQueue(); - publisher.subscribe(new BlockingSignalQueueSubscriber(this.queue)); + publisher.subscribe(this.queue.subscriber()); } ByteArrayPublisherInputStream(BlockingSignalQueue queue) { diff --git a/spring-web-reactive/src/main/java/org/springframework/rx/io/ByteArrayPublisherOutputStream.java b/spring-web-reactive/src/main/java/org/springframework/rx/io/ByteArrayPublisherOutputStream.java index a89b9daee8..80550a43c1 100644 --- a/spring-web-reactive/src/main/java/org/springframework/rx/io/ByteArrayPublisherOutputStream.java +++ b/spring-web-reactive/src/main/java/org/springframework/rx/io/ByteArrayPublisherOutputStream.java @@ -7,7 +7,6 @@ import java.util.Arrays; import org.reactivestreams.Publisher; import org.springframework.rx.util.BlockingSignalQueue; -import org.springframework.rx.util.BlockingSignalQueuePublisher; /** * {@code OutputStream} implementation that stores all written bytes, to be retrieved @@ -23,7 +22,7 @@ public class ByteArrayPublisherOutputStream extends OutputStream { * @return a publisher for the written bytes */ public Publisher toByteBufPublisher() { - return new BlockingSignalQueuePublisher(this.queue); + return this.queue.publisher(); } @Override diff --git a/spring-web-reactive/src/main/java/org/springframework/rx/util/BlockingSignalQueue.java b/spring-web-reactive/src/main/java/org/springframework/rx/util/BlockingSignalQueue.java index 476ab16eb4..890dc89eb6 100644 --- a/spring-web-reactive/src/main/java/org/springframework/rx/util/BlockingSignalQueue.java +++ b/spring-web-reactive/src/main/java/org/springframework/rx/util/BlockingSignalQueue.java @@ -19,6 +19,10 @@ package org.springframework.rx.util; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + import org.springframework.util.Assert; /** @@ -35,6 +39,8 @@ import org.springframework.util.Assert; */ public class BlockingSignalQueue { + private static final int DEFAULT_REQUEST_SIZE_SUBSCRIBER = 1; + private final BlockingQueue> queue = new LinkedBlockingQueue>(); @@ -119,121 +125,151 @@ public class BlockingSignalQueue { return signal != null ? signal.error() : null; } - private interface Signal { - - boolean isOnNext(); - - T next(); - - boolean isOnError(); - - Throwable error(); - - boolean isComplete(); + /** + * Returns a {@code Publisher} backed by this queue. + */ + public Publisher publisher() { + return new BlockingSignalQueuePublisher(); } - private static class OnNext implements Signal { + /** + * Returns a {@code Subscriber} backed by this queue. + */ + public Subscriber subscriber() { + return subscriber(DEFAULT_REQUEST_SIZE_SUBSCRIBER); + } - private final T next; + /** + * Returns a {@code Subscriber} backed by this queue, with the given request size. + * @see Subscription#request(long) + */ + public Subscriber subscriber(long requestSize) { + return new BlockingSignalQueueSubscriber(requestSize); + } - public OnNext(T next) { - Assert.notNull(next, "'next' must not be null"); - this.next = next; - } + private class BlockingSignalQueuePublisher implements Publisher { + + private Subscriber subscriber; + + private final Object subscriberMutex = new Object(); @Override - public boolean isOnNext() { - return true; + public void subscribe(Subscriber subscriber) { + synchronized (this.subscriberMutex) { + if (this.subscriber != null) { + subscriber.onError( + new IllegalStateException("Only one subscriber allowed")); + } + else { + this.subscriber = subscriber; + final SubscriptionThread thread = new SubscriptionThread(); + this.subscriber.onSubscribe(new Subscription() { + @Override + public void request(long n) { + thread.request(n); + } + + @Override + public void cancel() { + thread.cancel(); + } + }); + thread.start(); + } + } } - @Override - public T next() { - return next; - } + private class SubscriptionThread extends Thread { - @Override - public boolean isOnError() { - return false; - } + private volatile long demand = 0; - @Override - public Throwable error() { - throw new IllegalStateException(); - } + @Override + public void run() { + try { + while (!Thread.currentThread().isInterrupted()) { + if (demand > 0 && isHeadSignal()) { + subscriber.onNext(pollSignal()); + if (demand != Long.MAX_VALUE) { + demand--; + } + } + else if (isHeadError()) { + subscriber.onError(pollError()); + break; + } + else if (isComplete()) { + subscriber.onComplete(); + break; + } + } + } + catch (InterruptedException ex) { + // Allow thread to exit + } + } - @Override - public boolean isComplete() { - return false; + public void request(long n) { + if (n != Long.MAX_VALUE) { + this.demand += n; + } + else { + this.demand = Long.MAX_VALUE; + } + } + + public void cancel() { + interrupt(); + } } } - private static final class OnError implements Signal { + private class BlockingSignalQueueSubscriber implements Subscriber { - private final Throwable error; + private final long requestSize; - public OnError(Throwable error) { - Assert.notNull(error, "'error' must not be null"); - this.error = error; + private Subscription subscription; + + public BlockingSignalQueueSubscriber(long requestSize) { + this.requestSize = requestSize; } @Override - public boolean isOnError() { - return true; + public void onSubscribe(Subscription subscription) { + this.subscription = subscription; + + this.subscription.request(this.requestSize); } @Override - public Throwable error() { - return error; + public void onNext(T t) { + try { + putSignal(t); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + this.subscription.request(requestSize); } @Override - public boolean isOnNext() { - return false; + public void onError(Throwable t) { + try { + putError(t); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + this.subscription.request(requestSize); } @Override - public T next() { - throw new IllegalStateException(); - } - - @Override - public boolean isComplete() { - return false; + public void onComplete() { + try { + complete(); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } } } - - private static class OnComplete implements Signal { - - private static final OnComplete INSTANCE = new OnComplete(); - - private OnComplete() { - } - - @Override - public boolean isComplete() { - return true; - } - - @Override - public boolean isOnNext() { - return false; - } - - @Override - public T next() { - throw new IllegalStateException(); - } - - @Override - public boolean isOnError() { - return false; - } - - @Override - public Throwable error() { - throw new IllegalStateException(); - } - - } - } diff --git a/spring-web-reactive/src/main/java/org/springframework/rx/util/BlockingSignalQueuePublisher.java b/spring-web-reactive/src/main/java/org/springframework/rx/util/BlockingSignalQueuePublisher.java deleted file mode 100644 index a542cd8dbf..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/rx/util/BlockingSignalQueuePublisher.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright 2002-2015 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.rx.util; - -import org.reactivestreams.Publisher; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; - -import org.springframework.util.Assert; - -/** - * @author Arjen Poutsma - */ -public class BlockingSignalQueuePublisher implements Publisher { - - private final BlockingSignalQueue queue; - - private Subscriber subscriber; - - private final Object subscriberMutex = new Object(); - - public BlockingSignalQueuePublisher(BlockingSignalQueue queue) { - Assert.notNull(queue, "'queue' must not be null"); - this.queue = queue; - } - - @Override - public void subscribe(Subscriber subscriber) { - synchronized (this.subscriberMutex) { - if (this.subscriber != null) { - subscriber.onError( - new IllegalStateException("Only one subscriber allowed")); - } - else { - this.subscriber = subscriber; - final SubscriptionThread thread = new SubscriptionThread(); - this.subscriber.onSubscribe(new Subscription() { - @Override - public void request(long n) { - thread.request(n); - } - - @Override - public void cancel() { - thread.cancel(); - } - }); - thread.start(); - } - } - } - - private class SubscriptionThread extends Thread { - - private volatile long requestCount = 0; - - private long l = 0; - - @Override - public void run() { - try { - while (!Thread.currentThread().isInterrupted()) { - if ((l < requestCount || requestCount == Long.MAX_VALUE) && - queue.isHeadSignal()) { - subscriber.onNext(queue.pollSignal()); - l++; - } - else if (queue.isHeadError()) { - subscriber.onError(queue.pollError()); - break; - } - else if (queue.isComplete()) { - subscriber.onComplete(); - break; - } - } - } - catch (InterruptedException ex) { - // Allow thread to exit - } - } - - public void request(long n) { - if (n != Long.MAX_VALUE) { - this.requestCount += n; - } - else { - this.requestCount = Long.MAX_VALUE; - } - } - - public void cancel() { - interrupt(); - } - } -} diff --git a/spring-web-reactive/src/main/java/org/springframework/rx/util/BlockingSignalQueueSubscriber.java b/spring-web-reactive/src/main/java/org/springframework/rx/util/BlockingSignalQueueSubscriber.java deleted file mode 100644 index b17c0d709c..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/rx/util/BlockingSignalQueueSubscriber.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright 2002-2015 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.rx.util; - -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; - -import org.springframework.util.Assert; - -/** - * A simple byte array {@link Subscriber} that puts all published bytes on a - * {@link @BlockingSignalQueue}. - * - * @author Arjen Poutsma - */ -public class BlockingSignalQueueSubscriber implements Subscriber { - - /** - * The default request size to use. - */ - public static final int DEFAULT_REQUEST_SIZE = 1; - - private final BlockingSignalQueue queue; - - private Subscription subscription; - - private int initialRequestSize = DEFAULT_REQUEST_SIZE; - - private int requestSize = DEFAULT_REQUEST_SIZE; - - - /** - * Creates a new {@code BlockingSignalQueueSubscriber} using the given queue. - * @param queue the queue to use - */ - public BlockingSignalQueueSubscriber(BlockingSignalQueue queue) { - Assert.notNull(queue, "'queue' must not be null"); - this.queue = queue; - } - - /** - * Sets the request size used when subscribing, in {@link #onSubscribe(Subscription)}. - * Defaults to {@link #DEFAULT_REQUEST_SIZE}. - * @param initialRequestSize the initial request size - * @see Subscription#request(long) - */ - public void setInitialRequestSize(int initialRequestSize) { - this.initialRequestSize = initialRequestSize; - } - - /** - * Sets the request size used after data or an error comes in, in {@link - * #onNext(Object)} and {@link #onError(Throwable)}. Defaults to {@link - * #DEFAULT_REQUEST_SIZE}. - * @see Subscription#request(long) - */ - public void setRequestSize(int requestSize) { - this.requestSize = requestSize; - } - - @Override - public void onSubscribe(Subscription subscription) { - this.subscription = subscription; - - this.subscription.request(this.initialRequestSize); - } - - @Override - public void onNext(T t) { - try { - this.queue.putSignal(t); - } - catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - } - this.subscription.request(requestSize); - } - - @Override - public void onError(Throwable t) { - try { - this.queue.putError(t); - } - catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - } - this.subscription.request(requestSize); - } - - @Override - public void onComplete() { - try { - this.queue.complete(); - } - catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - } - } -} diff --git a/spring-web-reactive/src/main/java/org/springframework/rx/util/OnComplete.java b/spring-web-reactive/src/main/java/org/springframework/rx/util/OnComplete.java new file mode 100644 index 0000000000..74e1cccc10 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/rx/util/OnComplete.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2015 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.rx.util; + +/** + * @author Arjen Poutsma + */ +class OnComplete implements Signal { + + public static final OnComplete INSTANCE = new OnComplete(); + + private OnComplete() { + } + + @Override + public boolean isComplete() { + return true; + } + + @Override + public boolean isOnNext() { + return false; + } + + @Override + public T next() { + throw new IllegalStateException(); + } + + @Override + public boolean isOnError() { + return false; + } + + @Override + public Throwable error() { + throw new IllegalStateException(); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/rx/util/OnError.java b/spring-web-reactive/src/main/java/org/springframework/rx/util/OnError.java new file mode 100644 index 0000000000..250e6f7727 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/rx/util/OnError.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2015 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.rx.util; + +import org.springframework.util.Assert; + +/** + * @author Arjen Poutsma + */ +final class OnError implements Signal { + + private final Throwable error; + + public OnError(Throwable error) { + Assert.notNull(error, "'error' must not be null"); + this.error = error; + } + + @Override + public boolean isOnError() { + return true; + } + + @Override + public Throwable error() { + return error; + } + + @Override + public boolean isOnNext() { + return false; + } + + @Override + public T next() { + throw new IllegalStateException(); + } + + @Override + public boolean isComplete() { + return false; + } +} diff --git a/spring-web-reactive/src/main/java/org/springframework/rx/util/OnNext.java b/spring-web-reactive/src/main/java/org/springframework/rx/util/OnNext.java new file mode 100644 index 0000000000..6d1aa6d8ca --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/rx/util/OnNext.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2015 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.rx.util; + +import org.springframework.util.Assert; + +/** + * @author Arjen Poutsma + */ +class OnNext implements Signal { + + private final T next; + + public OnNext(T next) { + Assert.notNull(next, "'next' must not be null"); + this.next = next; + } + + @Override + public boolean isOnNext() { + return true; + } + + @Override + public T next() { + return next; + } + + @Override + public boolean isOnError() { + return false; + } + + @Override + public Throwable error() { + throw new IllegalStateException(); + } + + @Override + public boolean isComplete() { + return false; + } +} diff --git a/spring-web-reactive/src/main/java/org/springframework/rx/util/Signal.java b/spring-web-reactive/src/main/java/org/springframework/rx/util/Signal.java new file mode 100644 index 0000000000..74ea5eacac --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/rx/util/Signal.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-2015 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.rx.util; + +/** + * @author Arjen Poutsma + */ +interface Signal { + + boolean isOnNext(); + + T next(); + + boolean isOnError(); + + Throwable error(); + + boolean isComplete(); +} diff --git a/spring-web-reactive/src/main/java/org/springframework/rx/web/servlet/AsyncContextSynchronizer.java b/spring-web-reactive/src/main/java/org/springframework/rx/web/servlet/AsyncContextSynchronizer.java index 9d2b8d0880..20be9c51b8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/rx/web/servlet/AsyncContextSynchronizer.java +++ b/spring-web-reactive/src/main/java/org/springframework/rx/web/servlet/AsyncContextSynchronizer.java @@ -22,11 +22,16 @@ import javax.servlet.AsyncContext; import javax.servlet.ServletInputStream; import javax.servlet.ServletOutputStream; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + /** * @author Arjen Poutsma */ class AsyncContextSynchronizer { + private static final Log logger = LogFactory.getLog(AsyncContextSynchronizer.class); + private static final int READ_COMPLETE = 1; private static final int WRITE_COMPLETE = 1 << 1; @@ -50,7 +55,9 @@ class AsyncContextSynchronizer { } public void readComplete() { + logger.debug("Read complete"); if (complete.compareAndSet(WRITE_COMPLETE, COMPLETE)) { + logger.debug("Complete"); this.asyncContext.complete(); } else { @@ -59,7 +66,9 @@ class AsyncContextSynchronizer { } public void writeComplete() { + logger.debug("Write complete"); if (complete.compareAndSet(READ_COMPLETE, COMPLETE)) { + logger.debug("Complete"); this.asyncContext.complete(); } else { diff --git a/spring-web-reactive/src/main/java/org/springframework/rx/web/servlet/HttpHandler.java b/spring-web-reactive/src/main/java/org/springframework/rx/web/servlet/HttpHandler.java new file mode 100644 index 0000000000..e9a3686959 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/rx/web/servlet/HttpHandler.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2015 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.rx.web.servlet; + +import org.reactivestreams.Publisher; + +/** + * @author Arjen Poutsma + */ +public interface HttpHandler { + + Publisher handle(Publisher request); + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/rx/web/servlet/HttpHandlerServlet.java b/spring-web-reactive/src/main/java/org/springframework/rx/web/servlet/HttpHandlerServlet.java new file mode 100644 index 0000000000..f19d380314 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/rx/web/servlet/HttpHandlerServlet.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2015 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.rx.web.servlet; + +import java.io.IOException; +import javax.servlet.AsyncContext; +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.reactivestreams.Publisher; + +/** + * @author Arjen Poutsma + */ +@WebServlet(asyncSupported = true ) +public class HttpHandlerServlet extends HttpServlet { + + private static final int BUFFER_SIZE = 4096; + + private HttpHandler handler; + + public void setHandler(HttpHandler handler) { + this.handler = handler; + } + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + AsyncContext context = request.startAsync(); + final AsyncContextSynchronizer contextSynchronizer = + new AsyncContextSynchronizer(context); + + RequestBodyPublisher requestPublisher = new RequestBodyPublisher(contextSynchronizer, BUFFER_SIZE); + request.getInputStream().setReadListener(requestPublisher); + + ResponseBodySubscriber responseSubscriber = new ResponseBodySubscriber(contextSynchronizer); + response.getOutputStream().setWriteListener(responseSubscriber); + + Publisher responsePublisher = this.handler.handle(requestPublisher); + + responsePublisher.subscribe(responseSubscriber); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/rx/web/servlet/RequestBodyPublisher.java b/spring-web-reactive/src/main/java/org/springframework/rx/web/servlet/RequestBodyPublisher.java new file mode 100644 index 0000000000..6022922c83 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/rx/web/servlet/RequestBodyPublisher.java @@ -0,0 +1,142 @@ +/* + * Copyright 2002-2015 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.rx.web.servlet; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.Arrays; +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +/** + * @author Arjen Poutsma + */ +public class RequestBodyPublisher implements ReadListener, Publisher { + + private final Charset UTF_8 = Charset.forName("UTF-8"); + + private static final Log logger = LogFactory.getLog(RequestBodyPublisher.class); + + private final AsyncContextSynchronizer synchronizer; + + private final byte[] buffer; + + private long demand; + + private Subscriber subscriber; + + public RequestBodyPublisher(AsyncContextSynchronizer synchronizer, int bufferSize) { + this.synchronizer = synchronizer; + this.buffer = new byte[bufferSize]; + } + + @Override + public void subscribe(Subscriber s) { + this.subscriber = s; + + this.subscriber.onSubscribe(new RequestBodySubscription()); + } + + @Override + public void onDataAvailable() throws IOException { + ServletInputStream input = this.synchronizer.getInputStream(); + + while (true) { + logger.debug("Demand: " + this.demand); + + if (demand <= 0) { + break; + } + + boolean ready = input.isReady(); + logger.debug("Input " + ready + "/" + input.isFinished()); + + if (!ready) { + break; + } + + int read = input.read(buffer); + logger.debug("Input read:" + read); + + if (read == -1) { + break; + } + else if (read > 0) { + if (demand != Long.MAX_VALUE) { + demand--; + } + byte[] copy = Arrays.copyOf(this.buffer, read); + +// logger.debug("Next: " + new String(copy, UTF_8)); + + this.subscriber.onNext(copy); + + } + } + } + + @Override + public void onAllDataRead() throws IOException { + logger.debug("All data read"); + this.synchronizer.readComplete(); + this.subscriber.onComplete(); + } + + @Override + public void onError(Throwable t) { + logger.error("RequestBodyPublisher Error", t); + this.subscriber.onError(t); + } + + private class RequestBodySubscription implements Subscription { + + @Override + public void request(long n) { + logger.debug("Updating demand " + demand + " by " + n); + + boolean stalled = demand <= 0; + + if (n != Long.MAX_VALUE && demand != Long.MAX_VALUE) { + demand += n; + } + else { + demand = Long.MAX_VALUE; + } + + if (stalled) { + try { + onDataAvailable(); + } + catch (IOException ex) { + onError(ex); + } + } + } + + @Override + public void cancel() { + synchronizer.readComplete(); + demand = 0; + } + } +} diff --git a/spring-web-reactive/src/main/java/org/springframework/rx/web/servlet/ResponseBodySubscriber.java b/spring-web-reactive/src/main/java/org/springframework/rx/web/servlet/ResponseBodySubscriber.java new file mode 100644 index 0000000000..f7faa4b16a --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/rx/web/servlet/ResponseBodySubscriber.java @@ -0,0 +1,111 @@ +/* + * Copyright 2002-2015 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.rx.web.servlet; + +import java.io.IOException; +import java.util.concurrent.atomic.AtomicBoolean; +import javax.servlet.ServletOutputStream; +import javax.servlet.WriteListener; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +import org.springframework.util.Assert; + +/** + * @author Arjen Poutsma + */ +public class ResponseBodySubscriber implements WriteListener, Subscriber { + + private static final Log logger = LogFactory.getLog(ResponseBodySubscriber.class); + + private final AsyncContextSynchronizer synchronizer; + + private Subscription subscription; + + private byte[] buffer; + + private AtomicBoolean complete = new AtomicBoolean(false); + + public ResponseBodySubscriber(AsyncContextSynchronizer synchronizer) { + this.synchronizer = synchronizer; + } + + @Override + public void onSubscribe(Subscription subscription) { + this.subscription = subscription; + this.subscription.request(1); + } + + @Override + public void onNext(byte[] bytes) { + logger.debug("Next: " + bytes.length + " bytes"); + + Assert.isNull(buffer); + + this.buffer = bytes; + try { + onWritePossible(); + } + catch (IOException e) { + onError(e); + } + } + + @Override + public void onComplete() { + logger.debug("Complete buffer: " + (buffer == null)); + + if (complete.compareAndSet(false, true) && buffer == null) { + this.synchronizer.writeComplete(); + } + } + + @Override + public void onWritePossible() throws IOException { + ServletOutputStream output = this.synchronizer.getOutputStream(); + + boolean ready = output.isReady(); + logger.debug("Output: " + ready + " buffer: " + (buffer == null)); + + if (this.buffer != null && ready) { + output.write(this.buffer); + this.buffer = null; + + if (!complete.get()) { + this.subscription.request(1); + } + else { + this.synchronizer.writeComplete(); + } + } + else if (this.buffer == null && ready) { + this.subscription.request(1); + } + } + + @Override + public void onError(Throwable t) { + logger.error("ResponseBodySubscriber error", t); + } + + + private void complete() { + } +} diff --git a/spring-web-reactive/src/main/resources/log4j.properties b/spring-web-reactive/src/main/resources/log4j.properties new file mode 100644 index 0000000000..a8eb1d4774 --- /dev/null +++ b/spring-web-reactive/src/main/resources/log4j.properties @@ -0,0 +1,6 @@ +log4j.rootCategory=INFO, stdout +log4j.logger.org.springframework.rx=DEBUG + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d %p [%25.25c{1}] <%t> - %m%n \ No newline at end of file diff --git a/spring-web-reactive/src/test/java/org/springframework/rx/util/BlockingByteBufQueuePublisherTests.java b/spring-web-reactive/src/test/java/org/springframework/rx/util/BlockingByteBufQueuePublisherTests.java index ea06be0397..b09781b107 100644 --- a/spring-web-reactive/src/test/java/org/springframework/rx/util/BlockingByteBufQueuePublisherTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/rx/util/BlockingByteBufQueuePublisherTests.java @@ -22,6 +22,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import org.junit.Before; import org.junit.Test; +import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; @@ -34,12 +35,12 @@ public class BlockingByteBufQueuePublisherTests { private BlockingSignalQueue queue; - private BlockingSignalQueuePublisher publisher; + private Publisher publisher; @Before public void setUp() throws Exception { queue = new BlockingSignalQueue(); - publisher = new BlockingSignalQueuePublisher(queue); + publisher = queue.publisher(); } @Test diff --git a/spring-web-reactive/src/test/java/org/springframework/rx/web/servlet/AbstractHttpHandlerServletIntegrationTestCase.java b/spring-web-reactive/src/test/java/org/springframework/rx/web/servlet/AbstractHttpHandlerServletIntegrationTestCase.java new file mode 100644 index 0000000000..50e009b297 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/rx/web/servlet/AbstractHttpHandlerServletIntegrationTestCase.java @@ -0,0 +1,96 @@ +/* + * Copyright 2002-2015 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.rx.web.servlet; + +import java.net.URI; +import java.util.Random; + +import org.junit.Test; + +import org.springframework.http.HttpMethod; +import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.util.SocketUtils; +import org.springframework.web.client.RestTemplate; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +public abstract class AbstractHttpHandlerServletIntegrationTestCase { + + private static final int REQUEST_SIZE = 4096 * 3; + + protected static int port = SocketUtils.findAvailableTcpPort(); + + private Random rnd = new Random(); + + + @Test + public void bytes() throws Exception { + RestTemplate restTemplate = new RestTemplate(); + + byte[] body = randomBytes(); + RequestEntity + request = new RequestEntity(body, HttpMethod.POST, new URI(url())); + ResponseEntity response = restTemplate.exchange(request, byte[].class); + + assertArrayEquals(body, response.getBody()); + } + + @Test + public void string() throws Exception { + RestTemplate restTemplate = new RestTemplate(); + + String body = randomString(); + RequestEntity request = new RequestEntity(body, HttpMethod.POST, new URI(url())); + ResponseEntity response = restTemplate.exchange(request, String.class); + + assertEquals(body, response.getBody()); + } + + private static String url() { + return "http://localhost:" + port + "/rx"; + } + + private String randomString() { + StringBuilder builder = new StringBuilder(); + int i = 1; + while (builder.length() < REQUEST_SIZE) { + builder.append(randomChar()); + if (i % 5 == 0) { + builder.append(' '); + } + if (i % 80 == 0) { + builder.append('\n'); + } + i++; + } + return builder.toString(); + } + + private char randomChar() { + return (char) (rnd.nextInt(26) + 'a'); + } + + private byte[] randomBytes() { + byte[] buffer = new byte[REQUEST_SIZE]; + rnd.nextBytes(buffer); + return buffer; + } + + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/rx/web/servlet/CountingHttpHandler.java b/spring-web-reactive/src/test/java/org/springframework/rx/web/servlet/CountingHttpHandler.java new file mode 100644 index 0000000000..962fd7be68 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/rx/web/servlet/CountingHttpHandler.java @@ -0,0 +1,64 @@ +/* + * Copyright 2002-2015 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.rx.web.servlet; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +/** + * @author Arjen Poutsma + */ +public class CountingHttpHandler implements HttpHandler { + + private static final Log logger = LogFactory.getLog(CountingHttpHandler.class); + + @Override + public Publisher handle(Publisher request) { + request.subscribe(new Subscriber() { + private Subscription subscription; + + private int byteCount = 0; + + @Override + public void onSubscribe(Subscription s) { + this.subscription = s; + this.subscription.request(1); + } + + @Override + public void onNext(byte[] bytes) { + byteCount += bytes.length; + this.subscription.request(1); + } + + @Override + public void onError(Throwable t) { + logger.error("CountingHttpHandler Error", t); + t.printStackTrace(); + } + + @Override + public void onComplete() { + logger.info("Processed " + byteCount + " bytes"); + } + }); + return null; + } +} diff --git a/spring-web-reactive/src/test/java/org/springframework/rx/web/servlet/EchoHandler.java b/spring-web-reactive/src/test/java/org/springframework/rx/web/servlet/EchoHandler.java new file mode 100644 index 0000000000..faf728b0f1 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/rx/web/servlet/EchoHandler.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2015 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.rx.web.servlet; + +import org.reactivestreams.Publisher; + +/** + * @author Arjen Poutsma + */ +public class EchoHandler implements HttpHandler { + + @Override + public Publisher handle(Publisher request) { + return request; + } +} diff --git a/spring-web-reactive/src/test/java/org/springframework/rx/web/servlet/HttpHandlerServletJettyIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/rx/web/servlet/HttpHandlerServletJettyIntegrationTests.java new file mode 100644 index 0000000000..be3ef6f52f --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/rx/web/servlet/HttpHandlerServletJettyIntegrationTests.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2015 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.rx.web.servlet; + +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.junit.AfterClass; +import org.junit.BeforeClass; + +import org.springframework.util.SocketUtils; + +/** + * @author Arjen Poutsma + */ +public class HttpHandlerServletJettyIntegrationTests + extends AbstractHttpHandlerServletIntegrationTestCase { + + private static Server jettyServer; + + @BeforeClass + public static void startServer() throws Exception { + jettyServer = new Server(); + ServerConnector connector = new ServerConnector(jettyServer); + port = SocketUtils.findAvailableTcpPort(); + connector.setPort(port); + ServletContextHandler handler = new ServletContextHandler(jettyServer, "", false, false); + HttpHandlerServlet servlet = new HttpHandlerServlet(); + servlet.setHandler(new EchoHandler()); + ServletHolder servletHolder = new ServletHolder(servlet); + handler.addServlet(servletHolder, "/rx"); + jettyServer.addConnector(connector); + jettyServer.start(); + } + + @AfterClass + public static void stopServer() throws Exception { + jettyServer.stop(); + } + +} \ No newline at end of file diff --git a/spring-web-reactive/src/test/java/org/springframework/rx/web/servlet/HttpHandlerServletTomcatIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/rx/web/servlet/HttpHandlerServletTomcatIntegrationTests.java new file mode 100644 index 0000000000..a9d07171b6 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/rx/web/servlet/HttpHandlerServletTomcatIntegrationTests.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2015 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.rx.web.servlet; + +import java.io.File; + +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.startup.Tomcat; +import org.junit.AfterClass; +import org.junit.BeforeClass; + +/** + * @author Arjen Poutsma + */ +public class HttpHandlerServletTomcatIntegrationTests extends AbstractHttpHandlerServletIntegrationTestCase { + + private static Tomcat tomcatServer; + + @BeforeClass + public static void startServer() throws LifecycleException, InterruptedException { + tomcatServer = new Tomcat(); + tomcatServer.setPort(port); + File base = new File(System.getProperty("java.io.tmpdir")); + Context rootCtx = tomcatServer.addContext("", base.getAbsolutePath()); + + HttpHandlerServlet servlet = new HttpHandlerServlet(); + servlet.setHandler(new EchoHandler()); + + tomcatServer.addServlet(rootCtx, "handlerServlet", servlet); + rootCtx.addServletMapping("/rx", "handlerServlet"); + + tomcatServer.start(); + } + + @AfterClass + public static void stopServer() throws LifecycleException { + tomcatServer.stop(); + } + +} \ No newline at end of file From 74a29ac146280a399d88680d187c8dbeb2b85328 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Thu, 9 Jul 2015 13:40:29 +0200 Subject: [PATCH 005/498] Renamed 'rx' package to 'reactive' --- spring-web-reactive/.gitignore | 2 ++ .../io/ByteArrayPublisherInputStream.java | 4 +-- .../io/ByteArrayPublisherOutputStream.java | 4 +-- .../util/BlockingSignalQueue.java | 2 +- .../{rx => reactive}/util/OnComplete.java | 2 +- .../{rx => reactive}/util/OnError.java | 2 +- .../{rx => reactive}/util/OnNext.java | 2 +- .../{rx => reactive}/util/Signal.java | 2 +- .../servlet => reactive/web}/HttpHandler.java | 2 +- .../web/servlet/AsyncContextSynchronizer.java | 2 +- .../web/servlet/HttpHandlerServlet.java | 4 ++- .../web/servlet/RequestBodyPublisher.java | 2 +- .../web/servlet/ResponseBodySubscriber.java | 32 +++++++++---------- .../io/ByteBufPublisherInputStreamTests.java | 7 ++-- .../BlockingByteBufQueuePublisherTests.java | 2 +- .../util/BlockingByteBufQueueTests.java | 2 +- .../web}/CountingHttpHandler.java | 3 +- .../servlet => reactive/web}/EchoHandler.java | 2 +- ...HttpHandlerServletIntegrationTestCase.java | 4 +-- ...tpHandlerServletJettyIntegrationTests.java | 11 +++++-- ...pHandlerServletTomcatIntegrationTests.java | 10 +++++- 21 files changed, 60 insertions(+), 43 deletions(-) rename spring-web-reactive/src/main/java/org/springframework/{rx => reactive}/io/ByteArrayPublisherInputStream.java (96%) rename spring-web-reactive/src/main/java/org/springframework/{rx => reactive}/io/ByteArrayPublisherOutputStream.java (91%) rename spring-web-reactive/src/main/java/org/springframework/{rx => reactive}/util/BlockingSignalQueue.java (99%) rename spring-web-reactive/src/main/java/org/springframework/{rx => reactive}/util/OnComplete.java (96%) rename spring-web-reactive/src/main/java/org/springframework/{rx => reactive}/util/OnError.java (96%) rename spring-web-reactive/src/main/java/org/springframework/{rx => reactive}/util/OnNext.java (96%) rename spring-web-reactive/src/main/java/org/springframework/{rx => reactive}/util/Signal.java (94%) rename spring-web-reactive/src/main/java/org/springframework/{rx/web/servlet => reactive/web}/HttpHandler.java (94%) rename spring-web-reactive/src/main/java/org/springframework/{rx => reactive}/web/servlet/AsyncContextSynchronizer.java (97%) rename spring-web-reactive/src/main/java/org/springframework/{rx => reactive}/web/servlet/HttpHandlerServlet.java (94%) rename spring-web-reactive/src/main/java/org/springframework/{rx => reactive}/web/servlet/RequestBodyPublisher.java (98%) rename spring-web-reactive/src/main/java/org/springframework/{rx => reactive}/web/servlet/ResponseBodySubscriber.java (82%) rename spring-web-reactive/src/test/java/org/springframework/{rx => reactive}/io/ByteBufPublisherInputStreamTests.java (91%) rename spring-web-reactive/src/test/java/org/springframework/{rx => reactive}/util/BlockingByteBufQueuePublisherTests.java (99%) rename spring-web-reactive/src/test/java/org/springframework/{rx => reactive}/util/BlockingByteBufQueueTests.java (97%) rename spring-web-reactive/src/test/java/org/springframework/{rx/web/servlet => reactive/web}/CountingHttpHandler.java (96%) rename spring-web-reactive/src/test/java/org/springframework/{rx/web/servlet => reactive/web}/EchoHandler.java (95%) rename spring-web-reactive/src/test/java/org/springframework/{rx => reactive}/web/servlet/AbstractHttpHandlerServletIntegrationTestCase.java (96%) rename spring-web-reactive/src/test/java/org/springframework/{rx => reactive}/web/servlet/HttpHandlerServletJettyIntegrationTests.java (87%) rename spring-web-reactive/src/test/java/org/springframework/{rx => reactive}/web/servlet/HttpHandlerServletTomcatIntegrationTests.java (85%) diff --git a/spring-web-reactive/.gitignore b/spring-web-reactive/.gitignore index 8591e43e73..207211d54c 100644 --- a/spring-web-reactive/.gitignore +++ b/spring-web-reactive/.gitignore @@ -6,3 +6,5 @@ target /.idea/ bin .gradle +tomcat* +build diff --git a/spring-web-reactive/src/main/java/org/springframework/rx/io/ByteArrayPublisherInputStream.java b/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteArrayPublisherInputStream.java similarity index 96% rename from spring-web-reactive/src/main/java/org/springframework/rx/io/ByteArrayPublisherInputStream.java rename to spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteArrayPublisherInputStream.java index 3f5b882f44..03b81aafa3 100644 --- a/spring-web-reactive/src/main/java/org/springframework/rx/io/ByteArrayPublisherInputStream.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteArrayPublisherInputStream.java @@ -1,4 +1,4 @@ -package org.springframework.rx.io;/* +package org.springframework.reactive.io;/* * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,7 +20,7 @@ import java.io.InputStream; import org.reactivestreams.Publisher; -import org.springframework.rx.util.BlockingSignalQueue; +import org.springframework.reactive.util.BlockingSignalQueue; import org.springframework.util.Assert; /** diff --git a/spring-web-reactive/src/main/java/org/springframework/rx/io/ByteArrayPublisherOutputStream.java b/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteArrayPublisherOutputStream.java similarity index 91% rename from spring-web-reactive/src/main/java/org/springframework/rx/io/ByteArrayPublisherOutputStream.java rename to spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteArrayPublisherOutputStream.java index 80550a43c1..beda227f88 100644 --- a/spring-web-reactive/src/main/java/org/springframework/rx/io/ByteArrayPublisherOutputStream.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteArrayPublisherOutputStream.java @@ -1,4 +1,4 @@ -package org.springframework.rx.io; +package org.springframework.reactive.io; import java.io.IOException; import java.io.OutputStream; @@ -6,7 +6,7 @@ import java.util.Arrays; import org.reactivestreams.Publisher; -import org.springframework.rx.util.BlockingSignalQueue; +import org.springframework.reactive.util.BlockingSignalQueue; /** * {@code OutputStream} implementation that stores all written bytes, to be retrieved diff --git a/spring-web-reactive/src/main/java/org/springframework/rx/util/BlockingSignalQueue.java b/spring-web-reactive/src/main/java/org/springframework/reactive/util/BlockingSignalQueue.java similarity index 99% rename from spring-web-reactive/src/main/java/org/springframework/rx/util/BlockingSignalQueue.java rename to spring-web-reactive/src/main/java/org/springframework/reactive/util/BlockingSignalQueue.java index 890dc89eb6..1b854ff674 100644 --- a/spring-web-reactive/src/main/java/org/springframework/rx/util/BlockingSignalQueue.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/util/BlockingSignalQueue.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.rx.util; +package org.springframework.reactive.util; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; diff --git a/spring-web-reactive/src/main/java/org/springframework/rx/util/OnComplete.java b/spring-web-reactive/src/main/java/org/springframework/reactive/util/OnComplete.java similarity index 96% rename from spring-web-reactive/src/main/java/org/springframework/rx/util/OnComplete.java rename to spring-web-reactive/src/main/java/org/springframework/reactive/util/OnComplete.java index 74e1cccc10..47aeb326a8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/rx/util/OnComplete.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/util/OnComplete.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.rx.util; +package org.springframework.reactive.util; /** * @author Arjen Poutsma diff --git a/spring-web-reactive/src/main/java/org/springframework/rx/util/OnError.java b/spring-web-reactive/src/main/java/org/springframework/reactive/util/OnError.java similarity index 96% rename from spring-web-reactive/src/main/java/org/springframework/rx/util/OnError.java rename to spring-web-reactive/src/main/java/org/springframework/reactive/util/OnError.java index 250e6f7727..0286ac06a1 100644 --- a/spring-web-reactive/src/main/java/org/springframework/rx/util/OnError.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/util/OnError.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.rx.util; +package org.springframework.reactive.util; import org.springframework.util.Assert; diff --git a/spring-web-reactive/src/main/java/org/springframework/rx/util/OnNext.java b/spring-web-reactive/src/main/java/org/springframework/reactive/util/OnNext.java similarity index 96% rename from spring-web-reactive/src/main/java/org/springframework/rx/util/OnNext.java rename to spring-web-reactive/src/main/java/org/springframework/reactive/util/OnNext.java index 6d1aa6d8ca..f8e48e4b65 100644 --- a/spring-web-reactive/src/main/java/org/springframework/rx/util/OnNext.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/util/OnNext.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.rx.util; +package org.springframework.reactive.util; import org.springframework.util.Assert; diff --git a/spring-web-reactive/src/main/java/org/springframework/rx/util/Signal.java b/spring-web-reactive/src/main/java/org/springframework/reactive/util/Signal.java similarity index 94% rename from spring-web-reactive/src/main/java/org/springframework/rx/util/Signal.java rename to spring-web-reactive/src/main/java/org/springframework/reactive/util/Signal.java index 74ea5eacac..a0cb153e6e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/rx/util/Signal.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/util/Signal.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.rx.util; +package org.springframework.reactive.util; /** * @author Arjen Poutsma diff --git a/spring-web-reactive/src/main/java/org/springframework/rx/web/servlet/HttpHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/HttpHandler.java similarity index 94% rename from spring-web-reactive/src/main/java/org/springframework/rx/web/servlet/HttpHandler.java rename to spring-web-reactive/src/main/java/org/springframework/reactive/web/HttpHandler.java index e9a3686959..e1e0aac26a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/rx/web/servlet/HttpHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/HttpHandler.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.rx.web.servlet; +package org.springframework.reactive.web; import org.reactivestreams.Publisher; diff --git a/spring-web-reactive/src/main/java/org/springframework/rx/web/servlet/AsyncContextSynchronizer.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/AsyncContextSynchronizer.java similarity index 97% rename from spring-web-reactive/src/main/java/org/springframework/rx/web/servlet/AsyncContextSynchronizer.java rename to spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/AsyncContextSynchronizer.java index 20be9c51b8..80882923fa 100644 --- a/spring-web-reactive/src/main/java/org/springframework/rx/web/servlet/AsyncContextSynchronizer.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/AsyncContextSynchronizer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.rx.web.servlet; +package org.springframework.reactive.web.servlet; import java.io.IOException; import java.util.concurrent.atomic.AtomicInteger; diff --git a/spring-web-reactive/src/main/java/org/springframework/rx/web/servlet/HttpHandlerServlet.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/HttpHandlerServlet.java similarity index 94% rename from spring-web-reactive/src/main/java/org/springframework/rx/web/servlet/HttpHandlerServlet.java rename to spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/HttpHandlerServlet.java index f19d380314..af5f10f172 100644 --- a/spring-web-reactive/src/main/java/org/springframework/rx/web/servlet/HttpHandlerServlet.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/HttpHandlerServlet.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.rx.web.servlet; +package org.springframework.reactive.web.servlet; import java.io.IOException; import javax.servlet.AsyncContext; @@ -26,6 +26,8 @@ import javax.servlet.http.HttpServletResponse; import org.reactivestreams.Publisher; +import org.springframework.reactive.web.HttpHandler; + /** * @author Arjen Poutsma */ diff --git a/spring-web-reactive/src/main/java/org/springframework/rx/web/servlet/RequestBodyPublisher.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/RequestBodyPublisher.java similarity index 98% rename from spring-web-reactive/src/main/java/org/springframework/rx/web/servlet/RequestBodyPublisher.java rename to spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/RequestBodyPublisher.java index 6022922c83..24f28b7bff 100644 --- a/spring-web-reactive/src/main/java/org/springframework/rx/web/servlet/RequestBodyPublisher.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/RequestBodyPublisher.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.rx.web.servlet; +package org.springframework.reactive.web.servlet; import java.io.IOException; import java.nio.charset.Charset; diff --git a/spring-web-reactive/src/main/java/org/springframework/rx/web/servlet/ResponseBodySubscriber.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/ResponseBodySubscriber.java similarity index 82% rename from spring-web-reactive/src/main/java/org/springframework/rx/web/servlet/ResponseBodySubscriber.java rename to spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/ResponseBodySubscriber.java index f7faa4b16a..c4a6eefd37 100644 --- a/spring-web-reactive/src/main/java/org/springframework/rx/web/servlet/ResponseBodySubscriber.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/ResponseBodySubscriber.java @@ -14,10 +14,9 @@ * limitations under the License. */ -package org.springframework.rx.web.servlet; +package org.springframework.reactive.web.servlet; import java.io.IOException; -import java.util.concurrent.atomic.AtomicBoolean; import javax.servlet.ServletOutputStream; import javax.servlet.WriteListener; @@ -41,7 +40,7 @@ public class ResponseBodySubscriber implements WriteListener, Subscriber private byte[] buffer; - private AtomicBoolean complete = new AtomicBoolean(false); + private volatile boolean subscriberComplete = false; public ResponseBodySubscriber(AsyncContextSynchronizer synchronizer) { this.synchronizer = synchronizer; @@ -72,7 +71,9 @@ public class ResponseBodySubscriber implements WriteListener, Subscriber public void onComplete() { logger.debug("Complete buffer: " + (buffer == null)); - if (complete.compareAndSet(false, true) && buffer == null) { + this.subscriberComplete = true; + + if (buffer == null) { this.synchronizer.writeComplete(); } } @@ -84,20 +85,22 @@ public class ResponseBodySubscriber implements WriteListener, Subscriber boolean ready = output.isReady(); logger.debug("Output: " + ready + " buffer: " + (buffer == null)); - if (this.buffer != null && ready) { - output.write(this.buffer); - this.buffer = null; + if (ready) { + if (this.buffer != null) { + output.write(this.buffer); + this.buffer = null; - if (!complete.get()) { - this.subscription.request(1); + if (!subscriberComplete) { + this.subscription.request(1); + } + else { + this.synchronizer.writeComplete(); + } } else { - this.synchronizer.writeComplete(); + this.subscription.request(1); } } - else if (this.buffer == null && ready) { - this.subscription.request(1); - } } @Override @@ -105,7 +108,4 @@ public class ResponseBodySubscriber implements WriteListener, Subscriber logger.error("ResponseBodySubscriber error", t); } - - private void complete() { - } } diff --git a/spring-web-reactive/src/test/java/org/springframework/rx/io/ByteBufPublisherInputStreamTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/io/ByteBufPublisherInputStreamTests.java similarity index 91% rename from spring-web-reactive/src/test/java/org/springframework/rx/io/ByteBufPublisherInputStreamTests.java rename to spring-web-reactive/src/test/java/org/springframework/reactive/io/ByteBufPublisherInputStreamTests.java index cd8fa33ddf..45ae841646 100644 --- a/spring-web-reactive/src/test/java/org/springframework/rx/io/ByteBufPublisherInputStreamTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/io/ByteBufPublisherInputStreamTests.java @@ -14,14 +14,15 @@ * limitations under the License. */ -package org.springframework.rx.io; +package org.springframework.reactive.io; import org.junit.Before; import org.junit.Test; -import org.springframework.rx.util.BlockingSignalQueue; +import org.springframework.reactive.util.BlockingSignalQueue; -import static org.junit.Assert.*; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; /** * @author Arjen Poutsma diff --git a/spring-web-reactive/src/test/java/org/springframework/rx/util/BlockingByteBufQueuePublisherTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/util/BlockingByteBufQueuePublisherTests.java similarity index 99% rename from spring-web-reactive/src/test/java/org/springframework/rx/util/BlockingByteBufQueuePublisherTests.java rename to spring-web-reactive/src/test/java/org/springframework/reactive/util/BlockingByteBufQueuePublisherTests.java index b09781b107..6db97850ee 100644 --- a/spring-web-reactive/src/test/java/org/springframework/rx/util/BlockingByteBufQueuePublisherTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/util/BlockingByteBufQueuePublisherTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.rx.util; +package org.springframework.reactive.util; import java.util.ArrayList; import java.util.List; diff --git a/spring-web-reactive/src/test/java/org/springframework/rx/util/BlockingByteBufQueueTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/util/BlockingByteBufQueueTests.java similarity index 97% rename from spring-web-reactive/src/test/java/org/springframework/rx/util/BlockingByteBufQueueTests.java rename to spring-web-reactive/src/test/java/org/springframework/reactive/util/BlockingByteBufQueueTests.java index 3148d7df56..a04b618487 100644 --- a/spring-web-reactive/src/test/java/org/springframework/rx/util/BlockingByteBufQueueTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/util/BlockingByteBufQueueTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.rx.util; +package org.springframework.reactive.util; import org.junit.Before; import org.junit.Test; diff --git a/spring-web-reactive/src/test/java/org/springframework/rx/web/servlet/CountingHttpHandler.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/CountingHttpHandler.java similarity index 96% rename from spring-web-reactive/src/test/java/org/springframework/rx/web/servlet/CountingHttpHandler.java rename to spring-web-reactive/src/test/java/org/springframework/reactive/web/CountingHttpHandler.java index 962fd7be68..de6bd16d4f 100644 --- a/spring-web-reactive/src/test/java/org/springframework/rx/web/servlet/CountingHttpHandler.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/CountingHttpHandler.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.rx.web.servlet; +package org.springframework.reactive.web; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -51,7 +51,6 @@ public class CountingHttpHandler implements HttpHandler { @Override public void onError(Throwable t) { logger.error("CountingHttpHandler Error", t); - t.printStackTrace(); } @Override diff --git a/spring-web-reactive/src/test/java/org/springframework/rx/web/servlet/EchoHandler.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/EchoHandler.java similarity index 95% rename from spring-web-reactive/src/test/java/org/springframework/rx/web/servlet/EchoHandler.java rename to spring-web-reactive/src/test/java/org/springframework/reactive/web/EchoHandler.java index faf728b0f1..e554623144 100644 --- a/spring-web-reactive/src/test/java/org/springframework/rx/web/servlet/EchoHandler.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/EchoHandler.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.rx.web.servlet; +package org.springframework.reactive.web; import org.reactivestreams.Publisher; diff --git a/spring-web-reactive/src/test/java/org/springframework/rx/web/servlet/AbstractHttpHandlerServletIntegrationTestCase.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/servlet/AbstractHttpHandlerServletIntegrationTestCase.java similarity index 96% rename from spring-web-reactive/src/test/java/org/springframework/rx/web/servlet/AbstractHttpHandlerServletIntegrationTestCase.java rename to spring-web-reactive/src/test/java/org/springframework/reactive/web/servlet/AbstractHttpHandlerServletIntegrationTestCase.java index 50e009b297..6088296dd1 100644 --- a/spring-web-reactive/src/test/java/org/springframework/rx/web/servlet/AbstractHttpHandlerServletIntegrationTestCase.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/servlet/AbstractHttpHandlerServletIntegrationTestCase.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.rx.web.servlet; +package org.springframework.reactive.web.servlet; import java.net.URI; import java.util.Random; @@ -62,7 +62,7 @@ public abstract class AbstractHttpHandlerServletIntegrationTestCase { assertEquals(body, response.getBody()); } - private static String url() { + protected static String url() { return "http://localhost:" + port + "/rx"; } diff --git a/spring-web-reactive/src/test/java/org/springframework/rx/web/servlet/HttpHandlerServletJettyIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/servlet/HttpHandlerServletJettyIntegrationTests.java similarity index 87% rename from spring-web-reactive/src/test/java/org/springframework/rx/web/servlet/HttpHandlerServletJettyIntegrationTests.java rename to spring-web-reactive/src/test/java/org/springframework/reactive/web/servlet/HttpHandlerServletJettyIntegrationTests.java index be3ef6f52f..e52696476e 100644 --- a/spring-web-reactive/src/test/java/org/springframework/rx/web/servlet/HttpHandlerServletJettyIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/servlet/HttpHandlerServletJettyIntegrationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.rx.web.servlet; +package org.springframework.reactive.web.servlet; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; @@ -23,7 +23,7 @@ import org.eclipse.jetty.servlet.ServletHolder; import org.junit.AfterClass; import org.junit.BeforeClass; -import org.springframework.util.SocketUtils; +import org.springframework.reactive.web.EchoHandler; /** * @author Arjen Poutsma @@ -37,7 +37,6 @@ public class HttpHandlerServletJettyIntegrationTests public static void startServer() throws Exception { jettyServer = new Server(); ServerConnector connector = new ServerConnector(jettyServer); - port = SocketUtils.findAvailableTcpPort(); connector.setPort(port); ServletContextHandler handler = new ServletContextHandler(jettyServer, "", false, false); HttpHandlerServlet servlet = new HttpHandlerServlet(); @@ -53,4 +52,10 @@ public class HttpHandlerServletJettyIntegrationTests jettyServer.stop(); } + public static void main(String[] args) throws Exception { + startServer(); + System.out.println("Jetty running at: " + url()); + + } + } \ No newline at end of file diff --git a/spring-web-reactive/src/test/java/org/springframework/rx/web/servlet/HttpHandlerServletTomcatIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/servlet/HttpHandlerServletTomcatIntegrationTests.java similarity index 85% rename from spring-web-reactive/src/test/java/org/springframework/rx/web/servlet/HttpHandlerServletTomcatIntegrationTests.java rename to spring-web-reactive/src/test/java/org/springframework/reactive/web/servlet/HttpHandlerServletTomcatIntegrationTests.java index a9d07171b6..36b1ca8e53 100644 --- a/spring-web-reactive/src/test/java/org/springframework/rx/web/servlet/HttpHandlerServletTomcatIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/servlet/HttpHandlerServletTomcatIntegrationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.rx.web.servlet; +package org.springframework.reactive.web.servlet; import java.io.File; @@ -24,6 +24,8 @@ import org.apache.catalina.startup.Tomcat; import org.junit.AfterClass; import org.junit.BeforeClass; +import org.springframework.reactive.web.EchoHandler; + /** * @author Arjen Poutsma */ @@ -52,4 +54,10 @@ public class HttpHandlerServletTomcatIntegrationTests extends AbstractHttpHandle tomcatServer.stop(); } + public static void main(String[] args) throws Exception { + startServer(); + System.out.println("Tomcat running at: " + url()); + tomcatServer.getServer().await(); + } + } \ No newline at end of file From 3595c51d40a2e735964f22fd29be607b12f600b4 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Wed, 29 Jul 2015 14:21:10 +0200 Subject: [PATCH 006/498] Introduced DemandCounter --- spring-web-reactive/build.gradle | 7 ++ .../reactive/util/BlockingSignalQueue.java | 15 ++-- .../reactive/util/DemandCounter.java | 70 +++++++++++++++++++ .../web/servlet/AsyncContextSynchronizer.java | 46 ++++++++---- .../web/servlet/RequestBodyPublisher.java | 21 +++--- .../AsyncContextSynchronizerTests.java | 57 +++++++++++++++ 6 files changed, 180 insertions(+), 36 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/util/DemandCounter.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/reactive/web/servlet/AsyncContextSynchronizerTests.java diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index 0807ee5280..91ce186840 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -35,6 +35,13 @@ dependencies { testCompile 'org.eclipse.jetty:jetty-servlet:9.3.0.v20150612' testCompile("log4j:log4j:1.2.16") + testCompile("org.mockito:mockito-core:1.10.19") { + exclude group:'org.hamcrest', module:'hamcrest-core' + } + testCompile("org.hamcrest:hamcrest-all:1.3") + testCompile "org.springframework:spring-test:4.1.2.RELEASE" + + } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/util/BlockingSignalQueue.java b/spring-web-reactive/src/main/java/org/springframework/reactive/util/BlockingSignalQueue.java index 1b854ff674..67e81601d2 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/util/BlockingSignalQueue.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/util/BlockingSignalQueue.java @@ -181,17 +181,15 @@ public class BlockingSignalQueue { private class SubscriptionThread extends Thread { - private volatile long demand = 0; + private final DemandCounter demand = new DemandCounter(); @Override public void run() { try { while (!Thread.currentThread().isInterrupted()) { - if (demand > 0 && isHeadSignal()) { + if (this.demand.hasDemand() && isHeadSignal()) { subscriber.onNext(pollSignal()); - if (demand != Long.MAX_VALUE) { - demand--; - } + this.demand.decrement(); } else if (isHeadError()) { subscriber.onError(pollError()); @@ -209,12 +207,7 @@ public class BlockingSignalQueue { } public void request(long n) { - if (n != Long.MAX_VALUE) { - this.demand += n; - } - else { - this.demand = Long.MAX_VALUE; - } + this.demand.increase(n); } public void cancel() { diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/util/DemandCounter.java b/spring-web-reactive/src/main/java/org/springframework/reactive/util/DemandCounter.java new file mode 100644 index 0000000000..cf8cf42c45 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/util/DemandCounter.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2015 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.reactive.util; + +import java.util.concurrent.atomic.AtomicLong; + +import org.springframework.util.Assert; + +/** + * Small utility class for keeping track of Reactive Streams demand. + * @author Arjen Poutsma + */ +public final class DemandCounter { + + private final AtomicLong demand = new AtomicLong(); + + /** + * Increases the demand by the given number + * @param n the positive number to increase demand by + * @return the increased demand + * @see org.reactivestreams.Subscription#request(long) + */ + public long increase(long n) { + Assert.isTrue(n > 0, "'n' must be higher than 0"); + return demand.updateAndGet(d -> d != Long.MAX_VALUE ? d + n : Long.MAX_VALUE); + } + + /** + * Decreases the demand by one. + * @return the decremented demand + */ + public long decrement() { + return demand.updateAndGet(d -> d != Long.MAX_VALUE ? d - 1 : Long.MAX_VALUE); + } + + /** + * Indicates whether this counter has demand, i.e. whether it is higher than 0. + * @return {@code true} if this counter has demand; {@code false} otherwise + */ + public boolean hasDemand() { + return this.demand.get() > 0; + } + + /** + * Resets this counter to 0. + * @see org.reactivestreams.Subscription#cancel() + */ + public void reset() { + this.demand.set(0); + } + + @Override + public String toString() { + return demand.toString(); + } +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/AsyncContextSynchronizer.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/AsyncContextSynchronizer.java index 80882923fa..f158923136 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/AsyncContextSynchronizer.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/AsyncContextSynchronizer.java @@ -22,15 +22,17 @@ import javax.servlet.AsyncContext; import javax.servlet.ServletInputStream; import javax.servlet.ServletOutputStream; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - /** + * Utility class for synchronizing between the reading and writing side of an + * {@link AsyncContext}. This class will simply call {@link AsyncContext#complete()} when + * both {@link #readComplete()} and {@link #writeComplete()} have been called. + * * @author Arjen Poutsma + * @see AsyncContext */ -class AsyncContextSynchronizer { +final class AsyncContextSynchronizer { - private static final Log logger = LogFactory.getLog(AsyncContextSynchronizer.class); + private static final int NONE_COMPLETE = 0; private static final int READ_COMPLETE = 1; @@ -40,39 +42,59 @@ class AsyncContextSynchronizer { private final AsyncContext asyncContext; - private final AtomicInteger complete = new AtomicInteger(0); + private final AtomicInteger complete = new AtomicInteger(NONE_COMPLETE); + /** + * Creates a new {@code AsyncContextSynchronizer} based on the given context. + * @param asyncContext the context to base this synchronizer on + */ public AsyncContextSynchronizer(AsyncContext asyncContext) { this.asyncContext = asyncContext; } + /** + * Returns the input stream of this synchronizer. + * @return the input stream + * @throws IOException if an input or output exception occurred + */ public ServletInputStream getInputStream() throws IOException { return this.asyncContext.getRequest().getInputStream(); } + /** + * Returns the output stream of this synchronizer. + * @return the output stream + * @throws IOException if an input or output exception occurred + */ public ServletOutputStream getOutputStream() throws IOException { return this.asyncContext.getResponse().getOutputStream(); } + /** + * Completes the reading side of the asynchronous operation. When both this method and + * {@link #writeComplete()} have been called, the {@code AsyncContext} will be + * {@linkplain AsyncContext#complete() fully completed}. + */ public void readComplete() { - logger.debug("Read complete"); if (complete.compareAndSet(WRITE_COMPLETE, COMPLETE)) { - logger.debug("Complete"); this.asyncContext.complete(); } else { - this.complete.compareAndSet(0, READ_COMPLETE); + this.complete.compareAndSet(NONE_COMPLETE, READ_COMPLETE); } } + /** + * Completes the writing side of the asynchronous operation. When both this method and + * {@link #readComplete()} have been called, the {@code AsyncContext} will be + * {@linkplain AsyncContext#complete() fully completed}. + */ public void writeComplete() { - logger.debug("Write complete"); if (complete.compareAndSet(READ_COMPLETE, COMPLETE)) { - logger.debug("Complete"); this.asyncContext.complete(); } else { - this.complete.compareAndSet(0, WRITE_COMPLETE); + this.complete.compareAndSet(NONE_COMPLETE, WRITE_COMPLETE); } } } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/RequestBodyPublisher.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/RequestBodyPublisher.java index 24f28b7bff..05bf19828c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/RequestBodyPublisher.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/RequestBodyPublisher.java @@ -28,6 +28,8 @@ import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; +import org.springframework.reactive.util.DemandCounter; + /** * @author Arjen Poutsma */ @@ -41,7 +43,7 @@ public class RequestBodyPublisher implements ReadListener, Publisher { private final byte[] buffer; - private long demand; + private final DemandCounter demand = new DemandCounter(); private Subscriber subscriber; @@ -64,7 +66,7 @@ public class RequestBodyPublisher implements ReadListener, Publisher { while (true) { logger.debug("Demand: " + this.demand); - if (demand <= 0) { + if (!demand.hasDemand()) { break; } @@ -82,9 +84,7 @@ public class RequestBodyPublisher implements ReadListener, Publisher { break; } else if (read > 0) { - if (demand != Long.MAX_VALUE) { - demand--; - } + this.demand.decrement(); byte[] copy = Arrays.copyOf(this.buffer, read); // logger.debug("Next: " + new String(copy, UTF_8)); @@ -114,14 +114,9 @@ public class RequestBodyPublisher implements ReadListener, Publisher { public void request(long n) { logger.debug("Updating demand " + demand + " by " + n); - boolean stalled = demand <= 0; + boolean stalled = !demand.hasDemand(); - if (n != Long.MAX_VALUE && demand != Long.MAX_VALUE) { - demand += n; - } - else { - demand = Long.MAX_VALUE; - } + demand.increase(n); if (stalled) { try { @@ -136,7 +131,7 @@ public class RequestBodyPublisher implements ReadListener, Publisher { @Override public void cancel() { synchronizer.readComplete(); - demand = 0; + demand.reset(); } } } diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/servlet/AsyncContextSynchronizerTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/servlet/AsyncContextSynchronizerTests.java new file mode 100644 index 0000000000..0003f99608 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/servlet/AsyncContextSynchronizerTests.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2015 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.reactive.web.servlet; + +import javax.servlet.AsyncContext; + +import org.junit.Before; +import org.junit.Test; + +import static org.mockito.BDDMockito.mock; +import static org.mockito.BDDMockito.verify; + +/** + * @author Arjen Poutsma + */ +public class AsyncContextSynchronizerTests { + + private AsyncContext asyncContext; + + private AsyncContextSynchronizer synchronizer; + + @Before + public void setUp() throws Exception { + asyncContext = mock(AsyncContext.class); + synchronizer = new AsyncContextSynchronizer(asyncContext); + } + + @Test + public void readThenWrite() { + synchronizer.readComplete(); + synchronizer.writeComplete(); + + verify(asyncContext).complete(); + } + + @Test + public void writeThenRead() { + synchronizer.writeComplete(); + synchronizer.readComplete(); + + verify(asyncContext).complete(); + } +} \ No newline at end of file From c464cddf83d2b0796eaf4dae9a75c8bf0a7d323a Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 7 Aug 2015 17:30:52 -0400 Subject: [PATCH 007/498] Update versions --- spring-web-reactive/build.gradle | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index 91ce186840..4aa6cab66c 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -18,31 +18,28 @@ repositories { } dependencies { - compile "org.springframework:spring-core:4.1.2.RELEASE" - compile "org.reactivestreams:reactive-streams:1.0.0.RC3" - compile "io.netty:netty-buffer:4.0.25.Final" + compile "org.springframework:spring-core:4.2.0.RELEASE" + compile "org.reactivestreams:reactive-streams:1.0.0" compile "org.slf4j:slf4j-api:1.7.6" compile "ch.qos.logback:logback-classic:1.1.2" provided "javax.servlet:javax.servlet-api:3.1.0" testCompile "junit:junit:4.12" - testCompile "org.springframework:spring-web:4.1.2.RELEASE" - testCompile 'org.apache.tomcat:tomcat-util:8.0.23' - testCompile 'org.apache.tomcat.embed:tomcat-embed-core:8.0.23' + testCompile "org.springframework:spring-web:4.2.0.RELEASE" + testCompile "org.springframework:spring-test:4.2.0.RELEASE" - testCompile 'org.eclipse.jetty:jetty-server:9.3.0.v20150612' - testCompile 'org.eclipse.jetty:jetty-servlet:9.3.0.v20150612' + testCompile 'org.apache.tomcat:tomcat-util:8.0.24' + testCompile 'org.apache.tomcat.embed:tomcat-embed-core:8.0.24' + + testCompile 'org.eclipse.jetty:jetty-server:9.3.2.v20150730' + testCompile 'org.eclipse.jetty:jetty-servlet:9.3.2.v20150730' testCompile("log4j:log4j:1.2.16") testCompile("org.mockito:mockito-core:1.10.19") { exclude group:'org.hamcrest', module:'hamcrest-core' } testCompile("org.hamcrest:hamcrest-all:1.3") - testCompile "org.springframework:spring-test:4.1.2.RELEASE" - - - } From 17b3db298800f6b80eba51cc19c58151899461a5 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Sat, 8 Aug 2015 03:55:23 -0400 Subject: [PATCH 008/498] Add gradle wrapper --- .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 52271 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + spring-web-reactive/gradlew | 164 ++++++++++++++++++ spring-web-reactive/gradlew.bat | 90 ++++++++++ 4 files changed, 260 insertions(+) create mode 100644 spring-web-reactive/gradle/wrapper/gradle-wrapper.jar create mode 100644 spring-web-reactive/gradle/wrapper/gradle-wrapper.properties create mode 100755 spring-web-reactive/gradlew create mode 100644 spring-web-reactive/gradlew.bat diff --git a/spring-web-reactive/gradle/wrapper/gradle-wrapper.jar b/spring-web-reactive/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..30d399d8d2bf522ff5de94bf434a7cc43a9a74b5 GIT binary patch literal 52271 zcmafaW0a=B^559DjdyI@wy|T|wr$(CJv+9!W822gY&N+!|K#4>Bz;ajPk*RBjZ;RV75EK*;p4^!@(BB5~-#>pF^k0$_Qx&35mhPenc zNjoahrs}{XFFPtR8Xs)MInR7>x_1Kpw+a8w@n0(g``fp7GXFmo^}qAL{*%Yt$3(FfIbReeZ6|xbrftHf0>dl5l+$$VLbG+m|;Uk##see6$CK4I^ ziDe}0)5eiLr!R5hk6u9aKT36^C>3`nJ0l07RQ1h438axccsJk z{kKyd*$G`m`zrtre~(!7|FcIGPiGfXTSX`PzlY^wY3ls9=iw>j>SAGP=VEDW=wk2m zk3%R`v9(7LLh{1^gpVy8R2tN#ZmfE#9!J?P7~nw1MnW^mRmsT;*cyVG*SVY6CqC3a zMccC8L%tQqGz+E@0i)gy&0g_7PV@3~zaE~h-2zQ|SdqjALBoQBT2pPYH^#-Hv8!mV z-r%F^bXb!hjQwm2^oEuNkVelqJLf029>h5N1XzEvYb=HA`@uO_*rgQZG`tKgMrKh~aq~ z6oX{k?;tz&tW3rPe+`Q8F5(m5dJHyv`VX0of2nf;*UaVsiMR!)TjB`jnN2)6z~3CK@xZ_0x>|31=5G$w!HcYiYRDdK3mtO1GgiFavDsn&1zs zF|lz}sx*wA(IJoVYnkC+jmhbirgPO_Y1{luB>!3Jr2eOB{X?e2Vh8>z7F^h$>GKmb z?mzET;(r({HD^;NNqbvUS$lhHSBHOWI#xwT0Y?b!TRic{ z>a%hUpta3P2TbRe_O;s5@KjZ#Dijg4f=MWJ9euZnmd$UCUNS4I#WDUT2{yhVWt#Ee z?upJB_de&7>FHYm0Y4DU!Kxso=?RabJ*qsZ2r4K8J#pQ)NF?zFqW#XG1fX6dFC}qh z3%NlVXc@Re3vkXi*-&m)~SYS?OA8J?ygD3?N}Pq zrt_G*8B7^(uS7$OrAFL5LvQdQE2o40(6v`se%21Njk4FoLV-L0BN%%w40%k6Z1ydO zb@T(MiW@?G-j^j5Ypl@!r`Vw&lkJtR3B#%N~=C z@>#A{z8xFL=2)?mzv;5#+HAFR7$3BMS-F=U<&^217zGkGFFvNktqX z3z79GH^!htJe$D-`^(+kG*);7qocnfnPr^ieTpx&P;Z$+{aC8@h<0DDPkVx`_J~J> zdvwQxbiM1B{J6_V?~PNusoB5B88S%q#$F@Fxs4&l==UW@>9w2iU?9qMOgQWCl@7C* zsbi$wiEQEnaum!v49B_|^IjgM-TqMW!vBhhvP?oB!Ll4o-j?u3JLLFHM4ZVfl9Y_L zAjz@_3X5r=uaf|nFreX#gCtWU44~pA!yjZNXiZkoHhE$l@=ZTuxcLh53KdMOfanVe zPEX(#8GM7#%2*2}5rrdBk8p#FmzpIC>%1I9!2nRakS|^I*QHbG_^4<=p)(YOKvsTp zE#DzUI>Y&g)4mMaU6Bhrm8rSC{F_4J9sJlF0S5y5_=^l!{?W_n&SPj&7!dEvLzNIRMZBYyYU@Qftts7Zr7r>W- zqqk46|LEF|&6bn#CE~yMbiF&vEoLUA(}WzwmXH_=<~|I(9~{AE$ireF7~XBqPV2)* zcqjOCdi&>tUEuq31s(|TFqx>Wuo(ooWO(sd!W~Hu@AXg=iQgq^O3Lv9xH$vx*vrgDAirQqs9_DLS1e45HcUPdEMziO?Mm1v!)n93L%REy=7 zUxcX!jo!vyl_l0)O(Y~OT``;8mB(tcf}`Rh^weqPnDVDe-ngsZ~C z`onh0WLdaShAAb-3b{hT5ej9a$POQ9;RlPy}IYzKyv+8-HzB7fV!6X@a_T61qZ zWqb&&ip*@{;D-1vR3F2Q&}%Q>TFH&2n?2w8u8g=Y{!|;>P%<@AlshvM;?r7I)yXG% z^IpXZ(~)V*j^~sOG#cWCa+b8LC1IgqFx+Mq$I`6VYGE#AUajA9^$u-{0X#4h49a77 zH>d>h3P@u!{7h2>1j+*KYSNrKE-Q(z`C;n9N>mfdrlWo$!dB35;G4eTWA}(aUj&mNyi-N+lcYGpA zt1<~&u`$tIurZ2-%Tzb1>mb(~B8;f^0?FoPVdJ`NCAOE~hjEPS) z&r7EY4JrG~azq$9$V*bhKxeC;tbBnMds48pDuRy=pHoP*GfkO(UI;rT;Lg9ZH;JU~ zO6gTCRuyEbZ97jQyV7hM!Nfwr=jKjYsR;u8o(`(;qJ(MVo(yA<3kJximtAJjOqT=3 z8Bv-^`)t{h)WUo&t3alsZRJXGPOk&eYf}k2JO!7Au8>cvdJ3wkFE3*WP!m_glB-Rt z!uB>HV9WGcR#2n(rm=s}ulY7tXn5hC#UrNob)-1gzn-KH8T?GEs+JBEU!~9Vg*f6x z_^m1N20Do}>UIURE4srAMM6fAdzygdCLwHe$>CsoWE;S2x@C=1PRwT438P@Vt(Nk` zF~yz7O0RCS!%hMmUSsKwK$)ZtC#wO|L4GjyC?|vzagOP#7;W3*;;k?pc!CA=_U8>% z%G^&5MtFhvKq}RcAl))WF8I#w$So?>+_VEdDm_2=l^K320w~Bn2}p+4zEOt#OjZ6b zxEYoTYzvs$%+ZYwj;mZ@fF42F1-Hb<&72{1J)(D~VyVpo4!dq259t-_Oo3Yg7*R`N zUg!js4NRyfMbS*NLEF}rGrlXz0lHz))&&+B#Tdo@wlh-Q8wr7~9)$;s9+yJH0|m=F zSD9mUW>@HLt}mhAApYrhdviKhW`BfNU3bPSz=hD+!q`t*IhG+Z4XK;_e#AkF5 z&(W7iUWF4PNQ+N!-b-^3B$J4KeA1}&ta@HK=o2khx!I&g#2Y&SWo-;|KXDw!Xb)mP z$`WzPA!F(h*E=QP4;hu7@8J&T|ZPQ2H({7Vau6&g;mer3q?1K!!^`|0ld26 zq|J&h7L-!zn!GnYhjp`c7rG>kd1Y%8yJE9M0-KtN=)8mXh45d&i*bEmm%(4~f&}q@ z1uq)^@SQ~L?aVCAU7ZYFEbZ<730{&m?Un?Q!pxI7DwA^*?HloDysHW{L!JY!oQ8WMK(vT z@fFakL6Ijo$S$GH;cfXcoNvwVc8R7bQnOX2N1s$2fbX@qzTv>748In?JUSk@41;-8 zBw`fUVf$Jxguy{m1t_Z&Q6N$Ww*L9e%6V*r3Yp8&jVpxyM+W?l0km=pwm21ch9}+q z$Z&eb9BARV1?HVgjAzhy);(y1l6)+YZ3+u%f@Y3stu5sSYjQl;3DsM719wz98y4uClWqeD>l(n@ce)pal~-24U~{wq!1Z_ z2`t+)Hjy@nlMYnUu@C`_kopLb7Qqp+6~P=36$O!d2oW=46CGG54Md`6LV3lnTwrBs z!PN}$Kd}EQs!G22mdAfFHuhft!}y;8%)h&@l7@DF0|oy?FR|*E&Zuf=e{8c&hTNu# z6{V#^p+GD@A_CBDV5sM%OA*NwX@k1t?2|)HIBeKk(9!eX#J>jN;)XQ%xq^qVe$I}& z{{cL^a}>@*ZD$Ve)sJVYC!nrAHpV~JiCH3b7AQfAsEfzB$?RgU%+x7jQ_5XQ8Gf*N`i<1mZE zg6*_1dR3B`$&9CxHzk{&&Hf1EHD*JJF2glyBR+hBPnwP@PurN`F80!5{J57z;=kAc za65ouFAve7QEOmfcKg*~HZ04-Ze%9f)9pgrVMf7jcVvOdS{rf+MOsayTFPT}3}YuH z$`%^f$}lBC8IGAma+=j9ruB&42ynhH!5)$xu`tu7idwGOr&t=)a=Y2Sib&Di`^u9X zHQ=liR@by^O`ph|A~{#yG3hHXkO>V|(%=lUmf3vnJa#c%Hc>UNDJZRJ91k%?wnCnF zLJzR5MXCp)Vwu3Ew{OKUb?PFEl6kBOqCd&Qa4q=QDD-N$;F36Z_%SG}6{h2GX6*57 zRQIbqtpQeEIc4v{OI+qzMg_lH=!~Ow%Xx9U+%r9jhMU=7$;L7yJt)q+CF#lHydiPP zQSD=AtDqdsr4G!m%%IauT@{MQs+n7zk)^q5!VQrp?mFajX%NQT#yG9%PTFP>QNtfTM%6+b^n%O`Bk74Ih| zb>Fh1ic{a<8g<{oJzd|@J)fVVqs&^DGPR-*mj?!Z?nr<f)C8^oI(N4feAst}o?y z-9Ne339xN7Lt|Tc50a48C*{21Ii$0a-fzG1KNwDxfO9wkvVTRuAaF41CyVgT?b46; zQvjU!6L0pZM%DH&;`u`!x+!;LaPBfT8{<_OsEC5>>MoJQ5L+#3cmoiH9=67gZa;rvlDJ7_(CYt3KSR$Q#UR*+0hyk z>Dkd2R$q~_^IL2^LtY|xNZR(XzMZJ_IFVeNSsy;CeEVH|xuS#>itf+~;XXYSZ9t%1moPWayiX=iA z!aU~)WgV!vNTU=N;SpQ((yz#I1R#rZ&q!XD=wdlJk4L&BRcq(>6asB_j$7NKLR%v; z9SSp$oL7O|kne`e@>Bdf7!sJ*MqAtBlyt9;OP3UU1O=u6eGnFWKT%2?VHlR86@ugy z>K)(@ICcok6NTTr-Jh7rk=3jr9`ao!tjF;r~GXtH~_&Wb9J^ zd%FYu_4^3_v&odTH~%mHE;RYmeo+x^tUrB>x}Is&K{f+57e-7Y%$|uN%mf;l5Za95 zvojcY`uSCH~kno zs4pMlci*Y>O_pcxZY#?gt1^b-;f(1l9}Ov7ZpHtxfbVMHbX;579A>16C&H5Q>pVpH5LLr<_=!7ZfX23b1L4^WhtD?5WG;^zM}T>FUHRJv zK~xq88?P);SX-DS*1LmYUkC?LNwPRXLYNoh0Qwj@mw9OP&u{w=bKPQ)_F0-ptGcL0 zhPPLKIbHq|SZ`@1@P5=G^_@i+U2QOp@MX#G9OI20NzJm60^OE;^n?A8CH+XMS&3ek zP#E7Y==p;4UucIV{^B`LaH~>g6WqcfeuB#1&=l!@L=UMoQ0$U*q|y(}M(Y&P$Xs&| zJ&|dUymE?`x$DBj27PcDTJJn0`H8>7EPTV(nLEIsO&9Cw1Dc&3(&XFt9FTc{-_(F+ z-}h1wWjyG5(ihWu_3qwi; zAccCjB3fJjK`p=0VQo!nPkr0fT|FG;gbH}|1p`U>guv9M8g2phJBkPC`}ISoje6+? zvX|r5a%Y-@WjDM1&-dIH2XM}4{{d&zAVJQEG9HB8FjX&+h*H=wK=xOgNh8WgwBxW+ z0=^CzC4|O_GM>^_%C!!2jd&x*n2--yT>PZJ`Mok6Vf4YFqYp@a%)W}F4^DpKh`Cr7 z{>Z7xw-4UfT@##s#6h%@4^s^7~$}p2$v^iR5uJljApd9%#>QuxvX+CSZv18MPeXPCizQ*bm);q zWhnVEeM}dlCQP*^8;Q7OM|SSgP+J;DQy|bBhuFwJ2y*^|dBwz96-H;~RNsc}#i= zwu`Tp4$bwRVb7dxGr_e1+bJEc=mxLxN_f>hwb#^|hNdewcYdqXPrOxDE;|mP#H|a% z{u8#Vn}zVP(yJ}+-dx;!8<1in=Q8KsU%Q5CFV%5mGi8L;)*m%Vs0+S`ZY(z7aZ$VCjp?{r>C<9@$zVN;LVhxzPEdDPdb8g<)pckA z?mG@Ri>ode(r|hjNwV#*{!B^l2KO@4A+!X;#PW#?v2U!ydYIFHiXC3>i2k7{VTfji>h z8-(^;x!>f)Qh$mlD-z^1Nxu})XPbN=AUsb%qhmTKjd=1BjKr(L9gb1w4Y8p+duWfS zU>%C>*lCR@+(ku!(>_SA6=4CeM|$k4-zv|3!wHy+H&Oc$SHr%QM(IaBS@#s}O?R7j ztiQ>j^{X)jmTPq-%fFDxtm%p|^*M;>yA;3WM(rLV_PiB~#Eaicp!*NztJNH;q5BW$ zqqlfSq@C0A7@#?oRbzrZTNgP1*TWt(1qHii6cp5U@n|vsFxJ|AG5;)3qdrM4JElmN z+$u4wOW7(>$mMVRVJHsR8roIe8Vif+ml3~-?mpRos62r0k#YjdjmK;rHd{;QxB?JV zyoIBkfqYBZ!LZDdOZArQlgXUGmbpe7B-y7MftT;>%aM1fy3?^CuC{al$2-tfcA?d) z<=t7}BWsxH3ElE^?E&|f{ODX&bs+Ax>axcdY5oQ`8hT)YfF%_1-|p*a9$R~C=-sT| zRA~-Q$_9|G(Pf9I+y!zc>fu)&JACoq&;PMB^E;gIj6WeU=I!+scfSr}I%oD1fh+AQ zB^Q^b@ti5`bhx+(5XG5*+##vV>30UCR>QLYxHYY~k!AR`O6O_a3&wuW61eyHaq;HL zqy@?I*fmB)XY;Z@RH^IR|6m1nwWv>PDONtZV-{3@RkM_JcroRNLTM9?=CI}l%p86A zdxv|{zFWNI;L8K9hFSxD+`-pwvnyS|O?{H-rg6dPH<3oXgF0vU5;~yXtBUXd>lDs~ zX!y3-Pr9l;1Q^Z<15_k1kg|fR%aJKzwkIyED%CdxoXql=^QB;^*=2nVfi{w?0c@Dj z_MQEYjDpf^`%)$|4h>XnnKw05e5p4Jy69{uJ5p|PzY+S?FF~KWAd0$W<`;?=M+^d zhH&>)@D9v1JH2DP?tsjABL+OLE2@IB)sa@R!iKTz4AHYhMiArm)d-*zitT+1e4=B( zUpObeG_s*FMg$#?Kn4%GKd{(2HnXx*@phT7rEV?dhE>LGR3!C9!M>3DgjkVR>W)p3 zCD0L3Ex5-#aJQS6lJXP9_VsQaki5#jx}+mM1`#(C8ga~rPL{2Z;^^b+0{X)_618Sw z0y6LTkk;)quIAYpPY{)fHJLk?)(vxt?roO24{C!ck}A)_$gGS>g!V^@`F#wg+%Cok zzt6hJE|ESs@S^oHMp3H?3SzqBh4AN(5SGi#(HCarl^(Jli#(%PaSP9sPJ-9plwZv{ z1lkTGk4UAXYP^>V+4;nQ4A~n-<+1N)1lPzXIbG{Q;e3~T_=Trak{WyjW+n!zhT*%)q?gx zTl4(Gf6Y|ALS!H$8O?=}AlN=^3yZCTX@)9g5b_fif_E{lWS~0t`KpH8kkSnWWz+G1 zjFrz}gTnQ2k-`oag*031Nj7=MZfP}gvrNvv_crWzf9Cdzv^LyBeEyF2#hGg8_C8jW)NCAhsm2W_P21DeX7x$4EDD){~vBiLoby=d+&(;_f(?PMfamC zI_z%>Nq-rC%#z#1UC49j4@m63@_7LWD$ze=1%GPh`%@PB7yGH6Zh=1#L%&%hU7z%Y zs!IN(ef@!+|1YR28@#kw^XR= zxB$*nNZm7Y@L0&IlmoN}kEI?dBee+z+!MWCy+e4P4MYpOgr}2Q(wnR1ZiA>5_P*Cg zB4BMlcx?(v*+V3O+p~Buk;wIN6v!Ut?gYpl+KFu~elf}{E4`9+lcR0k$bC>+I zWxO5jD8sYPbMS)4c3i2UojI4T7uzE*Zz;POw{0d0`*iHJ%(Pb=sa^pV{t_JtHoPeC zX+t_k*=D%+Sv#+5CeoRfI)G`T90~AE@K9RaFR%8*w#*x9>H$ahFd>PUg_zP`VVPSR zr#Rb;I--8Rq;eTBju;dx2cmZ9Al>aiDY z#7(4S(A#aRvl7jm78sQ+O^S5eUS8|W%5@Pt9fm?J=r`~=l-gdv(LB~C-Gi#srwEDQ z4cCvA*XiRj9VDR6Ccy2k(Nvxic;~%YrfNeWl$cJpa%WO_4k?wxKZ{&`V#!&#jV@x+ z7!!YxOskc;cAF~`&aRWp8E)fnELtvb3-eHkeBPb~lR&iH=lZd^ZB(T6jDg5PnkJQFu9? z+24ww5L%opvEkE$LUHkZDd0ljo!W}0clObhAz`cPFx2)X3Sk91#yLL}N6AE0_O`l| z7ZhaKuAi7$?8uuZAFL(G0x3wE<-~^neGm=*HgJa(((J;yQI$NB)J;i0?vr`M1v+R? zd+{rD^zK}0Gi!2lXo0P+jVQ$HNYn^sRMONYVZPPT@enUb1pHHYgZMo5GN~SIz*;gv z1H<4(%53!6$4+VX_@Kp!>A9wwo{(KdWx)ja>x3&4=H(Urbn?0Vh}W3%ly5SgJ<+X5?N7-B=byoKyICr>3 zIFXe;chMk7-cak~YKL8Bf>VbZbX{5L9ygP_XS?oByNL*zmp8&n9{D42I^=W=TTM4X zwb_0axNK?kQ;)QUg?4FvxxV7L@sndJL0O12M6TMorI&cAL%Q464id6?Tbd_H!;=SRW9w2M*wc00yKVFslv|WN( zY7=Yikt+VY@DpzKq7@z_bVqr7D5B3xRbMrU5IO7;~w2nNyP7J_Gp>>7z?3!#uT4%-~h6)Ee1H z&^g}vZ{g}DIs@FDzE$QG_smSuEyso@I#ID3-kkYXR=nYuaa0{%;$WzZC@j)MDi+jC z!8KC;1mGCHGKr>dR;3;eDyp^0%DH`1?c7JcsCx$=m(cs^4G& zl@Fi8z|>J`^Z-faK{mhsK|;m%9?luacM+~uhN@<20dfp4ZN@qsi%gM67zZ`OHw=PE zr95O@U(HheB7OBYtyF=*Z5V&m?WDvIQ`edwpnT?bV`boB z!wPf&-@7 z0SoTB^Cy>rDHm%^b0cv@xBO%02~^=M79S}TG8cbVhj72!yN_87}iA1;J$_xTb+Zi@76a{<{OP0h&*Yx`U+mkA#x3YQ} zPmJsUz}U0r?foPOWd5JFI_hs_%wHNa_@)?(QJXg>@=W_S23#0{chEio`80k%1S?FWp1U;4#$xlI-5%PEzJcm zxjp$&(9f2xEx!&CyZZw|PGx&4$gQbVM|<2J&H7rpu;@Mc$YmF9sz}-k0QZ!YT$DUw z_I=P(NWFl!G-}aofV?5egW%oyhhdVp^TZH%Q4 zA2gia^vW{}T19^8q9&jtsgGO4R70}XzC-x?W0dBo+P+J8ik=6}CdPUq-VxQ#u4JVJ zo7bigUNyEcjG432-Epy)Rp_WDgwjoYP%W|&U~Gq-r`XK=jsnWGmXW6F}c7eg;$PHh>KZ@{cbTI<`ZP>s(M@zy=aHMA2nb(L0COlVcl8UXK+6`@Di+Wai;lJf^7s6V%NkKcad zDYY%2utqcw#CJFT9*V9U_{DyP&VYb)(6y`Z%Rq& z!PTtuI#psBgLPoNu{xvs^y26`oY;p!fE=bJW!cP^T>bUE*UKBV5Bd%!U{Q5{bKwN> zv)pn@Oc{6RyIS>!@Yvkv+hVLe+bmQ6fY2L}tT)Vbewg8`A`PFYyP+@QmL?b{RED;; zR6fwAAD}Ogejah(58bv{VG&WJhll7X-hjO9dK`8m5uFvthD1+FkJtT_>*{yKA(lXx zKucHMz#F_G)yTJw!)I3XQ7^9ydSlr9D)z?e*jKYE?xTKjR|ci30McU^4unzPsHGKN zMqwGd{W_1_jBQ_oeU^4!Ih}*#AKF%7txXZ0GD}Jzcf+i*?WLAe6#R_R-bSr17K%If z8O2SwYwMviXiJ?+$% zse=E~rK*PH@1Md4PFP)t(NhV%L3$657FUMap?fugnm3|N z79w3|qE%QyqZB}2WG&yc>iOaweUb`5o5p9PgyjqdU*sXP=pi$-1$9fGXYgS2?grS6 zwo#J~)tUTa0tmGNk!bg*Pss&uthJDJ$n)EgE>GAWRGOXeygh;f@HGAi4f){s40n?k z=6IO?H1_Z9XGzBIYESSEPCJQrmru?=DG_47*>STd@5s;1Y|r*+(7s4|t+RHvH<2!K z%leY$lIA{>PD_0bptxA`NZx-L!v}T4JecK#92kr*swa}@IVsyk{x(S}eI)5X+uhpS z8x~2mNLf$>ZCBxqUo(>~Yy4Z3LMYahA0S6NW;rB%)9Q z8@37&h7T$v2%L|&#dkP}N$&Jn*Eqv81Y*#vDw~2rM7*&nWf&wHeAwyfdRd%`>ykby zC*W9p2UbiX>R^-!H-ubrR;5Z}og8xx!%)^&CMl(*!F%or1y&({bg?6((#og-6Hey&3th3S%!n3N|Z2ZCZHJxvQ9rt zv|N#i*1=qehIz_=n*TWC6x-ab)fGr8cu!oYV+N)}3M;H4%$jwO>L!e53sxmJC~;O; zhJw|^&=2p!b8uk{-M|Z*J9n0{(8^>P+Y7vlFLc8#weQMg2iB8MFCe-*^BJV6uVWjg zWZe{-t0f67J<|IIn4{wsKlG*Amy{-yOWMMW)g}rh>uEE;jbkS-om>uAjeTzCg51683UTmY4+yT zW!qe`?~F{~1Y>mPJ9M0hNRBW$%ZwOA-NdIeaE6_K z>y8D3tAD7{3FouIXX9_MbY;zq%Ce0}VmT;aO~=*Mk4mflb_i4CApxEtZ^TDNoOzy_ z-eIE(&n1Vz*j&(BjO*fVvSCozTJU4?tWC8m4=d|D{WV0k+0M2!F1=T}z7V4-JA*y( z!;H(sOBmg=%7p&LLf%z%>VgtdN6jl2y95aXY}v9U;m~YWx{2#lwLpEJWGgs`sE*15 zvK`DtH-Q^ix>9@qVG+d*-C{lYPBbts1|%3!CkLP1t4iz%LO-di4lY%{8>jd{turVrD*_lLv!ShQC~S#SXjCO?##c zh2aZKVAHDf1sQpZiH^C7NRu?44JuEp?%W4-?d;Dg z;`gKA9$oC{WlQuT?fex!ci3GJhU;1J!YLHbyh8B-jsZ~pl59LGannKg9}1qxlbOOq zaJhTl zEJ`2Xd_ffdK^EE1v>8kUZG`eMXw(9S+?Lxx#yTUo?WdV}5kjC|glSJqX zv8RO|m#Ed@hW=};Yfl&2_@11Xm}pz0*SRx%OH_NODo@>e$cMAv(0u`~Yo|qbQ~mzA zMKt^U+GIXKH^xuD9n}NfU|?ZTOSS>XJwlg`lYHgea)!ZR?m^=oj+qyKBd6SJvPZk* zwc-2$b%%V~k$5{=(rG!OcR{;u2V3um|C+oT5F?rt`CER|iU9-!_|GxMe^!f$d6*iz z{?~JnR84mS+!gFUxugG?g9uGFI(?Q0SADS8=n=#aCK^`6@rm4r=LJTBm;)cY zm_6c5!ni$SWFOuj36eKau>6=kl_p=-7>VL_fJuJZI}0=3kASf|t;B~;Mt(vuhCU+c zKCF@SJ5#1>8YLfe{pf?sH*v6C)rOvO1~%@+wN}#>dkcrLw8U@xAySc{UeaP?7^AQ5 zmThfw^(i@*GMlM!xf+dzhRtbo8#;6Ql_s$t15q%*KeCm3`JrXnU*T^hV-aGX)bmxF z;O%jGc{6G+$gZ$YvOM2bZ!?>X<^-D zbT+YCx722}NY88YhKnw?yjF1#vo1v+pjId;cdyT*SH@Bc>6(GV*IBkddKx%b?y!r6 z=?0sTwf`I_Jcm(J8D~X@ESiO`X&i53!9}5l}PXzSYf9 zd&=h`{8BP-R?E*Nk$yzSSFhz2uVerdhbcCWF{S7reTkzXB;U@{9`hvC0AscwoqqU( zKQavt5OPm9y1UpKL%O(SWSSX=eo2rky_8jJ-ew7>iw~T=Xrt3EEzc!slebwG)FrE> z>ASkjJk%#@%SFWs-X4)?TzbBtDuwF#;WVw}?(K`UYqm`3vKbFKuqQ8uL2Y5}%T0y5 zia#E?tyZgnuk$LD^ihIn(i~|1qs(%NpH844QX-2S5E)E7lSM=V56o>5vLB^7??Vy_ zgEIztL|85kDrYF(VUnJ$^5hA;|41_6k-zO#<7gdprPj;eY_Et)Wexf!udXbBkCUA)>vi1E!r2P_NTw6Vl6)%M!WiK+jLRKEoHMR zinUK!i4qkppano|OyK(5p(Dv3DW`<#wQVfDMXH~H(jJdP47Y~`% z#ue|pQaVSv^h#bToy|pL!rWz8FQ53tnbEQ5j#7op?#c#(tj@SM2X*uH!;v8KtS5Fo zW_HE8)jSL zYO}ii#_KujRL4G*5peU)-lDW0%E}!YwL#IKUX_1l9ijy~GTFhO?W^=vEBe?m+tvBe zLaGWcoKg==%dO#6R}`U0>M)2+{b*~uamlaUNN<_NVZTGY4-(ORqK6|HvKFMKwp6^L zR+MC^`6^|^=u^Do;wy8mUp^Oct9~=vQ74vfO-m&Q0#~-mkqkpw&dMkVJ(So<)tf3h z46~mW_3T@Mzh<2XZYO7@F4j|BbhhXjs*hayIjTKyGoYO}`jEFn^!4Y! zL30ubp4U(r>Nx&RhaJkGXuRe%%f%D;1-Zdw2-9^Mq{rP-ZNLMpi~m+v?L=sPSAGcc z{j+Y!3CVrm);@{ z;T?sp1|%lk1Q&`&bz+#6#NFT*?Zv3k!hEnMBRfN47vcpR20yJAYT(5MQ@k;5Xv@+J zLjFd{X_il?74aOAMr~6XUh7sT4^yyLl%D89Io`m5=qK_pimk+af+T^EF>Y)Z{^#b# zt%%Bj9>JW!1Zx_1exoU~obfxHy6mBA{V6E)12gLp-3=21=O82wENQ}H@{=SO89z&c*S8Veq8`a3l@EQO zqaNR8IItz4^}>9d+Oj%YUQlb;;*C0!iC&8gaiDJ)bqg(92<>RbXiqFI3t#jqI%3Y( zPop=j=AyLA?pMYaqp0eHbDViOWV-5IUVwx+Fl6M54*?i+MadJHIRjiQoUe?v-1XdQ z5S305nVbg|sy~qPr2C6}q!v)8E%$i~p5_jGPA0%3*F%>XW6g)@4-z73pVcvWs$J2m zpLeW4!!31%k#VUG76V__S**9oC{-&P6=^fGM$2q<+1eC}Fa2EB3^s{ru^hI}e^KPM zMyj;bLtsRex^QMcgF)1U0biJ|ATXX`YuhzWMwP73e0U?P=>L|R?+13$8(PB23(4Js zy@KS0vvS~rk*^07Bd4}^gpc|e5%248Mei_y^mrD;zUYniPazU>1Dun%bVQ0T7DNXr zMq4Y09V_Dr1OQ$ni)BSyXJZ+D7 zXHh02bToWd;4AlF-G`mk23kD=$9B)}*I@kF9$WcOHc%d6BdemN(!^z0B3rvR>NPQ? z+vv#Qa~Ht|BiTdcN;g6;eb6!Jso)MFD3{sf{T;!fM^OwcEtoJI#ta?+R>|R;Ty2E% zjF8@wgWC=}Kkv52c@8Psigo4#G#E?T(;i}rq+t}E(I(gAekZX;HbTR5ukI>8n5}oC zXXTcy>tC{sG$yFf?bIqBAK3C^X3OAY^Too{qI_uZga0cK4Z$g?Zu$#Eg|UEusQ)t% z{l}Zjf5OrK?wkKJ?X3yvfi{Nz4Jp5|WTnOlT{4sc3cH*z8xY(06G;n&C;_R!EYP+m z2jl$iTz%_W=^)Lhd_8hWvN4&HPyPTchm-PGl-v~>rM$b>?aX;E&%3$1EB7{?uznxn z%yp0FSFh(SyaNB@T`|yVbS!n-K0P|_9dl=oE`7b?oisW)if(`g73bkt^_NHNR_|XU z=g?00`gZRHZm+0B(KvZ0?&(n<#j!sFvr|;G2;8qWg3u%P;M1+UL!9nj)q!}cd}jxK zdw=K$?NuLj?2#YzTCEw1SfLr#3`3x(MB2F(j!6BMK!{jXF%qs;!bIFpar}^=OYmYm z86RJ9cZl5SuR6emPB>yrO)xg5>VucBcrV3UxTgZcUu(pYr+Sa=vl>4ql{NQy4-T%M zlCPf>t}rpgAS15uevdwJR_*5_H?USp=RR?a>$gSk-+w;VuIhukt9186ppP=Lzy1L7 ztx(smiwEKL>hkjH7Y))GcUk`Y z5ECCi%1tZE!rM4TU=lk^UdvMlTfvxem>?j&r?OZ>W4w?APw@uZ8qL`fTtS zQtB<7SczI&5ZKELNH8DU6UNe1SFyvU%S#WTlf%`QC8Z+*k{IQx`J}f79r+Sj-x|4f<|Jux>{!M|pWYf+ z-ST5a#Kn+V{DNZ0224A_ddrj3nA#XfsiTE9S+P9jnY<}MtGSKvVl|Em)=o#A607CfVjjA9S%vhb@C~*a2EQP= zy%omjzEs5x58jMrb>4HOurbxT7SUM@$dcH_k6U7LsyzmU9Bx3>q_Ct|QX{Zxr4Fz@ zGJYP!*yY~eryK`JRpCpC84p3mL?Gk0Gh48K+R$+<|KOB+nBL`QDC%?)zHXgyxS2}o zf!(A9x9Wgcv%(sn!?7Ec!-?CcP%no4K?dJHyyT)*$AiuGoyt=pM`gqw%S^@k8>V0V z4i~0?c>K{$I?NY;_`hy_j6Q{m~KDzkiGK z_ffu;1bT+d;{6`SacCO z!z#1#uQP5`*%p&Urrk=&0`h1PBJxx*71yfl$|0Lt5_Lu$sO+F4>trJ6BS{J-of(R; znqrX@GUAyelkAOB;AqN)kur^1$g*t8&pGsyNZ|n42P$;s}e=Ef0&U zeA`jZs*E%l;3wd$oo^8Kh+#$+NzBNTi(70iEH)=Otim-ufx?&1Fe!w}-a_WL z3b9@#v&pt7wVF#bkr-YWhG|rhfwMABMZ<*Ku}@(4l8Aw|vSX#w9;23Ms1w zSC<+Ir!HNnF0m<+sQEdpqfFZn$+xA08nrn>k%Grb^0QdkgbOV;Kit2W`YwlfP5RRT2G3s4h?t5)!UZt~ ztK#FBL&P1pKsrye8S{&w@^ExelK;!LKh>=_q@VYF? z;_>~#$&OM13&!w@lx3P~g8~N3^wGM$Ybs$gFU+qlyxpp`?%oPWZNF-V;}NI47Q3^L z6zQ5TW`2EtX}l&7$2>xy4$xi;EXMN9^>l^O zpX}dt^G-p)6VSPIUolW9$svfNPfx=thP`;1S+wNs+PSh6QZ=X3FEu=#Ih!t_jC#tY z7t4@L1kbqL!4$7DY4QrHWPRfRvrE1hZcJR!wneIey(qiO(&qR5njE7~Vx5a{vafU= z)ya$}INqMlnsl?CHs*Gm@?JIPF$yE8pr2XE$;!z~-)=K?U$T3tT|t*z%Y~?_FuuG# zdxk5YL7D5##gr{wj@q_8USae@D&~NiU&5b$mcj$)ciL;Pm?1INBK8<9Uy##y@F;CU zG{5BquPJ2$`&r0uq3sHTD{+s!8^B47^RipsiHgpRoUp)5`1Om|oJQYZFd->&WM-2Y z+jMSmGg#v0-K{lm@K7En;FAw9nqm8(_94>4itl{!&h$c5Jhb(>aE;^WG5a0ho_P#k z=`>n+Y4`!6VFcFp<(fDGn0XZI%j$-p+V`Wfsdx5gviUanQCQKMLC02L-kZhqAFDJKEt24JM32 zX>A|&bwLR-xGzX@mrw_b>J0xDVriQ#YH{AYpBzPxW*}IViqyF8u~q zU?C~D8N<#3QCgHa! z%i?KtB+B&v;W5W8oy2USy=LKTj+&_Z`QpJr`GcqVwtDRmc6|RBE?NV#eo})g*6rN} zhVAR1l^#prL+5!{^P0NZ+RejdQ+Ik@^7pH{{xCL;z5Ef)do(8!08u9ieL2#1dVKMYKYZxBy98#CFs?lUx*#_eEO!>K!DVcH zdGN^HncO_w*;SJDV*_W|+&${EN7qQ1S1yi}H5b=0yu!PJ`dqxvn|pgs`A^1u$=l`! z7AEW-85?pZc4n>skM$;VkgurkG)2ecbYIlvN>b%UaLQareR0du>kXIMne04Rjh>ja zOJm_v=A~pE$}gH^TK6G5iT7xseUX#3keV|HJR9+g$u1o)wk^sTKGu+^WK4Dd6|PCC z*&kMT2?F_IS8|8B=Pgvkp`~)4nQ&T0-*6`YgSiY(GYn4))c1*2(ByIjf}HX8)B7rC z&d5F1D8EZT|BW`XU*~9w2)wL&5BLA(s{AwN`Cq`IT#a9vsG4Y>{48Y5F*r`NXsH?- zVTMpq8!(pQLZuRFNJ`bUqAX!QjVN;EgzPSiZEP^R9oBqXv+2Lf41bTiXwO@$_dEag z)4$-NHxpbc;(k6S`E9%V_Z7f<$NO$<=f@U!1BT{FA;w$gJM_RPC15g24TclHHNn= z%3))Msl?FP(v#6f=JB3R3(=~4{1-z9c(u5S4a?YsMm`I{<$RtS!4}}}Ls16B*~;RA zCFE^3T{I0u&U)AygIU#$7lBjVWRxt%JD|3mUGu4?1k3&FxUGkmjn>V`{dku=<;nM6H?3 z8xw;O<`w#tgfx@pCrNvj1x6M;bIoMn)ImU<%Z(~Dvg^o_X`D1>gDTAF1JlQ` z?Y0Rk=%+L12xR2Um(UM}Q!Uv+W%0yiatJP4)MXpxqnE?ceur3dpWVT$$C7W(Ad7OQ zW(07FjoY#!D~GG+S__T8FK&rdV8o2D$m<$v|3OeBckZrXV6vJB?+I0Q&55akuCrPQ zZU*OQXVhoj-{S`xTc(oCS}h)dA5qXgY;`LeY~fN~j3}d%Wj}YsHH!*FgWWVKtEo7% zHJCka&s(kt!Ix0uOwK~ysoe-RpANP#;|q6T$^GHRvO+{woF|P1&w_Kq=aoSqGzz;$ z*Wd$VhR9xrypy(YpJ6@06_07w6Ovvj^KcA}U4Pw$jA_~vwQAZkdkBBr8`%yn^BXnF zY|1lx{c2Y~DyMp-ZA=8M4nE-5zQ0V;O>J}Y+q0W4x)$_;wo<8D%n z!`fVX#C)T*rrWYPfxn@Q6qUT_)*!tiSediBO-cWahFdGUC+AFOSeqs;VqMXEvu z*%o*tngNJ+?;X}x>R4%u!~{AX)S}i#{yd>aw4uJZu8tysnfsX->l#F&^>#dTfy;r$ z9&&l4K^kS`n=Z?f{iVrgD@h2mp&`v~L{?|ix`67n;1n!!9Q9;ZT8{Z%tjs%KO;cRe zPUo=>|D{SI8*Zta^OK+@3{;6}Prl^Xo^!LgN89!4j#^fkSbG(fbc|}r9kfF?xK6Xn z1YQ@5h8GS>!!w45QHt_v&=*8WKMCyg^sG1>yC2jI6$OMH3*2k5pYYxNp2ruxMERnP zt>?dmG`|IjgqE?Y zfm?|c1z(LRCd0xBr_~~k6@@Vn{e_;CW=N{cxgOB7t*8bx)NVks2EHMQr1{_-@iJ4Yow z&jrCB7?wL1L^MwKQ<}W8nuXleT$a{lrIC+Lh^3X%lVS-Jj*O+ZeScuA=u{mU3<%Ru z?1Ta~3{lxdLZaLB{rnA*1cW#L6jcEUfR8x&{D2H-1!dw^=@(e4V zBXPJ#v7Vw?G}0~t&j@4v@@(6bhC0Wq;*N=}g9R&l+ltUp+C|&cLHD8B64iDaD#Ufm zzBugB@HF5v-1b26O3@fuv`ye?Q@;2{aG^N4zvx1n3|nzp+b3F$EEwVhHfn!wWrHgRcNDg+Ls6o&2!~fr|<5?3~C$xM40nq>h0pa?ejgP_Um+osTtap#sTgEz{+V!DVgg2c|zr&qy`*v|%k2qN4o$ zG~S$V&%H9mvmN_*yjnif&S_LWiH3GhJ<5yURu!%M^{oke1@N`vWL^&A({Dt^_*?zF zlEwE&e!1B;B=VjSvmW&#RI9p;59vL-zmfhqVSAUbyVBG~M#rW`BM9#;U-<(X5@k?g z1!baee)903$R-8_!>)ezvDF&ECABnUmq@;}jy$N;%haQ)b&?*%Pj@Zx<&(TSPsQ!- z_%e!bOqU&-@>_GE{lssw9He!Q4iIrZC?rGvemrxq=ZuF&VNVbL`14U6X|at+LC)@` zR8$!C=E++&j+(pty&FMQAxl0-G#pW(N>jQG1P2tvmz#rF&e3`|lwl z_vYYFF~1Qo=)yCVr!-;LzgT&I7&7|z9fN9h9n@0MDUi3~0_6bOhc@D2&^ z3duiUjQ;{H{ue#*zw_EcH6#7eEU^8|o4Z+g;kYqSw5Srw;B7BSV3Jyv$P(N)*#_vK z^_85Oc-QFw)3z4o&}w$QRS)*91nMOQ=(_P~ZMIbN`|4_ZI<*?Q@0jnHODEZYb7YNa z#+SIKx9tP({1fk!sZ{@be~5nfcU3c!&;~H>pIeMLx@HGdj_QX_a-&5s5M$~&{a`c# zA&Ak(q{ef>Gz5c^Ws>UyiFa*j#b4!CQU-ibzM|cGDhWsZV zPSM2}nveE~=5PtYB;8~Plz235H}`j{M)BvqI^wQGEc z9rbH|h#k#qFbKto=fbGP=fs$DGd|LTF%%-<=*%*scyqTgW;|&88`L-(y7Tth9HVaR zp}o`R$h{t3hYWj)%I-A!LZ{EALwwb@{TtF^4+X_7df_N(Eq?3Fxa#anAZ860o$rDoQyT;#i?`Kwurj4}BKysK7>nVQmatS5Nsshp{j zyS7G_fo*7u(Q+P%>ZN*aCp~9=tjao5cGcNm4 zx^?@S<p-aIyE;r_=AYe)b9h zzj^rv6QQ-}v0Cf7A|#5k>wLX}mH8FX52>q6R``I5aj(>*f3i+(F`6LcB&TwV1f zpOPb`4mv{k7WTW=>?1?FmVkn5!big+_SX>=c}=YQa&e+ez~sI1NEr5z9CTehje?9U zeQGJpCSAGIe8Q0$Z1}|?U+hS2PcEBSm6v21_B`XcXFU*4cyc40;{?Dg}W`~c$C^r1u0R%RqHCJ>{7(eSO$^7u3m~WQPS^$-(q&7a_2fFWJdGZdcs!8Yp93#wJGXC#+@-XFx|>~ zWg5SUiLzII8_j2bhj18wt_C_~^6>s+zj6K$qg)Pb`PYDVX=J7L+tMgt(x9w6zse)J zrWWHgUJmp%E@Gd$ZWQOvCOmDbvme4&D>*tpQvISkpoe!jph2$(V=}62#;K-r=px{4 zV=SM&(@pKFvW$W==2-~S-Tw&1LunP`!S#K40}R=1o4hYtUAAOR^O1p%&9v1;e~Mv!?1a_tMZAvG7he; zE(!g+ibYMAV|59+8DrA`A5jc3-gU&9%Ehp+qlG849RhUfZbL>lW#RoS2DMsm_Ux=T z|K|#Hv5ed&H*>KDzXXiopOce3I3(3%28T)wg51@M4yl?`judhBRFQ^Vxk)BpzD!Gdf#ou14?8X#gV$8aQC5b!&aX#wKA5qk_*wO!kHj9#S3 zfpfT#SU6nAV|8c)SSQA-8;;j_hf|h4AmqgK#I6X|Bi^JQUvhn%9ZFX#PLyfSQu$;$ zzM^i?+bX!Uuk9@9_E&+n1OxbcWwm-2^nejN=dF`W8^)>>#Cc$L@=1?vuQ#K}JjXsYEEOT{m5D-P)P}ys7UNH36m!HX{b7{zuY4R~4pfGV5Vi^-?R147 zD%l%2-?es1+bV6G4n$6GR4p(3ko&IXA+~(xQE|GL`XUzQacBze?)~!~HQF&6=utZ0 z$Wf?>HaxHaz7Vdtqw>KzA8y(;k}a|po=YGKx1k_^^zUDdNeGE>hyCRQSXcu*jL_YU zN!=4suP9`?J6XnmB6T|AChiP{Y{!9n6(*xTCBh?gJ`=4!L#e({8F5LQ^NHK@iL&LB zgD@%`@R`-CxQ8~aQh5hAwL^!2&`ZWwUt^g&CcMWa%{?u|%Q0S+=Zk`S=5!;nMj;)A zUkgmCf6>4`t~Sf4PcwYnqZbg3OF+Q)geEkt@yolApC*~;%L4b=P0^y0Dri{El=}4S z$X4s4+!}Hx*_v{nC%i<}C)#4{GV~O3b$(7WKQgmbWK*gp&bxjZMh%oA%7c;!x(UHc zJb*6c%(FyzY$UeZKe>)OnXJ6J#+#kL>6H@(rRUrJPT&TM*qJ(Zen2c1RTdSPih#F! zhNn89$nUneJz{GFdfXdLUFQ%+Dp(t{OZ5rb!Y)=Jk+Cg+kyn#$K#0-9B_~2J6CFQ) z1(JpSx*^=Z{P{OsfeXY>FUNrUD+Bd}BJlGUV)>t%g8pBcg8m;&Wk(?Kfx+?rP={4# zXB4Stq}8RQ<)@~n=q9G;4pa~n<(02#W|Wy4l$aV?SeP4F*wr1~;SrRXSeV$3Xs9OV zWaJsB+vFK#C#L0Fk3jzx>V*bA5$Nc!#SHLCaDciOczy_C>}F+a zO7CoDVrJ#&`nShmSM0V2BSt!Z(j+N{2qK1%?~(#uI1gQ1s>&W^0~xV~$nW z4pqV9;_`dmw}E=^?_$ry*6P1uvj2Kx3FG%^d_azjDv%??{GVSJHvTIB zZQ?5GU}py;Zpm5Mn*nKY?m&d}e?_5F)%1b9Xf%E>*l60e2)o*ydBme)*G+*;5h2RXO{)0P3jBG!L33uaJwzU(K(pv6~PPVzduR2|hw*i9w{(m4H zBS^uZ&rjFbkp|+v;LoK#iFk42d*MUii-&oRJm_hgMI7Ij!|4F79K)8we%~Y;)z64e zS$jZBbNXza<>?Hnzd=__%v}Z)E?tM3@C=^0c3OGpH?ILc;6K7CJHRW^0o;XM&? zRyJSjn0{#e%)dIN5KGml)+6Tt5Rk%+b&h7b*=OocxlFgC6=_Yeu5~|Rx0`VjhDk+} z<1I9`MFiDJFW4|F^V5yTKG8Gp1{v8H^iL1$d}T)KJxxi)uAvV7%^lcAWo61_;M?f+ zt*ei7zH!X4`WH_gd3aFWxuF$D(d1WGLYmrxhA3;SE)ls3ScyeKnCu_!>V(aj4|d;{ zr3d@%!lvC;Q^la)q%*jr_6ZQMqc}5=!j^g{!Y;_gLZ_z1mP1(2ofH+aMc@mO-w%0& zMcrLi=K@|Aj0dKfdi1zjUc8csnps7~J^oOr(crZ%-P>rt(vk^@obDhK%gz+COLyaF zOK@m(fV>GSpm|uvel^6QZJ`+Zq9q=64v>|~qAQ-QRn9AVlh7dTet}Jl$Bf8BlOeSX zRdEVg+lIQiT7;oB750LzS@a{VP{TS=prLli-EQdbR#XfrQuPc7PpO_wgy!O)Ji!_h z%o-Ied!{_J3E>-Q7Wy8R*O)${Vc7n6e#~E8k>#6Nd>OC{o&rDr7D4^1=l-n=Dj7Kg zfy@8pf`-Nj|AlQA|Fmq?fptIXim(x#Q$hn5A3z;;ub{UAm40w!;0p*xQPt~m6u1*4 zG~fRH;R!m96b>aS7IJE9-?nR4o6#^XzbT`CX){A=WdX)s+j*4Jw{yysmET<5g zhm~p#fBsf^D;F0ldkaO!zc%K=&KAJy z2(D)T$~~m&D=r$MjeX8>bk+VgEg0531O;L47sQCx5<0@n!Uiwkdzo^@5myP^w&}xH>73_@ODfWks~GrQLlMjj(6T=VkhF~X=S9fNiHaa$-%?#Z1=j=+S= zuh=Bar9-re^IBgu-N?L&pE2gF)wsS4Hk}wSgKhO1FhZhMJ$QNnak zc_Wg5E#j$$od&Rmk2X^SPW82|hAD%CQdfv%199y+R!Md+Y%xnNa!ceFR9YkOTTG2X z@degv0a@FP( zQGp(nd6$`yUEyu9VQY|1p^_;z5irnE5((Xij0zXIU3O6hr|mv*nf6@YKau^_`vx?U zVzk*ma1d%XK^Zsn6?b(_#C5Y>sgU1np+JAL$q#%lcx_5fq7N~y8$%Y1b@+qlZD)GRtqHiH64d1`M|6%gSI z7E)Ka;0tb#V2V7kP2N5ve8?RHqQI+D^S;>(^p{w&^T-`9T8M^17^E zj64Ug&h1ngxbO5^%8Q*oM^ZU3ix>(+wxqIv#20;@gRteOC|}HiWCLR4chOZ?sIl#j z?HWCs7ES&pYvD@XBAlD2DNS!N?o{H^RV<{m-)}D?NnIgZpCH&_k7h&2!m5!?4~$ha zLL0|~NL2^L;1mhwQu-$|4NgN=T`D#77(jGn_Ram-(H2Uz$; zf+hAb__g8npk=#_HZo1EbdbJvfPcy%j6v0c(TuA~CFWa#IpQ8DxrpD2g$oi(I2o2Z z24*~d>3T%gvGu;W0(7PE2QwGulFsU`yBy^a*R}SEcuz4PGa`L2Shn)X|0CKj$vi!l zaCDGyggSmFjrM}3;YC5#vSN>etg=m3CX&S4Axc2$Ts^+a@NfA#fKQutd*pd^(A_V@omWc_Wn z2hQwncEE}pKwi7qKc@PBPVuRUGcsVzXrYR)ti`QuI(D>YgTN!EudAs+5kX8H4W)0c zIAw{MVl1p@Hk~vb*I#_7n5AXW>4UVl4)eC&0I0WrZeAgG;bu@^)>w=-#R1~M{oE%( z<@`afh5m|!m6*!N-#^rxklo|Mz(ZxZ&B4|4VcoMwNXsBy(X2|3rvfBIt2!o5jEQrv zLw1MLY3@bD$B^%WBD~XC;wrIl$3tP7Ga~QLxD64h(~D$xN9m+3Eh~TMA+@A?zLmjI z$OvS($*mc z>-7O^ek3#vj<28l;F`DCy?7}nY;gV&6-Qpp;dX?e@leTJz3`e<%0*?O&k9$~VgWeC z_Ui4vn7u*k%x~Zav^W@jZEk{?&K;VrjDojuT6A9(_?togSE~qOT7HfJd3E8yiZcJJ z8A#S1STN?F)6hQ^$ln%WfR>FX+7Y_n57T6A3b3$HkU)*{tOQdR#4pkFEyP77VM4fa zF)bTL9&(VJtectZ;O8SUx)%V0c@7QlMyQSNfifr}Jxc}+MGq@Qil2{OuYA6*JNdQz z7Uu5F*?@*f!MBs_yWFd-K9{%I%aPAK|1Uzk+o_EZ9(4ue#Kov4D00}uS~1eMw_XOe z26zT~Ws1^Rh$bR~$k?m96>tz9%=e*8eOiHxdsA|*?Q;7+1~xE5egC=U=gHTn_#;&3_e5qQ+jz( z#pK^U8DYooTFAZK!MuY$$v%@;d#Mf91Ko0^ni3nW;{Y4nNn%=+D(z|A1>5cFT8s;)$qzErjML0 ziD7u7Hr$LASvu{+u9@x_)!~Z@iA6lGvb93@ox@E}w&Xc2)i=D=sh0f+Cvrt#$my5u zNC303wf!W;06T1)$Lm{&d0Y$R)1|S~WyRi7i~gVEJ_xzqMJD)m*o@XwEOICXt`la4cZ3VE78XZw0i9+>*DdZq@D`>yv7e({AvkT zkND$hT?3sR$7&DkeK`u(N14p@CQx#T*#3>0o^v-hT^IV<8ki~k{hDQ=f{o2MNPL zvoYAK@+7+xM*b3hZU-Nmf#%Wt(5PKm=5e#$TEJg!(OX`=TvDG=Tg2WG`EU|Ac*5tY z85?if*_GzFqJ~gBzz)m>lvTx(1B$UZ+(cZKO6+2Bo%rjvjn=Jgk(cRF6ll4EcW62w zIB7jGL}6x)r3O>_+lm-=Y`752QuDc8j|%+N(1)967Rg$7UWvkJG6uMzn_*^66b4*8 zB?j+c4Em#C{Kf`OH?n0qAeXHrx{4J}+xkpj826q~{uJ!Sp9c%>iNsxf+$vwQbbriw ziVukQ&@}iFkJP0kM*QY@SOY8Ws@i3L4^3Z%;3!$fj>B0^ZX+PgA6_;m`3_bu<*7QL zOZRT~u0FT}zGR$QwTrTi-0=wZXdM_w-WG>fwhZAoGj%2mDnDgKbYF(a=o{Fz-^*gj zwzOeIUv7)FSh489crAf{uB+vCZ;S5vy$Yt+fsU^*oAk1xygJ<=eG5BmUWczQfVVcx zAQy^X0uUL(p6C^S+L#7s!HM}|hC1}4ynle4i}drxpbCt(MN7^jC+l&R!+M=xb|n=X z1jf^Ouk_Xc9|v~A>R0)F8)zKkpO&Loh-m(PwZ1qf%wJnQY>+H*#vE8NEs3vT?}hFr z6cxV&Qqi{>kYkYUEsvNiVlfhZ=*&hcj<2^wA+xtF?0iN2RGh~5Z(jDwqHH?_EQL)! z63nv=^p9CAjFTguG~%8f$>GQYv4*SxiY!~i*;ix1?P+pn6s3MH0|SnU=3ORVK8nz} z6$#yIU7NL4`_Y{Bl02XZ7RIqTH#BItO&v$-W^XBo`_< zp;G;l+!qwLoy9y$h^PitL!U|q2HzHJ_k67`3tq0i2gx>cHzkFm$2W&qVDh|>T@Z*- z8wHeE9-zq-8AF!-x~s$f*t5rM;F5bByGh54r^&yPhggy z!rZr6i;^ia)kRBidKTcwqxnG7*JoIDr!?Y{$1{S7R)NY#4k^RKS6X2CER#1qPHoZS zNgXYiv-gACuEa9{Pg()P?0j5$$xQpyySA%fRpa^(9>=Q==fjIFVbM=F9Ky$dxln}? z2R}0&P)+o>emVfEceeQrvWBjB|8kIdz0E6bcDb_4*@yp&u{C2sa6yvG8ece%%-E~c z5L*$Q9ZqZ_1);e}P?>NK{hvNJ3_EQYjuP~ir#tzGx`U;+Pco%E#6dSS$Ou?1QiHOZ zUa3ZZ^!DggCSrpzryEF$k!(+`p3vldJ3W;2>pah|pU77#bbl_nd!o1ebDZ5Xnu^e# z3{mYzgp)o9Aof@d!ajp(M#d8Fg8N;6Vm)hbK`KL6Nzy|#$~TcA7`HT5cJip{bAUOS z3uh4Cv|Qf&V$rVLMOtpZF3?gkg4q`irJfIlQFRR0G=hsYT>AYrtbC72;EY_GyKN7v zE;J^7@d=gq5AHdZnJ=_`IU~)Gmf}u*;HMRD*qF%e-@$u-DFi$ljK&$DX4?er(mDV4 zdz63QousPUDK09Z`Pr}jROZ2QP`!o_gTr+&3m}3+&N0ToWXdGIF~Odp`=ztsKAgXY zxEKAcU&{FTJf0+Plf$J!W>3_6j{k&vuJfs<#lOz)15&9!E{5&c^!`>85g2G2M{1-p zfu2G!kkLv^+Z|^tZ7WxZwT2>`wwXK5$c-7hA-dNxaC#qapj1lhuOQWy<6hy>U@zLp{i>v0goz%WXZfJyM zAMcRmS{A?{94u@#r(Sga6JB##GIpf(C(KEmYBHlqV4p)T8=vpJ8yfL-S}_3RLQTi2 zE+I!C{5lx?OYr^WzKnY)aZ)NsfDs>fz7UP_>3i;YQcK-*4zbgh8(3b+Tgom5;)_}L zij@)AlIK2edojLXpN*)MXmCtss`*^-f%q;wrf}uXd#L!28(5NJmVOj@>Amj zvdBz39zgT8E8&DlkCft^UXevw9xGLOq9z_{a;nr#DeIUmB*`SPGJ;LYufmmDBd6c~Z?xdA z5prm}Ot}XfA@)EW{a1m>zv?{xD_ZbBdv@yfHvc~=x>tQl1-Osr=bs=mViAHux(SV- znm~fuDBFW_@`bagNmm$R#(hd&br zS%lna?|A!i^C_p#_j2a&ePj@OM&C;GzNo1w2szUebw_|!!>W~Bq=b(^OLr_1;37?%(##A z9QqVTl#IL`v(s%~0|Vz+8R>R@70%rCf(8>+;Bolb=5|toH%qQnyJD0H;lj36f&FF- zv%vwW^W=7uE3+{tR{!;xAX|f%`?f<<3qQ4-K?b!^8McJZm&K`-oG9J-tIVR0N)v9> z{aBjsKPjhsqU_1k?ujZzgwvyp;3OIg_9-xmJ4TqE<`xH-meDprmKKT9>?BQJ_c$=4 zjMxCytYKO3UqmSxF|O>r8NQupgg$=6j<$YTZlq-vBOF9{)e1{MgD+H9X&HZ7BELnJ zD)MD({Ai*5$spJF&E#uBOCx_s%Q?Z|#xuboK2JgdNp_GN>mOv6H}Ftj3C_15fk*W6 zQ@LssLl6rPe{u%XKQemMFSN>X5k(eG3>`eO2By+`tF7K7B!hjx!dnk)yJlSR10b2O z2~BPBdu&x5k6P<_Aq3zO_HpDFn zm7Q;ii%GQB6o=RAyOL1UHO{0M8NTY_mJt1l&frMH7X;blR$2Z^D5yG9sg6FBDs+M+ z0hVhb^~MveK6(`s!kkYZt#CVp7HNWEt@Um)yU(WX70HKUY-{esU-SNNJ5ZAE6FNyi z|0@&zKZxo7HhTWK>-?ABtD)<%sDbn+1#7BN90hK8kANt^1a%7oG^Iods$EDbphQ}< zK)g|1QY}$W`*`84_XD=)zV@gTu|;*TWZLz0Sk&T`@>O)hPg28ly-Bt#IdV2{IS=6A z@q_=C(EsxlHz57S4v&|K+=M5NL(a{Rcl)#-&OG$K%yXLD5$q0nYncAVQ+9L{dMk{^ zL|8%~ZuYD)D1nW*m$anFlWw$N%u$kRCw2g-iri@h4N+D?dej@mwEFNgO*?I#-A}T& z`j{rp{;-VALQ7;U#ehw{+}H-?apebor9J#I-EkS7E@$)*rI(2Eg|V45YwoYF?N6q-{yTyLb+>FoKRhs zx~U5_mvk~*TTmNK(Va!L7;yCIocCK5tt};4p-zA$3c$EM%1K#z7s{cmSPeB?LNvCOf8`?3{m|5el48Wx=_l*sG13tpH0Nx;9;ROU zRxz`t)G=g})nwWgNEf6ix%fGhE;~$JZG6&t*Hz%HIDVFJUA0SOyU>EMSEOTLiUz^k zC@Y~I7~Bi<7$GTPNdt4apBM86LtrR3@b)Yu;$fm_>Qk{x>NAb7q8I<$tc`cMXcOkq z=tq#^b!8Bk$SYia^abWU^EVrj9YaFKR$Z6{EW^DM8xMT9Z^mi^n$J1|oFwi$(KPDe zKF)h_X&!ni(>43<-=?*Aya_Y&y1&Qq!+e84G4ArPYMgiLMbtB&Xh_S)x%C$5o~uA! z)ISR^g^3JbT~!XiS`I2O;jyKK!dI6ipD7tIT(q*{w^tTrjSd>98OR8^`1SL%DUMr1 zoty*%29FrQC84%B%?K&EpagbmC9S3#$NlcEJ9y`nDk;d!u(-pfxKAEwX6NZHKgaP1 zYB$t_?F>eqRsQr2>Uw z_(OydVzS-~dc-l>{X`EmXAFX|Rdv9?J-mu_z(Aqxv^0Ze@0{dC$IX3^)}7NO##x~+ z9M3C6>Mb5#EE{I2d$azj^w@8$olxgF)9&oV`R*{O@bEZuYX)Ni|2j$bO%CT)Xd-hQ zwM1mrelZiLpY+Xh)RzFFoN=AYS10)wSREU_e&dln{ z-QKeQ4Br0Rtp2Za%>Rd_n5v@xSMZj?<>`xC}e-2KbVN?1otV0?Gf8uQuiI;twFnF0IOGq z?peO7GocyicU|yBF~GmL;iO|tCQBMo$&+-Fe;;HxPY*S*AkpOSf(S8XHh=UVc##ea zUQaRg{R~7zJCOi?eunC3;h-z&h)|?vFybC5n!%)VF{ASnIgJ@v|1lCxIw-{#tI?R2 zR$KlKZ;d!&&ucn3VFOuYA0z&9T-#_62%0Il%L~~x-znb z^P#1s5Ls!ytkHobY|s>fX`IhDv$zgD*P2LuysS8~D;>;?tiXW96Yq(SMdt#r2AZN7nB( zY5D1c_=t}FcIrtKLhQ>N&i0f&^^xW4qbG2fc#aFXFkfGhFLpNdT4{4F9?z|eK1<@! zYJFJPZP6h}oM)-VgkP@H$qGr1{U!-8lV*r59HgUqeo))HmDcBxVN^SQ=c^=M!;7bF-Vp_D#LR%hU=jFqOXEPi{` zviQDBaVvs_Og+?TFK!#hKwRuun0>tT>GTS9P6N9v|F;E+*IB6uxeN$-&$(;!s^}B; z-_SSmBHt%-G-WN+WHD_Vnn#XuC_+S%<)Mjv>q8!SuJBCStZuSZ+@D>+QWF3)fS95C z+4FTz3MpP=#?w>~0EN%lq3aHC!_fBisQ)?c_lB#r=EUDTW&A4A0 zp*joPiR%T|ptP>8Q(b|7+UP1$b@(sFIc)BKX0JdjS9dPjmnRYt;BuzfPeLlK zOxIUiI;BB2mqZ4H`HIu3HYo0!^@?RLpD@l=q5OG-o-U6*{X?odL|e`4%dJ+x3l>+0 zYqVRBTTQwwuj445KL)KJ!f!aB^(lXK=xFbT78!!PWeYf7)Al$ZQgMZVpOIi{)`?jQ6EGt zN1Fli^1-fQ_AW6%$y~nM{){i_1&A>$M_X2zsV>$$W{(fgty9e0&XaK%Wx9|P?(RQ@ zeG?yL81E?C<W zZN5#>k7@jMrYLPHOIeH1CpOsju9{rH0jI4h`qTq_mOfmrj9}zlOFZ7zYZvFJnE758=N6laV5R<(K#1Kyo z1+WD$nO^oJbwf~l;1+i3LhT5J7^fJYLms*@D>Q~0??Wbi*eH?7ovb#<531*sBqUvH z+U9r0YMiyeOG4U{^oDtp!AW)(StJi2q)@BV3s*IOD-`=*=AY#uTmJ(1^>p@7EIoXFwrc%;%KzWnF5|D26z! z{AaY}HS?db4Dx-hI3$OpXH?G=cY?vO+%f#1#0cmsw{|TTqcs z$L7$Vd%UAhzcx=P+Mg68NA>=MlLqmJuZxP@X2f28{~GD@+LyiN#*x2$(bHArR(-uT znfv3!VgHYf0N^cm@>CR$o9t9P4L#kW7TQA!Pz27Z)<^kRut0`|$oqMS&?>DUdp73?Z9UCZntcGFK-dt^CpAZwmX=VV5T+Ypb^d`CxT@_i6szTlgx ztHgj-1grdsMplBJC`(f}U?U7w`@!%?6;+hmt2Bm_otM`4-fLydBDZ8CKnE9@vHAfX zUoP+WRBN7IyU=;_AFV#%$PL^L-qDLfLgOq&dAd2pPISue{D)>YPcvn&qPdp07-1eU zzJDfttKVorH42n3Q|=R@#KfayWiZSYWe}uptFi1wI=ahv%D{2W04pkz=4cbEtRpWX zD8LmDRE(7XP!T*dRX`z0B$_?w?IiTG$iAuQgQD*ULx_(FGl2j^*?Pb)?RU*2QuMbo zEq&RT8!jCtp>^bPXv!Co^65#Q-Q9T?rJPHk$4=06@MVVAqn~Rm-r(mRmHh48Umucd zs|mYU8p8A|L;auv@pA^4^Y&>0!1Cqe;Qp%&JNaQCa%Cgj=*fBm6^-mmiT`Q zOy(xZDh>*vh0Z~Mi}?sD4HcdDgX5sO9gr%=&=!$lJ&E$BG24a1fkA)DXi_k|fB8do zfL6u4CU!t~`74Ke=ia@{;fk>ynq<)>f_A2MBjx5jg4-*-&yS3@lJS?O*9Tl&(@{Hdun>V2VjoU!p4XJ!u z`sV`b;DAv378}(tQWIx4Ijx6h3rnBHRgtieSnJw{eu?Qv?bCJqTCvm2)7kh_@>RL# zE%Fr9705W0o4C+8Jeu%tkrhY1f)6VZJX9p%e1RJw#{M$Pv5(N0_;s~wQLeYYb@ned&te6Ox{l{(K2M7ESVja1Hb3MN5H12SzFVU&LuBa|JH>666&HxE@r?=J7)GS zR<2g=X8&^*sZ{l!fml`_x?SVMwrA~;s5Hjz(pO`mSQ%pxGHa2=r!SB>=IeIu>A=c# z{=5HQXq0iHFD2-WqV8lzQdX zpKGm1w&DoY#gCFXaYu!X#7~p8CZu^?wQ)Uhs+>J)#PBJe#i}`uWi7Ph0;s#YAz5Jw zw~`e9sp-JY!2B>YhrZ0WjIK*AfMrTq0Qy6cjwymsTqkw_Pg9>xqdU!Lpb?z0#YoJ^ zmSnyN*RguGR$M-9oW0O`yzbsk*yHGP8Q-bGzsI|JiQKmLCN~M z8*#-Cx#tXmK@Ref1SrpIQOnx39dW4^ZlAs~Z@hb&J9NHS#1U;BPiUoAwAd!c9Mj2$ z24#}W2~M5TEN!HZrU{wJ)beG8>6LyKM^9yK@zbEC3o|AQ@u=;&qX>f8xF-JY%P^=s zs8pS7oUnskDO7)cj-gy6M#OT*+zct6a5@B{(0$cU44XEFrn39Q^6T6;+xR{Rn>kr9 zQrP5C&;*oe71IpJJo7gZJ)_U>PCxolSD^3)lF2{qW?^i^sZ!ZVK`FVcQ-G%3vW?@F zb7r)Kt4A4b%}sUAO|?dOLlj*$<3+4c_y7@Goq)wK>Kl%#zS!GZDT>Lnd5SL?sxSJ* zk1i@+wA z`hcof6#rthes>nC!?`F;*Xq!oamK}gk;Q=c^O7PB8pMJK`+Q;+Rf-2^gboUJk(7(| z9ekdg0;2FXcZ%jhp(Iz=Q?;l}MNBG0p|tEo-?GGWiQnSn=wexO!QI+@!OdKAul+J5 z<^6L+ip!0SLq7M4)|vT()00}~*wCtQ|btkyWthyh~dUKeakz#nBpKn!2FunJ_|0?lFez^B?l?~^x~Im2#$gf9FHTua z1}8l|>iSq5U>Ui}f#UQ);$8!wiJM-YCKP)2#6*@>h$>*IGFdW_8OlqBK@ED7?wf@mzih}MD&(oPbMp8oa&M-Vn;!CTRO(PmSZvNd#Vsw&m>#UVlWeC z^B%U}?{rm;HZ6pDMJJ=pif6JxrhB0~MqAI_t`;X!eY~#$r=As2XuY>Exy0Cr?AUUQvr1tQBLDCBVIjO5f1?rZ~# zk(mUxN>!87(fn2tE8~r-6^nDKvi7O& zTN<-k_2v?lG+Pr4odH%FecI+yo}bR-h7pR3=LZiKW-1BS{9S6Fm-WaCRRj>rU)k8u{Jt9)P_v57J2?b z@}gr5rVKk=Ep8KcoyK^rFth^g(-DA41`fi|Nl!Mow2BglypUaG%16C zd-UKWwM_DMf(5=s?}UXyn72%-pv{0e;WbPrq6J9Curr6|pid9sc2b@~nGZ!(_gW}R zd>4#2(+JK4?j)oUQiDsG4IDG%v5xOp7}h_6`JjAN-GmoJ-4NfDjb@t4%hh%3kM$sOK}rVT+G%cLU3MeygHY~yq>H5 zXF*6%U(^`%5(K2pjha}Yh;&dL)d&@mR?T3%_i`4C09IJ%CJ_~ESs{CN3lFp<cEHYvvZxsME}pi^r~`wE zR(Zgs-l?`OOui2RwdVOqNP`MB5%Y(uCqdyuh6XYj&SY`ji&KT8yGk_s0Q+i;aM?5- zdy2{P*c_p3bO^!G;}kI3o#7$-plZ7pE(%o1`*$eB4({rt=cR}Juz3?$kt1+a8 z;q2}fG$OYb{8u2zQ0y)_IOhEnw(C5*RB+CwEeoqwZ4=qSdrSrEIj{YN4rBUoUm1NO zT&9H=c$!s`QXI^CiGQG>?ity42j7-hG3nCYnYDF*aF4$Nl0N*J-rsr?EW|$y)?eTQ z2a_^9HEZiWraH$4_S?5}E;s8VTaYVVQ1ERD?Yf^Vzlix;@9=<_kjoh4!-VxF7(uQK zLIv(V^FP@Z0kLFbm}Hg-?lE-@eHS*8U?e%r$|a%#0Z_k6BX9S^=%5-5q} zh~z!E>VCuTe}W~#+u@A;g;>DwQ@6*!D#Iinq(E1cnMcoR1$4ay6ygxOKhZ`71sEw> zJGoa|#@cGF!myuz3IL(n2d_ac)Ull+s~^G3uRU|o7<8(8p)66!W)zR&>`*4XQ~t9e zj%HD$_=pu3GpiS_FA5d=Zqhlee^l6$tTkf<{yurrMT0T<#@W>k^xkDdjEaprF($T6A#m{3NEFeK?V9UJASIzNF-3;$ZW2DJ1C4 z+60`Xih-PF4DJWLECu}lbSQ&f05tU2g!ZBzDX~SZQWz#fXiB^3r+P9xv;FrroTv=! zni^qGP0eLX5hx{6EmPGNBl^OfAvTVBS!e)CxDIej#izrN?OhdSUs4TwE}r8B55D6> zMRdgCkm#~y!4AsJI09fVghHl;r!B0#0|cnSpHf#TRU3(KQ9_m;c|^YAxJFPg6do+d zcV~ChQN{yZX~k1)4WmyRmPYW3LupYAiXhiQ93_Y~8QAfM5UJu^lIgNpU%JWgHN7ls zmq36DlRpz@a(1!d-W}9$xJmzN(}{k~nv}n`>bdFY2191lQLW$AV2&x8P!Ei+Liqi$XVbQ7&w{*$& zBHO=doIpiDJSm~dY3K#HiD;6*m2T)nhf=X>PTeJhI;iIu&I7GXoptfm;HrW%yy~^2(-j6zk z@fCK+fx#(HG}>f7O`gwf~?U2yt7x2NojM1imx}>oPJI*zX!^ugOE9eJm@Nz$D(bQ5 z9agonHaTb_)4q&ACr{}2`YDuuMA#_TpUF$Q1-FNdsn__Yh78DTE8KH7(ym_t#UbWjpCo-UXKEbpHc=OFO?@3(pH!ps znXe3cF}&h+q6u|mp8X#GIec3BaUoO)dI=O-DSMp6xE$Rd;av z>pJ!+$cC^ag+|Z`Xl2P87>7($#y&tSGI4A3E=kCo1kz*@ld*Zmo40nuLs63hgt!+< zVP&d&^)!*nR$fDWM&@16<>xA3~$dOR_D`4x?e5|#72UnM4tjLE?IvvDb>|Jd#9OqP* zw6YtaPywLJwr9UwZ?y@R(Rb#;RlZfC=aw07;)8ivdEwqd-83jsbjXO|+k`(AOkI%$ z`bnubTn#iAx58rKeIF*#Eo^Hs z2p9*oIW;U{LhUdprOLtN9Z-OjpM<XPqNMAh;5WRA{JA@-VUBE2Asuc$Qh;|2))eC{&v8byr*cob)JHUV#1(swddDYOX=T{0x@Ug9EETtB>jv5?5pBU- zAjHz08TgDn1JYD+_u!mt4_{-Vax!}|+rM=tIOFS+88_5+ z^BXQVNIs;5GoH#GCaDX2XJ({vcktV_nT~cbD*}l`xvf_UM0`+bSCmZR3Vc~HW$Znz zKKC$gOupRqOr$s!35_HL79h|Tt4(;)_|jm{=pnSAGSoNW^=%o{7I!-IiDJK!r$IF5 zGzPts^}}ne$!=@OSr@HcP(GsmjNV8jERE?3m~{agTr3{!bi&#myZuVobHV`XSrbx} z(*=o!s~OV~+v~^ZOQ>PDIdx|Q#>53NLqVK^RF?wY{9aTOfuYowXr}uE-YUnqGujt6 z7+YO;F$pqnpiDx?XVhCvlSL)L$+axX%5Ju7mlU1OIeo$M>-YJbWbf?JT8k?ug9p43 zmOn_j4iUPF;GD|d)>)#=(tH9-{jB-5rlzPRX%xa^22>@9?Fqzz+g?jh7<${~xLtB? z)@bnFv$wXYROVA4-KdwG)U5$RE$nG&1{o+zHlcU7|8r3vOV&e$uM3&`RRUB%UY;45}9WNEqN@ph8b!( zQ8Oi5($^`zUBinEFBIcIO{SV6`D#$`G>|2ajnV2}f{!g|xiq#?%R{=x@pO*sxa?B| ztR)sIlDLqA$_P?m!5m7!CJ8rxlw6&LhC?&O6Hh%BPL)nvLMoFZKEH=}a%mqheg~bj zLK46)Jm&G7QoXPqBy?rX!!2!R%=t#^mT-3bsxfkTP5b=WinPF{>TdrR?ymvzeln=b zh`IWl)VgA`Aj#y0_9S;qZg4GZlIc)JNUaPvQG^(xui-MI;A$iJ$g0Nr_Wc17S#S^YWjl3PusxQ!)wU8b8 zFDF#aeJM!o$?`DADxMHNAZEJ~37%z9K|H`EELfXxd1kk~1D^+fVfB^vE8gX{gus(q zP8#n>$2_-_?mAGc;a!1_r%;Q5A2Rl`D|Ws8XM%2#K&mA6>S3ZSgN+PlDTfZgC=(ls zm&A@kk;cmfW89r0B}hsr6~eFYifW50>0>}L`!=SQWrUPCV>cIK&lak8qFzeUO^%DK zb;G1evX6LifZX+YX)KcE8#6f0K%rmfZCvGrDbX}1=o|~8K3Rr?$7h&k1ziysH@RgY z{wk6x@9k^JpF6y3O+|Vy=g#O%A7KZ_!Z*svG$;09pWmGH?5PE+@IJ+K63A3G zRxQj3C%h%n3+a83X?IpT9C|j9f%VX-U^n`S?1AX(xE>Rd2=n1Z;Z)gMjS=KX0e`3S z7wBro{K8hVEJ`ZaJaVVTROdCtB#>bNW}5@N=l7*#o*|`}5%^--4HcpKSh-7)JenNy zz(_n1cZ_*HlPkY|<1wAGFAe^ejgC#2M~>K80Zsz*A97m>&%{gwf-fO!IGXHtLFPaB z-&53Z_*)T-ofB9e3q0E0{0fPG;tkNTN)22HXZaVdDl#DeP*32mFbMm<{8nWN|B0FI zf2hYh*oDNS3i$x%CkPjxlN-XM-~l}-islg7!sKjDFkQ~(EOz?zTHAvpR5~}5r~}D} zx4z^}Rg52#tlI~!tHl+ron`xltoF9AATRpDATcI!tCII9rBskRRh8cTef438rEkUHMhEA+zg*XY08C@c<&hLhWA^8_Fv^SZM)W~Il7h@#hDRC z;D_T-kWj22P#@^WwO4$^dx9mjFu=&H?b^FyH@T(Ly$Bt!!KMOW$9bv6YG|h&2M^YU zCGxhRi*YJ(LBW(c8<*WZ+Pz2mS#CJ})k@Uo4>!wACtr&wu2dnN-KP`r83?6%l_42R z3D%P12Dd6P;xiy_Xjq=(8^QS3tyzaReeH-TW18P$VF-W!G`Ph>d-x4eY8ZLYmgp_Z zN$pPinOpkuoSq_cpCbmxXSF`rphklW;_gG+x-7lZ>m?x$PFGc&f+o51$}<}B8zzt4 z>4S$Hz4fx|ian>^e7yJc2lsNsE(y&Gmn1~KG}7n2?}h6gDi5h+Z?gyZpALhVB1tKl zyx+4x3bXPMGD}i|@INOM4O5vJ>)#(s4g~!uzHm&n4vs91I=ssj8Ux)V`sV!QOCp|9 z_)YS~Fs67!5t8AeXr`cQlns=!>|H7kiQC2;Z*ghB+|?dPB@U>Ja>Z)GbHAgb_$sMgr~G)JhY{!TEY52na@|#S?S|HmaH06E?59!Gbui(%>6w`R-#h5uMX! z0J{rT_9=QD=D~G4vDNy`P7OnhnumO|Y1EcXWM(=djE1uos--9OP5}>zC!E4gpZ6C( zuD8)|P^CaSANdHayg=YFqVm{k>Z;)4g$6&;Fwb16N#(cZ>?-D|Q$Ew6KV~-!=U7Av zc*Pk>`6Q(P`qiA!!dlj>Yxr#hrp(uX0^y1cbC&^-pjoU5SN^QxRI$TJKUQT^OdMFO zPA2$MH*IjCoTeJVPa3DO`**Oi)^2xR+ATF(WBu+l?`1+>>tS=-VaII8yrzTK*C{e_ zDK)^Mg-2V;&pKI<6S?Nj)K%_Bc+ONA_WB@s;!}K%9rZqZA28~b$32&j`F*+oi`%dm zm(`mzf;~jxBz~Y%;XJ4j-}z{o22D(mZ_g%+g5vo1aLV+J7s4Zz$Rv2aRq=+G7Y??8rDt!e1iy& z)&NN*U#B+|7pcEFX(?*S{}x+~sr_k;458jCT!EMH0>8L)kbk^!4L-?NjJOB(piv7C zo;6lt^LKi^A}3RkE{r$mxtW+{b_}M3LMM<>S)i0Wx*}mC5~~QY5?whdTa5-ih)t`h zerXv`DOtuC2}T6FBT{|Ot#W)CV!A9B_w>Zqn^H`TlVwXLnBLQ9_T)9iVlN%@X^G)- zmP+cbr6;F!2gQm)O=+EcU{cTlHh>V(2mh1uE%#RkaF$v!s##wN?hzfce2EP! z^VPf7wJtvzpICd}rF&j)RJ`(rvVjng(NWe)8b0JPO|bK*)vOO2Y;VeV19|}&w>9@ zA2~5HcZe}|+`+L`Ww2!1ll&Eh6tMw%{O3e{Gmm9d*vm`+lhy}p0JRQtg1&kr){q8o zLcN6|^;}wkg0ifpVwusKmkQ^k9L*NHP-IFY;N5Ccd@9_FZ|75USR#U-rg&}%h9+UO zqJNk#C`giY?8LjC5LY*DcR_PR!90NpCku;h)jY;Y5l+yID$8tEr}DajdRla|C!JZ9jS7ZNR?01x z(29C1wdrL=YOxVlG-&JGxru#`LvRr*x#&9t!iYKezI~KPJOY0uOXC!x^tjzoC!+N3 z{nNF^nX*)eZU>pfhV}$EAxl#9Qv@T9k_3ldr>eURyt9vm3j@@h<(CKp9~)y4yxE9;sUsj8c(7knL%j`1o#`5%Ch&^Sez!sOEPdI&6 zVDw&BqsIW}LMCTJ0HjFlnA&Wa9t9CkDK zXj`8X!ztT=v=f|BhhEyJey-fUg*2Mzmw1dvGsk1nDft>e$HrwSAlXa1HpdRnYj;#G zFAKPvbfbS-by>00KuvT{tAU}ryQZXM^I6aXWk~r!SM*_jo%ySU?%sRWqRO$7btT1h z66E7j5S)>9RjUTgF2?NIVycAJas+~Dw$;R!gXH%!)4&kKZlqnk=?tkW#kscq+yboW z+rDQal~@?2_heHhcafFu&RM;HvEow^*-ICyJ%;E*c@nCl&L(6RdZ}o1F*QZG!QBbI>Sga6MhY zJtASBj*zP)0>ULKMME%=^Q|Ms0&OsoOrGh&Ur|9MWn9}GUE7^opMeEm;Hx)FpK6=$ z_{v~P*=6*BN?ENw4Q@|+L;X1+8)Zi~fzB>%!h`h^bpruB>*Bp-oO;obx^UH&dKbO$ z(q8}M=W`~0+uJFDUkz7WMhiv@aBe0B&dqec8?N7iGXK8YB2rQFKhh#~_4G%i`C8~g zR9HFmLt$7gFG|3fNKAY3ApNaHc+`WwP0I8r-mo7i+OD%hrK3eXflK-y4xi>e$|6?A{B10 zD#AtKv}EPe(^Pt9YGbX4`+_lK8F{KDoVv&%CLAH+g@SXJvA)2b~P z>boypUaQ}6JuuS^2rJSMnz?|-^5S+$xt5PJ^Nq8*`Z&O7bQv`9F3GXQpNe)XQkz^p z^tlEZ8Mr6Sz70+qeI0ZhLc0vns#%y2L@V)bnd_D~!9l`QSKA-FOWT~a)${p8 z+TfUfuJ7Qp31=TU6nIiOcQdZCB3(X$(~<*+*oXDli+H*V(s*JYkt(*HH9Gn}#lFCK`}qFL#aAdF*HX&p9s~sLs?VmvZ?e*GDVXv}phS9WATfZe zCv0Slh59;TF(m5tX|l&tGKmJv5lLF(RIK0?3xFJeW?;XT3&8UX36MatEl}Tbs72&} zRjy4%<~CwS_wcN{yU50+!K1t@+oH+QjGY{erwlNSF7Gm3Fz{lq%(l5Jko+t0+W{vW z<|v)p!~=_#ZPFLCcZ-EBZAY91b2W`SDFK>@N6ZUZq4(xZgDWbsp98!@^srNCj!sou zbnOcjsP4M#a7!8s;T4|YR;^`{MfNy4Y3+m%yOw^u`?}l3!@pdh;-r}iuu}i*!pyg; zUX=Ybu;z8O+89#^3%8YlQg7~Sa=H?=@poZtL4hx}B8}Uq>*&^Qwp7?8S>UhWWNLZf zStvJnd5Lh7mye_o=WBZvN25s|7>tY73Bj-_x>b32R&1Sh^7j=AQ_eI-&RY(<@U<61(X_-G^BC@j6ZrN%T3o%&$Ta80FN_$+ds*mg z4Bl+7KLj8820g-KM9N!88(EefeLyXEr}f1E>FQgJV$ad{#7w~3$WkRnHjdjU+s z@8GxI1|5oJe8gu!J%r%-m&`dt~ z8U?WpmRwOb!9-7yLjq=~7tZ;VEK{yu_+COu9zvF1zI#(71z8uuskuKv@8l5fYXv^L zz_!sKI77Te=J{%r7KM8lznuCrZJbCZGE5c3daD@b-nI3whMy8#5*`N_wP*az8S%T} z|67FDqaeLV1zDMHL1a&04E9t-G35tRR#@>0S!ziIbWm8B<@&uQ3n`AOrTBYxqb{{P3i5k_Xu+7pGy6q}2>-lt{55ZSh?$Q8V533IZ8e z)AAPOU+%Rt@$JMZu%|Jx!Q{_3Rv!@LvA30H^aZ1fEvRDXhrTq~?Qo|&hqP@s<1Nj2 z8NbE7CeK`Zi$&fz?gpc^Qmz&-d^DO?5pe7c*EQm_?vHsBL0kP%DNWEs*D;k|7>z#d z=wqqTDLXzMTjeXI#Z>8j6+|1g9`jA;{$BUbP`~!C$T;TqJ}@HE1NcSouVn0mjR4km zM&hP+_6~}U`rrHiudm-;6-z~6G7~SWDjVBs6G?=Gx;aUIK^PBaUs4kAs7XX+*cG0V2~ddK#KcXI~0Ehk(PZ!Zia~Iclre z2g#qn6e9aNJp#Fo^D}-u&h633g_}c=9-Xm9f>Q5G=Ms%#t!YK|Y8A!ErF1KkdgYRG zbsS*^;3fhFrc!yg?pG3=+e_?P0JAiqq10yFZXCTivnlCRM+ti6LDZoXquQo2jizLd z$k^;*WS#Njw8XjsO~>XjDmG7MD!iZ^^^e6G73Sb+XJj}>`yq0;R78T!A(O6{K|+&M zbHzqGL?4?>Z9GO9H(xKQ)tJOpWDG8XT|luZD@RHf>uNSB3_55Ov=ljCQy_Xx7enuH ze;Kc5A>a+&L|lYO-A0mCY=yMqA~cJmS&6XKVsA`_m+*Z8kF+99<614pv$yTe{4}-3 z1b~yqt4#IQ$kj@ev6tR?MtCvcQNwIbUA z!;4kuj~H{_U;^a5I`?#33lH9fZunudyVD4_>d>guC)K*~adU_y9lS)kavh4CuDmeY zPrQ{x{~!WMV~8;VXqc0m9En$TUyy}@--hr%)xkcriO%#D*}tEYO{jn2HgE1wkqY_B zSQsPyWpzO;-I=z_GLKG?N-d)EN80tTXOKp78?&olk*?c&WYc?SNzb!kCwU?u{Bv6- z2avMfUY=jMMFBWWj|+7|d%Xi0Fy#+BA6P~_U9#pU^&_=Kh%|+LwELk9@e0_w4B|by zaTIFF@wz1%=FV?9Ajc$H>yV1Dodg-LD6w-it5zgtvTlzMgKb3#R7iCcy33OlRFoKAEQIE;yRz}PME$62;E1Bs8Wu2 z$3`~C&1~Vn9L^PdZ z33{h&m3EtM%nU{*tO?j|CYgN}V~4?UnTTf_20QLrwjNr&!BZ8{PR4s&9+`9s`~Bpn zS~`O1I=$5UDEK}u&x}b3yWtwd8W=CKr1(8#zjDNWA^O#Z#DVane2c990<_UwzuRa< zS9=E|%YWlj$cP=5?iNH3`Y=~wSz9+_HZ8WuCX6Q96NnX!iS?4<#hzCx;baUM8pWjW zvb3rn98pIwDy1oMkx-9%I?LIIhmrKg7Vnm}Cml~Ll8BKaNiEQG)B{F9Eikghh`on+ zDL%j$&fi80)(!VdX3rZFEd8qsA)NQ<`4s)1i>B33S;BQuw>+VM(+vPt`H6QJyj@l;B#6*A|Sezu|o?d)gbzUWi2?e>*W zToiD2)QPw&zook6cb8t$CH{hz!)qy@4sh5G3|M^kBB#VHCS)$< zfjGZ}yA4_-2}yHFFfu&`Rb<5xvTet~?^JCdr#yO7xo~13pi9kTui2t#cUN%}BDPZJ zBr{xQ?OOPCx=tQ1ml=l~j5=H? zXt+&1;);Q`jM)zp_OP2u13X+cV`M%rN*IE;O%5#ava-;MAJAkg-8%zu8&3FIuOm~E z6RoI_;MDz;z0ue&HD%%4T@T-whr@q!s3-(ow@f_L(#(B<8?X!6F^4BLDc(jlf_kfzXp@Daq@}O$vpcE`Z zOprA1o(s;W8=33^s4ob%XEhnqnBI${#&-0~;~x8B+Ylh>uLe_zym~D$dzkueR^k)qj?i{>RJ4!OO`P$oF!Z(0Na!A$oZ9jk4)$AW$k@ zsFk0+q*4_|yWUfVko^Ac)hMNGpt+1R#KgsN=QE&Yts2Nw4g zf#f>$@4|ta(=M^M#a&}v5NDcrv|*=8I)iaNSrgTEUQ+BzZ49t{i`qeTJ?4r`6v}UO z0d*>2(eM)y1=Qlq3|O$R>XDqc*qn&L>*oL@`Y0(`S2B3nrbH&A?&sF2#pN)P%r)~Z zo*2}!U2Y%KG~!lYKNO2}#)M~Y8P3#=H;;`SWCPw1RYvB-jaxGO+7D@}tU>Qxf zwOXQKeTsepe_;H1Eu%YJy?4zGYfC1A!5`jNW0WZb$8&gqCXS{e`89LelT1Pwuk^T8 zkrE#XR0<|?U5zeyLKX)uBY(a3<1xnbO$FBG{qcgv- zbcA@3bg-F81b;J2{c|>=lsJx?DNfRC#8GMr5&6An$%;~Hb^8a4BFPTW$l|9ttpZjp z=|Vh-qbV9`&UFO}s@oEP`1`(2bmVpw0dGFTr&Zg`ftxB_%F7qr!c9#|=qwx-ptY z#J~DLx`a^pWv$+V%3ss&YhC-^-rQ$>IuTMsj42=)a2ju@hO$jrIO=T1hmDimUr}X0 z!f#mL@j2wu_y|{1Z3I3?JDid2Iqu5?qb0%7*x88J(@3>T1=;{pANA%OQ~SB1$(KCc z-uH+Gq0vkDB-zOVX&Yk5Ybqnd5 z6{OV1e&TJ`i%i*?w5$C|LIWO+5DO4mz`OqH*QZi5c2-jYXynC!ClT=co&^B7)&2h? z13=A-KV$&d`bGEu2`D-kFi$u%GzdO$(>;**zq0p0^YHyZ200S?_ET0&Nr+xbP8_&X z|JPz&pmmGibc>XLC;GSl{C?#5e*0YfZ!uXRIVo{5MWtu5;*Sx&6#!0k|2cru-S-0- zE8h zKm$d8EgbEE8_UE^EsTT=42c7XPc_ z`L2vjD!__^0DI?~$@p>9_}*ds5&gNf@&D|FQM-dM3}B#%6|l|U_C@_TYJ6V&%)x*XiFW>LwkUonE*6Q zzuqTahCiYSTU$GP%e!GCt7mEjbh`e`w()ofbczuVi2(0WE#_Z26ModS##e^*kI>(T zfS8Msf#ZMW(;uS-;O3Q70a1m49Z2&7@;}X=;{PM+Uk}B1>~EF+b4NVRaQg$g#&=Ze zkGS8v^?#Y4$0-hf;t{;~Bi=8!{(mJreB2w4)93wUp?vvAmj7*W{**Q6C!Dv&e`n9{ z2KbLN=-=!2O>gFL(wm=vD4PE}17FHlHU&C$p3zPo5#?#ere@54V%Y>A7_#I zQM|@iW2al;9OU?hJdTaDgRR2SG{xSSx&Get}{Ko$T z|NTzkB1KdE%B{{_`wo%Vlq*JJ(4pCo>E|AOS7)hr*k=&{`2PqGfje&+o?LU+wvS%=vh)_D{~E(EpqB&*tiJQ0-65Stm4}a^s|D!>Voy|XKl52jW`5Wx_2K{yU2iy19>-ZD@r0!qf|8F1U p \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/spring-web-reactive/gradlew.bat b/spring-web-reactive/gradlew.bat new file mode 100644 index 0000000000..aec99730b4 --- /dev/null +++ b/spring-web-reactive/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega From e770b7de419df2d24d93711fe413d9d42e55f8e1 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Sat, 8 Aug 2015 03:46:58 -0400 Subject: [PATCH 009/498] Tests pass For Tomcat, failures occur if HttpHandlerServlet buffer size is significantly less than the Tomcat input buffer size. Also on command line Jetty tests fail with port unavailable unless the server from the Tomcat tests is also destroyed. --- .../reactive/web/servlet/HttpHandlerServlet.java | 2 +- .../AbstractHttpHandlerServletIntegrationTestCase.java | 6 ++---- .../servlet/HttpHandlerServletJettyIntegrationTests.java | 1 + .../servlet/HttpHandlerServletTomcatIntegrationTests.java | 6 ++++-- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/HttpHandlerServlet.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/HttpHandlerServlet.java index af5f10f172..48bfe3886f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/HttpHandlerServlet.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/HttpHandlerServlet.java @@ -34,7 +34,7 @@ import org.springframework.reactive.web.HttpHandler; @WebServlet(asyncSupported = true ) public class HttpHandlerServlet extends HttpServlet { - private static final int BUFFER_SIZE = 4096; + private static final int BUFFER_SIZE = 8192; private HttpHandler handler; diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/servlet/AbstractHttpHandlerServletIntegrationTestCase.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/servlet/AbstractHttpHandlerServletIntegrationTestCase.java index 6088296dd1..8b4494d2d7 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/servlet/AbstractHttpHandlerServletIntegrationTestCase.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/servlet/AbstractHttpHandlerServletIntegrationTestCase.java @@ -21,7 +21,6 @@ import java.util.Random; import org.junit.Test; -import org.springframework.http.HttpMethod; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; import org.springframework.util.SocketUtils; @@ -44,8 +43,7 @@ public abstract class AbstractHttpHandlerServletIntegrationTestCase { RestTemplate restTemplate = new RestTemplate(); byte[] body = randomBytes(); - RequestEntity - request = new RequestEntity(body, HttpMethod.POST, new URI(url())); + RequestEntity request = RequestEntity.post(new URI(url())).body(body); ResponseEntity response = restTemplate.exchange(request, byte[].class); assertArrayEquals(body, response.getBody()); @@ -56,7 +54,7 @@ public abstract class AbstractHttpHandlerServletIntegrationTestCase { RestTemplate restTemplate = new RestTemplate(); String body = randomString(); - RequestEntity request = new RequestEntity(body, HttpMethod.POST, new URI(url())); + RequestEntity request = RequestEntity.post(new URI(url())).body(body); ResponseEntity response = restTemplate.exchange(request, String.class); assertEquals(body, response.getBody()); diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/servlet/HttpHandlerServletJettyIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/servlet/HttpHandlerServletJettyIntegrationTests.java index e52696476e..d9ed1548ea 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/servlet/HttpHandlerServletJettyIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/servlet/HttpHandlerServletJettyIntegrationTests.java @@ -50,6 +50,7 @@ public class HttpHandlerServletJettyIntegrationTests @AfterClass public static void stopServer() throws Exception { jettyServer.stop(); + jettyServer.destroy(); } public static void main(String[] args) throws Exception { diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/servlet/HttpHandlerServletTomcatIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/servlet/HttpHandlerServletTomcatIntegrationTests.java index 36b1ca8e53..1c1bda427e 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/servlet/HttpHandlerServletTomcatIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/servlet/HttpHandlerServletTomcatIntegrationTests.java @@ -33,6 +33,7 @@ public class HttpHandlerServletTomcatIntegrationTests extends AbstractHttpHandle private static Tomcat tomcatServer; + @BeforeClass public static void startServer() throws LifecycleException, InterruptedException { tomcatServer = new Tomcat(); @@ -43,15 +44,16 @@ public class HttpHandlerServletTomcatIntegrationTests extends AbstractHttpHandle HttpHandlerServlet servlet = new HttpHandlerServlet(); servlet.setHandler(new EchoHandler()); - tomcatServer.addServlet(rootCtx, "handlerServlet", servlet); + Tomcat.addServlet(rootCtx, "handlerServlet", servlet); rootCtx.addServletMapping("/rx", "handlerServlet"); tomcatServer.start(); } @AfterClass - public static void stopServer() throws LifecycleException { + public static void stopServer() throws Exception { tomcatServer.stop(); + tomcatServer.destroy(); } public static void main(String[] args) throws Exception { From bb2526752597d1c29fea939b93841a490f5821bf Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 10 Aug 2015 14:18:59 -0400 Subject: [PATCH 010/498] Fix whitespaces in build.gradle --- spring-web-reactive/build.gradle | 48 ++++++++++++++++---------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index 4aa6cab66c..67f9f93d19 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -1,10 +1,10 @@ buildscript { - repositories { - maven { url 'http://repo.springsource.org/plugins-release' } - } - dependencies { - classpath 'org.springframework.build.gradle:propdeps-plugin:0.0.7' - } + repositories { + maven { url 'http://repo.springsource.org/plugins-release' } + } + dependencies { + classpath 'org.springframework.build.gradle:propdeps-plugin:0.0.7' + } } apply plugin: 'java' @@ -14,32 +14,32 @@ apply plugin: 'propdeps-maven' repositories { - mavenCentral() + mavenCentral() } dependencies { - compile "org.springframework:spring-core:4.2.0.RELEASE" - compile "org.reactivestreams:reactive-streams:1.0.0" - compile "org.slf4j:slf4j-api:1.7.6" - compile "ch.qos.logback:logback-classic:1.1.2" + compile "org.springframework:spring-core:4.2.0.RELEASE" + compile "org.reactivestreams:reactive-streams:1.0.0" + compile "org.slf4j:slf4j-api:1.7.6" + compile "ch.qos.logback:logback-classic:1.1.2" - provided "javax.servlet:javax.servlet-api:3.1.0" + provided "javax.servlet:javax.servlet-api:3.1.0" - testCompile "junit:junit:4.12" - testCompile "org.springframework:spring-web:4.2.0.RELEASE" - testCompile "org.springframework:spring-test:4.2.0.RELEASE" + testCompile "junit:junit:4.12" + testCompile "org.springframework:spring-web:4.2.0.RELEASE" + testCompile "org.springframework:spring-test:4.2.0.RELEASE" - testCompile 'org.apache.tomcat:tomcat-util:8.0.24' - testCompile 'org.apache.tomcat.embed:tomcat-embed-core:8.0.24' + testCompile 'org.apache.tomcat:tomcat-util:8.0.24' + testCompile 'org.apache.tomcat.embed:tomcat-embed-core:8.0.24' - testCompile 'org.eclipse.jetty:jetty-server:9.3.2.v20150730' - testCompile 'org.eclipse.jetty:jetty-servlet:9.3.2.v20150730' + testCompile 'org.eclipse.jetty:jetty-server:9.3.2.v20150730' + testCompile 'org.eclipse.jetty:jetty-servlet:9.3.2.v20150730' - testCompile("log4j:log4j:1.2.16") - testCompile("org.mockito:mockito-core:1.10.19") { - exclude group:'org.hamcrest', module:'hamcrest-core' - } - testCompile("org.hamcrest:hamcrest-all:1.3") + testCompile("log4j:log4j:1.2.16") + testCompile("org.mockito:mockito-core:1.10.19") { + exclude group: 'org.hamcrest', module: 'hamcrest-core' + } + testCompile("org.hamcrest:hamcrest-all:1.3") } From 2cb32a0fd69c528d55d3d53ae98bf886dcb4a5f9 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 11 Aug 2015 16:01:13 -0400 Subject: [PATCH 011/498] Add ServerHttpRequest & ServerHttpResponse This commit introduces HTTP request and response abstractions along with Servlet-based implementations similar to the ones in the http package of spring-web but using Reactive Streams. In turn HttpHandler now accepts the request and response types and returns Publisher that reflects the end of handling. The write method on the response also returns Publisher allowing deferred writing. At the moment however the underlying Servlet 3.1 support only supports a single publisher after which the connection is closed. Only simple byte[] is supported for reading and writing. --- spring-web-reactive/build.gradle | 2 +- .../reactive/web/HttpHandler.java | 3 +- .../reactive/web/HttpMessage.java | 30 +++++ .../reactive/web/HttpRequest.java | 32 +++++ .../reactive/web/ServerHttpRequest.java | 28 +++++ .../reactive/web/ServerHttpResponse.java | 31 +++++ .../web/servlet/AsyncContextSynchronizer.java | 5 + .../web/servlet/HttpHandlerServlet.java | 53 +++++++- .../web/servlet/ServletServerHttpRequest.java | 119 ++++++++++++++++++ .../servlet/ServletServerHttpResponse.java | 88 +++++++++++++ ...stractHttpHandlerIntegrationTestCase.java} | 4 +- .../reactive/web/CountingHttpHandler.java | 4 +- .../reactive/web/EchoHandler.java | 4 +- ... => HttpHandlerJettyIntegrationTests.java} | 5 +- ...=> HttpHandlerTomcatIntegrationTests.java} | 3 +- 15 files changed, 394 insertions(+), 17 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/web/HttpMessage.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/web/HttpRequest.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/web/ServerHttpRequest.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/web/ServerHttpResponse.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/ServletServerHttpRequest.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/ServletServerHttpResponse.java rename spring-web-reactive/src/test/java/org/springframework/reactive/web/{servlet/AbstractHttpHandlerServletIntegrationTestCase.java => AbstractHttpHandlerIntegrationTestCase.java} (95%) rename spring-web-reactive/src/test/java/org/springframework/reactive/web/servlet/{HttpHandlerServletJettyIntegrationTests.java => HttpHandlerJettyIntegrationTests.java} (91%) rename spring-web-reactive/src/test/java/org/springframework/reactive/web/servlet/{HttpHandlerServletTomcatIntegrationTests.java => HttpHandlerTomcatIntegrationTests.java} (91%) diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index 67f9f93d19..34c0b96a55 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -19,6 +19,7 @@ repositories { dependencies { compile "org.springframework:spring-core:4.2.0.RELEASE" + compile "org.springframework:spring-web:4.2.0.RELEASE" compile "org.reactivestreams:reactive-streams:1.0.0" compile "org.slf4j:slf4j-api:1.7.6" compile "ch.qos.logback:logback-classic:1.1.2" @@ -26,7 +27,6 @@ dependencies { provided "javax.servlet:javax.servlet-api:3.1.0" testCompile "junit:junit:4.12" - testCompile "org.springframework:spring-web:4.2.0.RELEASE" testCompile "org.springframework:spring-test:4.2.0.RELEASE" testCompile 'org.apache.tomcat:tomcat-util:8.0.24' diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/HttpHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/HttpHandler.java index e1e0aac26a..7065344567 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/HttpHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/HttpHandler.java @@ -20,9 +20,10 @@ import org.reactivestreams.Publisher; /** * @author Arjen Poutsma + * @author Rossen Stoyanchev */ public interface HttpHandler { - Publisher handle(Publisher request); + Publisher handle(ServerHttpRequest request, ServerHttpResponse response); } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/HttpMessage.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/HttpMessage.java new file mode 100644 index 0000000000..6871fcc3b5 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/HttpMessage.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2015 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.reactive.web; + +import java.net.URI; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; + +/** + * @author Rossen Stoyanchev + */ +public interface HttpMessage { + + HttpHeaders getHeaders(); + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/HttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/HttpRequest.java new file mode 100644 index 0000000000..bffe2f4344 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/HttpRequest.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2015 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.reactive.web; + +import java.net.URI; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; + +/** + * @author Rossen Stoyanchev + */ +public interface HttpRequest extends HttpMessage { + + HttpMethod getMethod(); + + URI getURI(); + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/ServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/ServerHttpRequest.java new file mode 100644 index 0000000000..060e2cfd4b --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/ServerHttpRequest.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2015 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.reactive.web; + +import org.reactivestreams.Publisher; + +/** + * + * @author Rossen Stoyanchev + */ +public interface ServerHttpRequest extends HttpRequest { + + Publisher getBody(); + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/ServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/ServerHttpResponse.java new file mode 100644 index 0000000000..7ff7c6af5f --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/ServerHttpResponse.java @@ -0,0 +1,31 @@ +/* + * Copyright 2002-2015 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.reactive.web; + +import org.reactivestreams.Publisher; + +import org.springframework.http.HttpStatus; + +/** + * @author Rossen Stoyanchev + */ +public interface ServerHttpResponse extends HttpMessage { + + void setStatusCode(HttpStatus status); + + Publisher writeWith(Publisher contentPublisher); + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/AsyncContextSynchronizer.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/AsyncContextSynchronizer.java index f158923136..cbb0427e9a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/AsyncContextSynchronizer.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/AsyncContextSynchronizer.java @@ -97,4 +97,9 @@ final class AsyncContextSynchronizer { this.complete.compareAndSet(NONE_COMPLETE, WRITE_COMPLETE); } } + + public void complete() { + readComplete(); + writeComplete(); + } } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/HttpHandlerServlet.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/HttpHandlerServlet.java index 48bfe3886f..31ff0a7f17 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/HttpHandlerServlet.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/HttpHandlerServlet.java @@ -24,41 +24,82 @@ import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.reactivestreams.Publisher; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; import org.springframework.reactive.web.HttpHandler; /** * @author Arjen Poutsma + * @author Rossen Stoyanchev */ @WebServlet(asyncSupported = true ) public class HttpHandlerServlet extends HttpServlet { private static final int BUFFER_SIZE = 8192; + private static Log logger = LogFactory.getLog(HttpHandlerServlet.class); + + private HttpHandler handler; + public void setHandler(HttpHandler handler) { this.handler = handler; } + @Override protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { AsyncContext context = request.startAsync(); - final AsyncContextSynchronizer contextSynchronizer = - new AsyncContextSynchronizer(context); + AsyncContextSynchronizer contextSynchronizer = new AsyncContextSynchronizer(context); RequestBodyPublisher requestPublisher = new RequestBodyPublisher(contextSynchronizer, BUFFER_SIZE); request.getInputStream().setReadListener(requestPublisher); + ServletServerHttpRequest httpRequest = new ServletServerHttpRequest(request, requestPublisher); ResponseBodySubscriber responseSubscriber = new ResponseBodySubscriber(contextSynchronizer); response.getOutputStream().setWriteListener(responseSubscriber); + ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response, responseSubscriber); - Publisher responsePublisher = this.handler.handle(requestPublisher); - - responsePublisher.subscribe(responseSubscriber); + HandlerResultSubscriber resultSubscriber = new HandlerResultSubscriber(contextSynchronizer); + this.handler.handle(httpRequest, httpResponse).subscribe(resultSubscriber); } + + private static class HandlerResultSubscriber implements Subscriber { + + private final AsyncContextSynchronizer synchronizer; + + + public HandlerResultSubscriber(AsyncContextSynchronizer synchronizer) { + this.synchronizer = synchronizer; + } + + + @Override + public void onSubscribe(Subscription subscription) { + subscription.request(Long.MAX_VALUE); + } + + @Override + public void onNext(Void aVoid) { + // no op + } + + @Override + public void onError(Throwable ex) { + logger.error("Error from request handling. Completing the request.", ex); + this.synchronizer.complete(); + } + + @Override + public void onComplete() { + this.synchronizer.complete(); + } + } } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/ServletServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/ServletServerHttpRequest.java new file mode 100644 index 0000000000..f2b1596a30 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/ServletServerHttpRequest.java @@ -0,0 +1,119 @@ +/* + * Copyright 2002-2015 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.reactive.web.servlet; + +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.Charset; +import java.util.Enumeration; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; + +import org.reactivestreams.Publisher; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.reactive.web.ServerHttpRequest; +import org.springframework.util.Assert; +import org.springframework.util.LinkedCaseInsensitiveMap; +import org.springframework.util.StringUtils; + +/** + * @author Rossen Stoyanchev + */ +public class ServletServerHttpRequest implements ServerHttpRequest { + + private final HttpServletRequest servletRequest; + + private final Publisher requestBodyPublisher; + + private HttpHeaders headers; + + + public ServletServerHttpRequest(HttpServletRequest servletRequest, Publisher requestBodyPublisher) { + Assert.notNull(servletRequest, "HttpServletRequest must not be null"); + this.servletRequest = servletRequest; + this.requestBodyPublisher = requestBodyPublisher; + } + + + @Override + public HttpMethod getMethod() { + return HttpMethod.valueOf(this.servletRequest.getMethod()); + } + + @Override + public URI getURI() { + try { + return new URI(this.servletRequest.getScheme(), null, this.servletRequest.getServerName(), + this.servletRequest.getServerPort(), this.servletRequest.getRequestURI(), + this.servletRequest.getQueryString(), null); + } + catch (URISyntaxException ex) { + throw new IllegalStateException("Could not get HttpServletRequest URI: " + ex.getMessage(), ex); + } + } + + @Override + public HttpHeaders getHeaders() { + if (this.headers == null) { + this.headers = new HttpHeaders(); + for (Enumeration headerNames = this.servletRequest.getHeaderNames(); headerNames.hasMoreElements();) { + String headerName = (String) headerNames.nextElement(); + for (Enumeration headerValues = this.servletRequest.getHeaders(headerName); + headerValues.hasMoreElements();) { + String headerValue = (String) headerValues.nextElement(); + this.headers.add(headerName, headerValue); + } + } + // HttpServletRequest exposes some headers as properties: we should include those if not already present + MediaType contentType = this.headers.getContentType(); + if (contentType == null) { + String requestContentType = this.servletRequest.getContentType(); + if (StringUtils.hasLength(requestContentType)) { + contentType = MediaType.parseMediaType(requestContentType); + this.headers.setContentType(contentType); + } + } + if (contentType != null && contentType.getCharSet() == null) { + String requestEncoding = this.servletRequest.getCharacterEncoding(); + if (StringUtils.hasLength(requestEncoding)) { + Charset charSet = Charset.forName(requestEncoding); + Map params = new LinkedCaseInsensitiveMap<>(); + params.putAll(contentType.getParameters()); + params.put("charset", charSet.toString()); + MediaType newContentType = new MediaType(contentType.getType(), contentType.getSubtype(), params); + this.headers.setContentType(newContentType); + } + } + if (this.headers.getContentLength() == -1) { + int requestContentLength = this.servletRequest.getContentLength(); + if (requestContentLength != -1) { + this.headers.setContentLength(requestContentLength); + } + } + } + return this.headers; + } + + @Override + public Publisher getBody() { + return this.requestBodyPublisher; + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/ServletServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/ServletServerHttpResponse.java new file mode 100644 index 0000000000..6b8b42c536 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/ServletServerHttpResponse.java @@ -0,0 +1,88 @@ +/* + * Copyright 2002-2015 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.reactive.web.servlet; + +import java.util.List; +import java.util.Map; +import javax.servlet.http.HttpServletResponse; + +import org.reactivestreams.Publisher; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.reactive.web.ServerHttpResponse; +import org.springframework.util.Assert; + +/** + * @author Rossen Stoyanchev + */ +public class ServletServerHttpResponse implements ServerHttpResponse { + + private final HttpServletResponse servletResponse; + + private final ResponseBodySubscriber responseSubscriber; + + private final HttpHeaders headers; + + private boolean headersWritten = false; + + + public ServletServerHttpResponse(HttpServletResponse servletResponse, ResponseBodySubscriber responseSubscriber) { + Assert.notNull(servletResponse, "'servletResponse' must not be null"); + Assert.notNull(responseSubscriber, "'responseSubscriber' must not be null"); + this.servletResponse = servletResponse; + this.responseSubscriber = responseSubscriber; + this.headers = new HttpHeaders(); + } + + + @Override + public void setStatusCode(HttpStatus status) { + this.servletResponse.setStatus(status.value()); + } + + @Override + public HttpHeaders getHeaders() { + return (this.headersWritten ? HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers); + } + + @Override + public Publisher writeWith(final Publisher contentPublisher) { + writeHeaders(); + return (s -> contentPublisher.subscribe(responseSubscriber)); + } + + private void writeHeaders() { + if (!this.headersWritten) { + for (Map.Entry> entry : this.headers.entrySet()) { + String headerName = entry.getKey(); + for (String headerValue : entry.getValue()) { + this.servletResponse.addHeader(headerName, headerValue); + } + } + // HttpServletResponse exposes some headers as properties: we should include those if not already present + if (this.servletResponse.getContentType() == null && this.headers.getContentType() != null) { + this.servletResponse.setContentType(this.headers.getContentType().toString()); + } + if (this.servletResponse.getCharacterEncoding() == null && this.headers.getContentType() != null && + this.headers.getContentType().getCharSet() != null) { + this.servletResponse.setCharacterEncoding(this.headers.getContentType().getCharSet().name()); + } + this.headersWritten = true; + } + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/servlet/AbstractHttpHandlerServletIntegrationTestCase.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/AbstractHttpHandlerIntegrationTestCase.java similarity index 95% rename from spring-web-reactive/src/test/java/org/springframework/reactive/web/servlet/AbstractHttpHandlerServletIntegrationTestCase.java rename to spring-web-reactive/src/test/java/org/springframework/reactive/web/AbstractHttpHandlerIntegrationTestCase.java index 8b4494d2d7..d818fde298 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/servlet/AbstractHttpHandlerServletIntegrationTestCase.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/AbstractHttpHandlerIntegrationTestCase.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.web.servlet; +package org.springframework.reactive.web; import java.net.URI; import java.util.Random; @@ -29,7 +29,7 @@ import org.springframework.web.client.RestTemplate; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; -public abstract class AbstractHttpHandlerServletIntegrationTestCase { +public abstract class AbstractHttpHandlerIntegrationTestCase { private static final int REQUEST_SIZE = 4096 * 3; diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/CountingHttpHandler.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/CountingHttpHandler.java index de6bd16d4f..e82512b3b5 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/CountingHttpHandler.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/CountingHttpHandler.java @@ -30,8 +30,8 @@ public class CountingHttpHandler implements HttpHandler { private static final Log logger = LogFactory.getLog(CountingHttpHandler.class); @Override - public Publisher handle(Publisher request) { - request.subscribe(new Subscriber() { + public Publisher handle(ServerHttpRequest request, ServerHttpResponse response) { + request.getBody().subscribe(new Subscriber() { private Subscription subscription; private int byteCount = 0; diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/EchoHandler.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/EchoHandler.java index e554623144..fd4f94a510 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/EchoHandler.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/EchoHandler.java @@ -24,7 +24,7 @@ import org.reactivestreams.Publisher; public class EchoHandler implements HttpHandler { @Override - public Publisher handle(Publisher request) { - return request; + public Publisher handle(ServerHttpRequest request, ServerHttpResponse response) { + return response.writeWith(request.getBody()); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/servlet/HttpHandlerServletJettyIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/servlet/HttpHandlerJettyIntegrationTests.java similarity index 91% rename from spring-web-reactive/src/test/java/org/springframework/reactive/web/servlet/HttpHandlerServletJettyIntegrationTests.java rename to spring-web-reactive/src/test/java/org/springframework/reactive/web/servlet/HttpHandlerJettyIntegrationTests.java index d9ed1548ea..df1c945b85 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/servlet/HttpHandlerServletJettyIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/servlet/HttpHandlerJettyIntegrationTests.java @@ -23,13 +23,14 @@ import org.eclipse.jetty.servlet.ServletHolder; import org.junit.AfterClass; import org.junit.BeforeClass; +import org.springframework.reactive.web.AbstractHttpHandlerIntegrationTestCase; import org.springframework.reactive.web.EchoHandler; /** * @author Arjen Poutsma */ -public class HttpHandlerServletJettyIntegrationTests - extends AbstractHttpHandlerServletIntegrationTestCase { +public class HttpHandlerJettyIntegrationTests + extends AbstractHttpHandlerIntegrationTestCase { private static Server jettyServer; diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/servlet/HttpHandlerServletTomcatIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/servlet/HttpHandlerTomcatIntegrationTests.java similarity index 91% rename from spring-web-reactive/src/test/java/org/springframework/reactive/web/servlet/HttpHandlerServletTomcatIntegrationTests.java rename to spring-web-reactive/src/test/java/org/springframework/reactive/web/servlet/HttpHandlerTomcatIntegrationTests.java index 1c1bda427e..3482bfe50a 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/servlet/HttpHandlerServletTomcatIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/servlet/HttpHandlerTomcatIntegrationTests.java @@ -24,12 +24,13 @@ import org.apache.catalina.startup.Tomcat; import org.junit.AfterClass; import org.junit.BeforeClass; +import org.springframework.reactive.web.AbstractHttpHandlerIntegrationTestCase; import org.springframework.reactive.web.EchoHandler; /** * @author Arjen Poutsma */ -public class HttpHandlerServletTomcatIntegrationTests extends AbstractHttpHandlerServletIntegrationTestCase { +public class HttpHandlerTomcatIntegrationTests extends AbstractHttpHandlerIntegrationTestCase { private static Tomcat tomcatServer; From c682895deeaf2f03d9c6b60e1f4da0a469ba796a Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 11 Aug 2015 16:09:05 -0400 Subject: [PATCH 012/498] Add RxNetty support This commit adds RxNetty integration that includes RxNetty-based implementations of ServerHttpRequest and ServerHttpResponse as well as an adapter from the RxNetty RequestHandler to the HttpHandler contracts. Only byte[] is supported at the moment for reading and writing with a corresponding copy to and from Netty ByteBuf. --- spring-web-reactive/build.gradle | 4 + .../web/rxnetty/RequestHandlerAdapter.java | 50 +++++++++++ .../web/rxnetty/RxNettyServerHttpRequest.java | 86 +++++++++++++++++++ .../rxnetty/RxNettyServerHttpResponse.java | 72 ++++++++++++++++ .../HttpHandlerRxNettyIntegrationTests.java | 46 ++++++++++ 5 files changed, 258 insertions(+) create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/web/rxnetty/RequestHandlerAdapter.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/web/rxnetty/RxNettyServerHttpRequest.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/web/rxnetty/RxNettyServerHttpResponse.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/reactive/web/rxnetty/HttpHandlerRxNettyIntegrationTests.java diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index 34c0b96a55..bf58b483b6 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -15,6 +15,7 @@ apply plugin: 'propdeps-maven' repositories { mavenCentral() + maven { url 'https://oss.jfrog.org/libs-snapshot' } // RxNetty 0.5.x snapshots } dependencies { @@ -24,6 +25,9 @@ dependencies { compile "org.slf4j:slf4j-api:1.7.6" compile "ch.qos.logback:logback-classic:1.1.2" + optional "io.reactivex:rxnetty:0.5.0-SNAPSHOT" + optional "io.reactivex:rxjava-reactive-streams:1.0.1" + provided "javax.servlet:javax.servlet-api:3.1.0" testCompile "junit:junit:4.12" diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/rxnetty/RequestHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/rxnetty/RequestHandlerAdapter.java new file mode 100644 index 0000000000..2a7b7d7413 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/rxnetty/RequestHandlerAdapter.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2015 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.reactive.web.rxnetty; + +import io.netty.buffer.ByteBuf; +import io.reactivex.netty.protocol.http.server.HttpServerRequest; +import io.reactivex.netty.protocol.http.server.HttpServerResponse; +import io.reactivex.netty.protocol.http.server.RequestHandler; +import org.reactivestreams.Publisher; +import rx.Observable; +import rx.RxReactiveStreams; + +import org.springframework.reactive.web.HttpHandler; +import org.springframework.util.Assert; + +/** + * @author Rossen Stoyanchev + */ +public class RequestHandlerAdapter implements RequestHandler { + + private final HttpHandler httpHandler; + + + public RequestHandlerAdapter(HttpHandler httpHandler) { + Assert.notNull(httpHandler, "'httpHandler' is required."); + this.httpHandler = httpHandler; + } + + @Override + public Observable handle(HttpServerRequest request, HttpServerResponse response) { + RxNettyServerHttpRequest adaptedRequest = new RxNettyServerHttpRequest(request); + RxNettyServerHttpResponse adaptedResponse = new RxNettyServerHttpResponse(response); + Publisher result = this.httpHandler.handle(adaptedRequest, adaptedResponse); + return RxReactiveStreams.toObservable(result); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/rxnetty/RxNettyServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/rxnetty/RxNettyServerHttpRequest.java new file mode 100644 index 0000000000..5dd01a400f --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/rxnetty/RxNettyServerHttpRequest.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002-2015 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.reactive.web.rxnetty; + +import java.net.URI; +import java.net.URISyntaxException; + +import io.netty.buffer.ByteBuf; +import io.reactivex.netty.protocol.http.server.HttpServerRequest; +import org.reactivestreams.Publisher; +import rx.Observable; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.reactive.web.ServerHttpRequest; +import org.springframework.util.Assert; + +/** + * @author Rossen Stoyanchev + */ +public class RxNettyServerHttpRequest implements ServerHttpRequest { + + private final HttpServerRequest request; + + private HttpHeaders headers; + + + public RxNettyServerHttpRequest(HttpServerRequest request) { + Assert.notNull("'request', request must not be null."); + this.request = request; + } + + + @Override + public HttpHeaders getHeaders() { + if (this.headers == null) { + this.headers = new HttpHeaders(); + for (String name : this.request.getHeaderNames()) { + for (String value : this.request.getAllHeaderValues(name)) { + this.headers.add(name, value); + } + } + } + return this.headers; + } + + @Override + public HttpMethod getMethod() { + return HttpMethod.valueOf(this.request.getHttpMethod().name()); + } + + @Override + public URI getURI() { + try { + return new URI(this.request.getUri()); + } + catch (URISyntaxException ex) { + throw new IllegalStateException("Could not get URI: " + ex.getMessage(), ex); + } + + } + + @Override + public Publisher getBody() { + Observable bytesContent = this.request.getContent().map(byteBuf -> { + byte[] copy = new byte[byteBuf.readableBytes()]; + byteBuf.readBytes(copy); + return copy; + }); + return rx.RxReactiveStreams.toPublisher(bytesContent); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/rxnetty/RxNettyServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/rxnetty/RxNettyServerHttpResponse.java new file mode 100644 index 0000000000..d40c07463a --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/rxnetty/RxNettyServerHttpResponse.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2015 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.reactive.web.rxnetty; + +import io.netty.handler.codec.http.HttpResponseStatus; +import io.reactivex.netty.protocol.http.server.HttpServerResponse; +import org.reactivestreams.Publisher; +import rx.Observable; +import rx.RxReactiveStreams; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.reactive.web.ServerHttpResponse; +import org.springframework.util.Assert; + +/** + * @author Rossen Stoyanchev + */ +public class RxNettyServerHttpResponse implements ServerHttpResponse { + + private final HttpServerResponse response; + + private final HttpHeaders headers; + + private boolean headersWritten = false; + + + public RxNettyServerHttpResponse(HttpServerResponse response) { + Assert.notNull("'response', response must not be null."); + this.response = response; + this.headers = new HttpHeaders(); + } + + + @Override + public void setStatusCode(HttpStatus status) { + this.response.setStatus(HttpResponseStatus.valueOf(status.value())); + } + + @Override + public HttpHeaders getHeaders() { + return (this.headersWritten ? HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers); + } + + @Override + public Publisher writeWith(Publisher contentPublisher) { + writeHeaders(); + Observable contentObservable = RxReactiveStreams.toObservable(contentPublisher); + return RxReactiveStreams.toPublisher(this.response.writeBytes(contentObservable)); + } + + private void writeHeaders() { + if (!this.headersWritten) { + for (String name : this.headers.keySet()) { + this.response.setHeader(name, this.headers.get(name)); + } + } + } +} diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/rxnetty/HttpHandlerRxNettyIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/rxnetty/HttpHandlerRxNettyIntegrationTests.java new file mode 100644 index 0000000000..eef884033d --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/rxnetty/HttpHandlerRxNettyIntegrationTests.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2015 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.reactive.web.rxnetty; + +import io.netty.buffer.ByteBuf; +import io.reactivex.netty.protocol.http.server.HttpServer; +import org.junit.AfterClass; +import org.junit.BeforeClass; + +import org.springframework.reactive.web.EchoHandler; +import org.springframework.reactive.web.AbstractHttpHandlerIntegrationTestCase; + +/** + * @author Rossen Stoyanchev + */ +public class HttpHandlerRxNettyIntegrationTests extends AbstractHttpHandlerIntegrationTestCase { + + private static HttpServer httpServer; + + + @BeforeClass + public static void startServer() throws Exception { + RequestHandlerAdapter requestHandler = new RequestHandlerAdapter(new EchoHandler()); + httpServer = HttpServer.newServer(port); + httpServer.start(requestHandler::handle); + } + + @AfterClass + public static void stopServer() throws Exception { + httpServer.shutdown(); + } + +} \ No newline at end of file From 773d0444bf0834345a085616b8808a7e6c1925b1 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 17 Aug 2015 15:54:06 -0400 Subject: [PATCH 013/498] Add request handling infrastructure HandlerMapping, HandlerAdapter, HandlerResultHandler (+ HandlerResult) as the basic request handling contracts. DispatcherHandler to drive overall request handling. DispatcherApp provides minimal implementations of the above contracts enough to put together a running example that returns 200 text/plain "Hello world". --- .../reactive/web/DispatcherHttpHandler.java | 169 +++++++++++++ .../reactive/web/HandlerAdapter.java | 29 +++ .../reactive/web/HandlerMapping.java | 25 ++ .../reactive/web/HandlerResult.java | 35 +++ .../reactive/web/HandlerResultHandler.java | 29 +++ .../reactive/web/DispatcherApp.java | 233 ++++++++++++++++++ 6 files changed, 520 insertions(+) create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/web/DispatcherHttpHandler.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/web/HandlerAdapter.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/web/HandlerMapping.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/web/HandlerResult.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/web/HandlerResultHandler.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/reactive/web/DispatcherApp.java diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/DispatcherHttpHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/DispatcherHttpHandler.java new file mode 100644 index 0000000000..9f6f4aad6d --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/DispatcherHttpHandler.java @@ -0,0 +1,169 @@ +/* + * Copyright 2002-2015 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.reactive.web; + +import java.util.ArrayList; +import java.util.List; + +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.context.ApplicationContext; +import org.springframework.http.HttpStatus; + +/** + * @author Rossen Stoyanchev + */ +public class DispatcherHttpHandler implements HttpHandler { + + private List handlerMappings; + + private List handlerAdapters; + + private List resultHandlers; + + + protected void initStrategies(ApplicationContext context) { + + this.handlerMappings = new ArrayList<>(BeanFactoryUtils.beansOfTypeIncludingAncestors( + context, HandlerMapping.class, true, false).values()); + + this.handlerAdapters = new ArrayList<>(BeanFactoryUtils.beansOfTypeIncludingAncestors( + context, HandlerAdapter.class, true, false).values()); + + this.resultHandlers = new ArrayList<>(BeanFactoryUtils.beansOfTypeIncludingAncestors( + context, HandlerResultHandler.class, true, false).values()); + } + + + @Override + public Publisher handle(ServerHttpRequest request, ServerHttpResponse response) { + + Object handler = getHandler(request); + if (handler == null) { + // No exception handling mechanism yet + response.setStatusCode(HttpStatus.NOT_FOUND); + return Publishers.complete(); + } + + HandlerAdapter handlerAdapter = getHandlerAdapter(handler); + final Publisher resultPublisher = handlerAdapter.handle(request, response, handler); + + return new Publisher() { + + @Override + public void subscribe(final Subscriber subscriber) { + + resultPublisher.subscribe(new Subscriber() { + + @Override + public void onSubscribe(Subscription subscription) { + subscription.request(Long.MAX_VALUE); + } + + @Override + public void onNext(HandlerResult result) { + for (HandlerResultHandler resultHandler : resultHandlers) { + if (resultHandler.supports(result)) { + Publisher publisher = resultHandler.handleResult(request, response, result); + publisher.subscribe(new Subscriber() { + @Override + public void onSubscribe(Subscription subscription) { + subscription.request(Long.MAX_VALUE); + } + + @Override + public void onNext(Void aVoid) { + // no op + } + + @Override + public void onError(Throwable error) { + // Result handling error (no exception handling mechanism yet) + subscriber.onError(error); + } + + @Override + public void onComplete() { + subscriber.onComplete(); + } + }); + } + } + } + + @Override + public void onError(Throwable error) { + // Application handler error (no exception handling mechanism yet) + subscriber.onError(error); + } + + @Override + public void onComplete() { + // do nothing + } + }); + } + }; + } + + protected Object getHandler(ServerHttpRequest request) { + Object handler = null; + for (HandlerMapping handlerMapping : this.handlerMappings) { + handler = handlerMapping.getHandler(request); + if (handler != null) { + break; + } + } + return handler; + } + + protected HandlerAdapter getHandlerAdapter(Object handler) { + for (HandlerAdapter handlerAdapter : this.handlerAdapters) { + if (handlerAdapter.supports(handler)) { + return handlerAdapter; + } + } + // more specific exception + throw new IllegalStateException("No HandlerAdapter for " + handler); + } + + + private static class Publishers { + + + public static Publisher complete() { + return subscriber -> { + subscriber.onSubscribe(new NoopSubscription()); + subscriber.onComplete(); + }; + } + } + + private static class NoopSubscription implements Subscription { + + @Override + public void request(long n) { + } + + @Override + public void cancel() { + } + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/HandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/HandlerAdapter.java new file mode 100644 index 0000000000..d745ef6dfb --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/HandlerAdapter.java @@ -0,0 +1,29 @@ +/* + * Copyright 2002-2015 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.reactive.web; + +import org.reactivestreams.Publisher; + +/** + * @author Rossen Stoyanchev + */ +public interface HandlerAdapter { + + boolean supports(Object handler); + + Publisher handle(ServerHttpRequest request, ServerHttpResponse response, Object handler); + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/HandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/HandlerMapping.java new file mode 100644 index 0000000000..018e33002c --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/HandlerMapping.java @@ -0,0 +1,25 @@ +/* + * Copyright 2002-2015 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.reactive.web; + +/** + * @author Rossen Stoyanchev + */ +public interface HandlerMapping { + + Object getHandler(ServerHttpRequest request); + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/HandlerResult.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/HandlerResult.java new file mode 100644 index 0000000000..a5403b5961 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/HandlerResult.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2015 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.reactive.web; + +/** + * @author Rossen Stoyanchev + */ +public class HandlerResult { + + private final Object returnValue; + + + public HandlerResult(Object returnValue) { + this.returnValue = returnValue; + } + + + public Object getReturnValue() { + return this.returnValue; + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/HandlerResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/HandlerResultHandler.java new file mode 100644 index 0000000000..ae18cff9b3 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/HandlerResultHandler.java @@ -0,0 +1,29 @@ +/* + * Copyright 2002-2015 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.reactive.web; + +import org.reactivestreams.Publisher; + +/** + * @author Rossen Stoyanchev + */ +public interface HandlerResultHandler { + + boolean supports(HandlerResult result); + + Publisher handleResult(ServerHttpRequest request, ServerHttpResponse response, HandlerResult result); + +} \ No newline at end of file diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/DispatcherApp.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/DispatcherApp.java new file mode 100644 index 0000000000..7b3eb7c5d7 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/DispatcherApp.java @@ -0,0 +1,233 @@ +/* + * Copyright 2002-2015 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.reactive.web; + +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.Map; + +import io.netty.buffer.ByteBuf; +import io.reactivex.netty.protocol.http.server.HttpServer; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +import org.springframework.http.MediaType; +import org.springframework.reactive.web.rxnetty.RequestHandlerAdapter; +import org.springframework.web.context.support.StaticWebApplicationContext; + +/** + * @author Rossen Stoyanchev + */ +public class DispatcherApp { + + public static void main(String[] args) { + + StaticWebApplicationContext wac = new StaticWebApplicationContext(); + wac.registerSingleton("handlerMapping", SimpleUrlHandlerMapping.class); + wac.registerSingleton("handlerAdapter", PlainTextHandlerAdapter.class); + wac.registerSingleton("resultHandler", PlainTextResultHandler.class); + wac.refresh(); + + SimpleUrlHandlerMapping handlerMapping = wac.getBean(SimpleUrlHandlerMapping.class); + handlerMapping.addHandler("/text", new HelloWorldTextHandler()); + + DispatcherHttpHandler dispatcherHandler = new DispatcherHttpHandler(); + dispatcherHandler.initStrategies(wac); + + RequestHandlerAdapter requestHandler = new RequestHandlerAdapter(dispatcherHandler); + HttpServer server = HttpServer.newServer(8080); + server.start(requestHandler::handle); + server.awaitShutdown(); + } + + + private static class SimpleUrlHandlerMapping implements HandlerMapping { + + private final Map handlerMap = new HashMap<>(); + + + public void addHandler(String path, Object handler) { + this.handlerMap.put(path, handler); + } + + @Override + public Object getHandler(ServerHttpRequest request) { + return this.handlerMap.get(request.getURI().getPath()); + } + } + + private interface PlainTextHandler { + + Publisher handle(ServerHttpRequest request, ServerHttpResponse response); + + } + + private static class HelloWorldTextHandler implements PlainTextHandler { + + @Override + public Publisher handle(ServerHttpRequest request, ServerHttpResponse response) { + + return new Publisher() { + + @Override + public void subscribe(Subscriber subscriber) { + subscriber.onSubscribe(new AbstractSubscription(subscriber) { + + @Override + protected void requestInternal(long n) { + invokeOnNext("Hello world."); + invokeOnComplete(); + } + }); + } + }; + } + + } + + private static class PlainTextHandlerAdapter implements HandlerAdapter { + + @Override + public boolean supports(Object handler) { + return PlainTextHandler.class.isAssignableFrom(handler.getClass()); + } + + @Override + public Publisher handle(ServerHttpRequest request, ServerHttpResponse response, + Object handler) { + + PlainTextHandler textHandler = (PlainTextHandler) handler; + final Publisher resultPublisher = textHandler.handle(request, response); + + return new Publisher() { + + @Override + public void subscribe(Subscriber handlerResultSubscriber) { + handlerResultSubscriber.onSubscribe(new AbstractSubscription(handlerResultSubscriber) { + + @Override + protected void requestInternal(long n) { + resultPublisher.subscribe(new Subscriber() { + + @Override + public void onSubscribe(Subscription subscription) { + subscription.request(Long.MAX_VALUE); + } + + @Override + public void onNext(Object result) { + invokeOnNext(new HandlerResult(result)); + } + + @Override + public void onError(Throwable error) { + invokeOnError(error); + } + + @Override + public void onComplete() { + invokeOnComplete(); + } + }); + } + }); + } + }; + } + } + + private static class PlainTextResultHandler implements HandlerResultHandler { + + @Override + public boolean supports(HandlerResult result) { + Object value = result.getReturnValue(); + return (value != null && String.class.equals(value.getClass())); + } + + @Override + public Publisher handleResult(ServerHttpRequest request, ServerHttpResponse response, + HandlerResult result) { + + response.getHeaders().setContentType(MediaType.TEXT_PLAIN); + + return response.writeWith(new Publisher() { + + @Override + public void subscribe(Subscriber writeSubscriber) { + writeSubscriber.onSubscribe(new AbstractSubscription(writeSubscriber) { + + @Override + protected void requestInternal(long n) { + Charset charset = Charset.forName("UTF-8"); + invokeOnNext(((String) result.getReturnValue()).getBytes(charset)); + invokeOnComplete(); + } + }); + } + }); + } + } + + + private static abstract class AbstractSubscription implements Subscription { + + private final Subscriber subscriber; + + private volatile boolean terminated; + + + public AbstractSubscription(Subscriber subscriber) { + this.subscriber = subscriber; + } + + protected boolean isTerminated() { + return this.terminated; + } + + @Override + public void request(long n) { + if (isTerminated()) { + return; + } + if (n > 0) { + requestInternal(n); + } + } + + protected abstract void requestInternal(long n); + + @Override + public void cancel() { + this.terminated = true; + } + + protected void invokeOnNext(T data) { + this.subscriber.onNext(data); + } + + protected void invokeOnError(Throwable error) { + this.terminated = true; + this.subscriber.onError(error); + } + + protected void invokeOnComplete() { + this.terminated = true; + this.subscriber.onComplete(); + } + } + +} From 69d4eaaa3cd9816b5f1338871fa871ad6a93def8 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 17 Aug 2015 16:52:39 -0400 Subject: [PATCH 014/498] Use PublisherFactory from reactor-core --- spring-web-reactive/build.gradle | 1 + .../reactive/web/DispatcherHttpHandler.java | 27 +--- .../reactive/web/DispatcherApp.java | 143 ++++-------------- 3 files changed, 35 insertions(+), 136 deletions(-) diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index bf58b483b6..3cb153e31c 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -22,6 +22,7 @@ dependencies { compile "org.springframework:spring-core:4.2.0.RELEASE" compile "org.springframework:spring-web:4.2.0.RELEASE" compile "org.reactivestreams:reactive-streams:1.0.0" + compile "io.projectreactor:reactor-core:2.0.5.RELEASE" compile "org.slf4j:slf4j-api:1.7.6" compile "ch.qos.logback:logback-classic:1.1.2" diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/DispatcherHttpHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/DispatcherHttpHandler.java index 9f6f4aad6d..5093586451 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/DispatcherHttpHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/DispatcherHttpHandler.java @@ -21,6 +21,8 @@ import java.util.List; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; +import reactor.core.reactivestreams.PublisherFactory; +import reactor.core.reactivestreams.SubscriberWithContext; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.context.ApplicationContext; @@ -58,7 +60,7 @@ public class DispatcherHttpHandler implements HttpHandler { if (handler == null) { // No exception handling mechanism yet response.setStatusCode(HttpStatus.NOT_FOUND); - return Publishers.complete(); + return PublisherFactory.forEach(SubscriberWithContext::onComplete); } HandlerAdapter handlerAdapter = getHandlerAdapter(handler); @@ -143,27 +145,4 @@ public class DispatcherHttpHandler implements HttpHandler { throw new IllegalStateException("No HandlerAdapter for " + handler); } - - private static class Publishers { - - - public static Publisher complete() { - return subscriber -> { - subscriber.onSubscribe(new NoopSubscription()); - subscriber.onComplete(); - }; - } - } - - private static class NoopSubscription implements Subscription { - - @Override - public void request(long n) { - } - - @Override - public void cancel() { - } - } - } diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/DispatcherApp.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/DispatcherApp.java index 7b3eb7c5d7..1707908b17 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/DispatcherApp.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/DispatcherApp.java @@ -24,6 +24,7 @@ import io.reactivex.netty.protocol.http.server.HttpServer; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; +import reactor.core.reactivestreams.PublisherFactory; import org.springframework.http.MediaType; import org.springframework.reactive.web.rxnetty.RequestHandlerAdapter; @@ -80,21 +81,10 @@ public class DispatcherApp { @Override public Publisher handle(ServerHttpRequest request, ServerHttpResponse response) { - - return new Publisher() { - - @Override - public void subscribe(Subscriber subscriber) { - subscriber.onSubscribe(new AbstractSubscription(subscriber) { - - @Override - protected void requestInternal(long n) { - invokeOnNext("Hello world."); - invokeOnComplete(); - } - }); - } - }; + return PublisherFactory.forEach((subscriber) -> { + subscriber.onNext("Hello world."); + subscriber.onComplete(); + }); } } @@ -113,40 +103,30 @@ public class DispatcherApp { PlainTextHandler textHandler = (PlainTextHandler) handler; final Publisher resultPublisher = textHandler.handle(request, response); - return new Publisher() { + return PublisherFactory.forEach((subscriber) -> { + resultPublisher.subscribe(new Subscriber() { - @Override - public void subscribe(Subscriber handlerResultSubscriber) { - handlerResultSubscriber.onSubscribe(new AbstractSubscription(handlerResultSubscriber) { + @Override + public void onSubscribe(Subscription subscription) { + subscription.request(Long.MAX_VALUE); + } - @Override - protected void requestInternal(long n) { - resultPublisher.subscribe(new Subscriber() { + @Override + public void onNext(Object result) { + subscriber.onNext(new HandlerResult(result)); + } - @Override - public void onSubscribe(Subscription subscription) { - subscription.request(Long.MAX_VALUE); - } + @Override + public void onError(Throwable error) { + subscriber.onError(error); + } - @Override - public void onNext(Object result) { - invokeOnNext(new HandlerResult(result)); - } - - @Override - public void onError(Throwable error) { - invokeOnError(error); - } - - @Override - public void onComplete() { - invokeOnComplete(); - } - }); - } - }); - } - }; + @Override + public void onComplete() { + subscriber.onComplete(); + } + }); + }); } } @@ -159,74 +139,13 @@ public class DispatcherApp { } @Override - public Publisher handleResult(ServerHttpRequest request, ServerHttpResponse response, - HandlerResult result) { - + public Publisher handleResult(ServerHttpRequest request, ServerHttpResponse response, HandlerResult result) { response.getHeaders().setContentType(MediaType.TEXT_PLAIN); - - return response.writeWith(new Publisher() { - - @Override - public void subscribe(Subscriber writeSubscriber) { - writeSubscriber.onSubscribe(new AbstractSubscription(writeSubscriber) { - - @Override - protected void requestInternal(long n) { - Charset charset = Charset.forName("UTF-8"); - invokeOnNext(((String) result.getReturnValue()).getBytes(charset)); - invokeOnComplete(); - } - }); - } - }); - } - } - - - private static abstract class AbstractSubscription implements Subscription { - - private final Subscriber subscriber; - - private volatile boolean terminated; - - - public AbstractSubscription(Subscriber subscriber) { - this.subscriber = subscriber; - } - - protected boolean isTerminated() { - return this.terminated; - } - - @Override - public void request(long n) { - if (isTerminated()) { - return; - } - if (n > 0) { - requestInternal(n); - } - } - - protected abstract void requestInternal(long n); - - @Override - public void cancel() { - this.terminated = true; - } - - protected void invokeOnNext(T data) { - this.subscriber.onNext(data); - } - - protected void invokeOnError(Throwable error) { - this.terminated = true; - this.subscriber.onError(error); - } - - protected void invokeOnComplete() { - this.terminated = true; - this.subscriber.onComplete(); + return response.writeWith(PublisherFactory.forEach((writeSubscriber) -> { + Charset charset = Charset.forName("UTF-8"); + writeSubscriber.onNext(((String) result.getReturnValue()).getBytes(charset)); + writeSubscriber.onComplete(); + })); } } From 6634d1f536cf1421a7bcc766285d6177276181ec Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 17 Aug 2015 18:36:18 -0400 Subject: [PATCH 015/498] Use Streams from reactor-streams --- spring-web-reactive/build.gradle | 1 + .../reactive/web/DispatcherHttpHandler.java | 69 +++---------------- .../reactive/web/DispatcherApp.java | 52 +++----------- 3 files changed, 21 insertions(+), 101 deletions(-) diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index 3cb153e31c..c657c13445 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -23,6 +23,7 @@ dependencies { compile "org.springframework:spring-web:4.2.0.RELEASE" compile "org.reactivestreams:reactive-streams:1.0.0" compile "io.projectreactor:reactor-core:2.0.5.RELEASE" + compile "io.projectreactor:reactor-net:2.0.5.RELEASE" compile "org.slf4j:slf4j-api:1.7.6" compile "ch.qos.logback:logback-classic:1.1.2" diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/DispatcherHttpHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/DispatcherHttpHandler.java index 5093586451..1d5d0da3b0 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/DispatcherHttpHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/DispatcherHttpHandler.java @@ -19,10 +19,10 @@ import java.util.ArrayList; import java.util.List; import org.reactivestreams.Publisher; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; import reactor.core.reactivestreams.PublisherFactory; import reactor.core.reactivestreams.SubscriberWithContext; +import reactor.rx.Promises; +import reactor.rx.Streams; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.context.ApplicationContext; @@ -64,64 +64,17 @@ public class DispatcherHttpHandler implements HttpHandler { } HandlerAdapter handlerAdapter = getHandlerAdapter(handler); - final Publisher resultPublisher = handlerAdapter.handle(request, response, handler); + Publisher resultPublisher = handlerAdapter.handle(request, response, handler); - return new Publisher() { - - @Override - public void subscribe(final Subscriber subscriber) { - - resultPublisher.subscribe(new Subscriber() { - - @Override - public void onSubscribe(Subscription subscription) { - subscription.request(Long.MAX_VALUE); - } - - @Override - public void onNext(HandlerResult result) { - for (HandlerResultHandler resultHandler : resultHandlers) { - if (resultHandler.supports(result)) { - Publisher publisher = resultHandler.handleResult(request, response, result); - publisher.subscribe(new Subscriber() { - @Override - public void onSubscribe(Subscription subscription) { - subscription.request(Long.MAX_VALUE); - } - - @Override - public void onNext(Void aVoid) { - // no op - } - - @Override - public void onError(Throwable error) { - // Result handling error (no exception handling mechanism yet) - subscriber.onError(error); - } - - @Override - public void onComplete() { - subscriber.onComplete(); - } - }); - } - } - } - - @Override - public void onError(Throwable error) { - // Application handler error (no exception handling mechanism yet) - subscriber.onError(error); - } - - @Override - public void onComplete() { - // do nothing - } - }); + return Streams.wrap(resultPublisher).concatMap((HandlerResult result) -> { + for (HandlerResultHandler resultHandler : resultHandlers) { + if (resultHandler.supports(result)) { + return resultHandler.handleResult(request, response, result); + } } - }; + String error = "No HandlerResultHandler for " + result.getReturnValue(); + return Promises.error(new IllegalStateException(error)); + }); } protected Object getHandler(ServerHttpRequest request) { diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/DispatcherApp.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/DispatcherApp.java index 1707908b17..138051c1ed 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/DispatcherApp.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/DispatcherApp.java @@ -22,9 +22,8 @@ import java.util.Map; import io.netty.buffer.ByteBuf; import io.reactivex.netty.protocol.http.server.HttpServer; import org.reactivestreams.Publisher; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; -import reactor.core.reactivestreams.PublisherFactory; +import reactor.rx.Stream; +import reactor.rx.Streams; import org.springframework.http.MediaType; import org.springframework.reactive.web.rxnetty.RequestHandlerAdapter; @@ -81,12 +80,8 @@ public class DispatcherApp { @Override public Publisher handle(ServerHttpRequest request, ServerHttpResponse response) { - return PublisherFactory.forEach((subscriber) -> { - subscriber.onNext("Hello world."); - subscriber.onComplete(); - }); + return Streams.just("Hello world."); } - } private static class PlainTextHandlerAdapter implements HandlerAdapter { @@ -97,36 +92,10 @@ public class DispatcherApp { } @Override - public Publisher handle(ServerHttpRequest request, ServerHttpResponse response, - Object handler) { - - PlainTextHandler textHandler = (PlainTextHandler) handler; - final Publisher resultPublisher = textHandler.handle(request, response); - - return PublisherFactory.forEach((subscriber) -> { - resultPublisher.subscribe(new Subscriber() { - - @Override - public void onSubscribe(Subscription subscription) { - subscription.request(Long.MAX_VALUE); - } - - @Override - public void onNext(Object result) { - subscriber.onNext(new HandlerResult(result)); - } - - @Override - public void onError(Throwable error) { - subscriber.onError(error); - } - - @Override - public void onComplete() { - subscriber.onComplete(); - } - }); - }); + public Publisher handle(ServerHttpRequest request, ServerHttpResponse response, Object handler) { + Publisher resultPublisher = ((PlainTextHandler) handler).handle(request, response); + Stream stream = Streams.wrap(resultPublisher); + return stream.concatMap((returnValue) -> Streams.just(new HandlerResult(returnValue))); } } @@ -141,11 +110,8 @@ public class DispatcherApp { @Override public Publisher handleResult(ServerHttpRequest request, ServerHttpResponse response, HandlerResult result) { response.getHeaders().setContentType(MediaType.TEXT_PLAIN); - return response.writeWith(PublisherFactory.forEach((writeSubscriber) -> { - Charset charset = Charset.forName("UTF-8"); - writeSubscriber.onNext(((String) result.getReturnValue()).getBytes(charset)); - writeSubscriber.onComplete(); - })); + byte[] bytes = ((String) result.getReturnValue()).getBytes(Charset.forName("UTF-8")); + return response.writeWith(Streams.just(bytes)); } } From 56d69404858d2170e2e177ce9a3fd453b70f2d67 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 24 Aug 2015 14:03:36 -0400 Subject: [PATCH 016/498] Replace Promises.error->Streams.fail, concatMap->map --- .../springframework/reactive/web/DispatcherHttpHandler.java | 5 ++--- .../org/springframework/reactive/web/DispatcherApp.java | 6 ++---- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/DispatcherHttpHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/DispatcherHttpHandler.java index 1d5d0da3b0..781edd006b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/DispatcherHttpHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/DispatcherHttpHandler.java @@ -21,7 +21,6 @@ import java.util.List; import org.reactivestreams.Publisher; import reactor.core.reactivestreams.PublisherFactory; import reactor.core.reactivestreams.SubscriberWithContext; -import reactor.rx.Promises; import reactor.rx.Streams; import org.springframework.beans.factory.BeanFactoryUtils; @@ -72,8 +71,8 @@ public class DispatcherHttpHandler implements HttpHandler { return resultHandler.handleResult(request, response, result); } } - String error = "No HandlerResultHandler for " + result.getReturnValue(); - return Promises.error(new IllegalStateException(error)); + return Streams.fail(new IllegalStateException( + "No HandlerResultHandler for " + result.getReturnValue())); }); } diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/DispatcherApp.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/DispatcherApp.java index 138051c1ed..6fac51f692 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/DispatcherApp.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/DispatcherApp.java @@ -22,7 +22,6 @@ import java.util.Map; import io.netty.buffer.ByteBuf; import io.reactivex.netty.protocol.http.server.HttpServer; import org.reactivestreams.Publisher; -import reactor.rx.Stream; import reactor.rx.Streams; import org.springframework.http.MediaType; @@ -93,9 +92,8 @@ public class DispatcherApp { @Override public Publisher handle(ServerHttpRequest request, ServerHttpResponse response, Object handler) { - Publisher resultPublisher = ((PlainTextHandler) handler).handle(request, response); - Stream stream = Streams.wrap(resultPublisher); - return stream.concatMap((returnValue) -> Streams.just(new HandlerResult(returnValue))); + Publisher publisher = ((PlainTextHandler) handler).handle(request, response); + return Streams.wrap(publisher).map(HandlerResult::new); } } From f020a1add19b3761f2fd8e01fe5e96ce1b8d45c7 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 24 Aug 2015 14:41:58 -0400 Subject: [PATCH 017/498] Refactor package structure Introduce http and dispatch packages under org.springframework.web to separate HTTP-specific adapter code from higher level web framework code. Move classes into their respective locations. --- .../DispatcherHandler.java} | 7 +++++-- .../reactive/web/{ => dispatch}/HandlerAdapter.java | 5 ++++- .../reactive/web/{ => dispatch}/HandlerMapping.java | 4 +++- .../reactive/web/{ => dispatch}/HandlerResult.java | 2 +- .../reactive/web/{ => dispatch}/HandlerResultHandler.java | 5 ++++- .../reactive/web/{ => http}/HttpMessage.java | 2 +- .../reactive/web/{ => http}/HttpRequest.java | 3 +-- .../web/{HttpHandler.java => http/ServerHttpHandler.java} | 7 +++++-- .../reactive/web/{ => http}/ServerHttpRequest.java | 2 +- .../reactive/web/{ => http}/ServerHttpResponse.java | 2 +- .../web/{ => http}/rxnetty/RequestHandlerAdapter.java | 8 ++++---- .../web/{ => http}/rxnetty/RxNettyServerHttpRequest.java | 4 ++-- .../web/{ => http}/rxnetty/RxNettyServerHttpResponse.java | 4 ++-- .../web/{ => http}/servlet/AsyncContextSynchronizer.java | 2 +- .../web/{ => http}/servlet/HttpHandlerServlet.java | 8 ++++---- .../web/{ => http}/servlet/RequestBodyPublisher.java | 2 +- .../web/{ => http}/servlet/ResponseBodySubscriber.java | 2 +- .../web/{ => http}/servlet/ServletServerHttpRequest.java | 4 ++-- .../web/{ => http}/servlet/ServletServerHttpResponse.java | 4 ++-- .../reactive/web/{ => dispatch}/DispatcherApp.java | 8 +++++--- .../AbstractHttpHandlerIntegrationTestCase.java | 2 +- .../CountingHandler.java} | 6 +++--- .../reactive/web/{ => http}/EchoHandler.java | 8 ++++++-- .../rxnetty/HttpHandlerRxNettyIntegrationTests.java | 6 +++--- .../{ => http}/servlet/AsyncContextSynchronizerTests.java | 2 +- .../servlet/HttpHandlerJettyIntegrationTests.java | 6 +++--- .../servlet/HttpHandlerTomcatIntegrationTests.java | 6 +++--- 27 files changed, 70 insertions(+), 51 deletions(-) rename spring-web-reactive/src/main/java/org/springframework/reactive/web/{DispatcherHttpHandler.java => dispatch/DispatcherHandler.java} (91%) rename spring-web-reactive/src/main/java/org/springframework/reactive/web/{ => dispatch}/HandlerAdapter.java (82%) rename spring-web-reactive/src/main/java/org/springframework/reactive/web/{ => dispatch}/HandlerMapping.java (86%) rename spring-web-reactive/src/main/java/org/springframework/reactive/web/{ => dispatch}/HandlerResult.java (94%) rename spring-web-reactive/src/main/java/org/springframework/reactive/web/{ => dispatch}/HandlerResultHandler.java (83%) rename spring-web-reactive/src/main/java/org/springframework/reactive/web/{ => http}/HttpMessage.java (94%) rename spring-web-reactive/src/main/java/org/springframework/reactive/web/{ => http}/HttpRequest.java (90%) rename spring-web-reactive/src/main/java/org/springframework/reactive/web/{HttpHandler.java => http/ServerHttpHandler.java} (79%) rename spring-web-reactive/src/main/java/org/springframework/reactive/web/{ => http}/ServerHttpRequest.java (94%) rename spring-web-reactive/src/main/java/org/springframework/reactive/web/{ => http}/ServerHttpResponse.java (95%) rename spring-web-reactive/src/main/java/org/springframework/reactive/web/{ => http}/rxnetty/RequestHandlerAdapter.java (87%) rename spring-web-reactive/src/main/java/org/springframework/reactive/web/{ => http}/rxnetty/RxNettyServerHttpRequest.java (94%) rename spring-web-reactive/src/main/java/org/springframework/reactive/web/{ => http}/rxnetty/RxNettyServerHttpResponse.java (94%) rename spring-web-reactive/src/main/java/org/springframework/reactive/web/{ => http}/servlet/AsyncContextSynchronizer.java (98%) rename spring-web-reactive/src/main/java/org/springframework/reactive/web/{ => http}/servlet/HttpHandlerServlet.java (93%) rename spring-web-reactive/src/main/java/org/springframework/reactive/web/{ => http}/servlet/RequestBodyPublisher.java (98%) rename spring-web-reactive/src/main/java/org/springframework/reactive/web/{ => http}/servlet/ResponseBodySubscriber.java (97%) rename spring-web-reactive/src/main/java/org/springframework/reactive/web/{ => http}/servlet/ServletServerHttpRequest.java (97%) rename spring-web-reactive/src/main/java/org/springframework/reactive/web/{ => http}/servlet/ServletServerHttpResponse.java (96%) rename spring-web-reactive/src/test/java/org/springframework/reactive/web/{ => dispatch}/DispatcherApp.java (91%) rename spring-web-reactive/src/test/java/org/springframework/reactive/web/{ => http}/AbstractHttpHandlerIntegrationTestCase.java (98%) rename spring-web-reactive/src/test/java/org/springframework/reactive/web/{CountingHttpHandler.java => http/CountingHandler.java} (89%) rename spring-web-reactive/src/test/java/org/springframework/reactive/web/{ => http}/EchoHandler.java (74%) rename spring-web-reactive/src/test/java/org/springframework/reactive/web/{ => http}/rxnetty/HttpHandlerRxNettyIntegrationTests.java (86%) rename spring-web-reactive/src/test/java/org/springframework/reactive/web/{ => http}/servlet/AsyncContextSynchronizerTests.java (96%) rename spring-web-reactive/src/test/java/org/springframework/reactive/web/{ => http}/servlet/HttpHandlerJettyIntegrationTests.java (90%) rename spring-web-reactive/src/test/java/org/springframework/reactive/web/{ => http}/servlet/HttpHandlerTomcatIntegrationTests.java (90%) diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/DispatcherHttpHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/DispatcherHandler.java similarity index 91% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/DispatcherHttpHandler.java rename to spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/DispatcherHandler.java index 781edd006b..84cb8cfb7f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/DispatcherHttpHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/DispatcherHandler.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.reactive.web; +package org.springframework.reactive.web.dispatch; import java.util.ArrayList; import java.util.List; @@ -26,11 +26,14 @@ import reactor.rx.Streams; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.context.ApplicationContext; import org.springframework.http.HttpStatus; +import org.springframework.reactive.web.http.ServerHttpHandler; +import org.springframework.reactive.web.http.ServerHttpRequest; +import org.springframework.reactive.web.http.ServerHttpResponse; /** * @author Rossen Stoyanchev */ -public class DispatcherHttpHandler implements HttpHandler { +public class DispatcherHandler implements ServerHttpHandler { private List handlerMappings; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/HandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerAdapter.java similarity index 82% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/HandlerAdapter.java rename to spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerAdapter.java index d745ef6dfb..49f778540a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/HandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerAdapter.java @@ -13,10 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.reactive.web; +package org.springframework.reactive.web.dispatch; import org.reactivestreams.Publisher; +import org.springframework.reactive.web.http.ServerHttpRequest; +import org.springframework.reactive.web.http.ServerHttpResponse; + /** * @author Rossen Stoyanchev */ diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/HandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerMapping.java similarity index 86% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/HandlerMapping.java rename to spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerMapping.java index 018e33002c..e3472eba53 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/HandlerMapping.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerMapping.java @@ -13,7 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.reactive.web; +package org.springframework.reactive.web.dispatch; + +import org.springframework.reactive.web.http.ServerHttpRequest; /** * @author Rossen Stoyanchev diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/HandlerResult.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerResult.java similarity index 94% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/HandlerResult.java rename to spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerResult.java index a5403b5961..29a02c00e8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/HandlerResult.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerResult.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.reactive.web; +package org.springframework.reactive.web.dispatch; /** * @author Rossen Stoyanchev diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/HandlerResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerResultHandler.java similarity index 83% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/HandlerResultHandler.java rename to spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerResultHandler.java index ae18cff9b3..46257e3ec4 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/HandlerResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerResultHandler.java @@ -13,10 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.reactive.web; +package org.springframework.reactive.web.dispatch; import org.reactivestreams.Publisher; +import org.springframework.reactive.web.http.ServerHttpRequest; +import org.springframework.reactive.web.http.ServerHttpResponse; + /** * @author Rossen Stoyanchev */ diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/HttpMessage.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/HttpMessage.java similarity index 94% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/HttpMessage.java rename to spring-web-reactive/src/main/java/org/springframework/reactive/web/http/HttpMessage.java index 6871fcc3b5..0c5477c077 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/HttpMessage.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/HttpMessage.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.reactive.web; +package org.springframework.reactive.web.http; import java.net.URI; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/HttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/HttpRequest.java similarity index 90% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/HttpRequest.java rename to spring-web-reactive/src/main/java/org/springframework/reactive/web/http/HttpRequest.java index bffe2f4344..96767fe2fe 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/HttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/HttpRequest.java @@ -13,11 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.reactive.web; +package org.springframework.reactive.web.http; import java.net.URI; -import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; /** diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/HttpHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/ServerHttpHandler.java similarity index 79% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/HttpHandler.java rename to spring-web-reactive/src/main/java/org/springframework/reactive/web/http/ServerHttpHandler.java index 7065344567..d393e45f3d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/HttpHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/ServerHttpHandler.java @@ -14,15 +14,18 @@ * limitations under the License. */ -package org.springframework.reactive.web; +package org.springframework.reactive.web.http; import org.reactivestreams.Publisher; +import org.springframework.reactive.web.http.ServerHttpRequest; +import org.springframework.reactive.web.http.ServerHttpResponse; + /** * @author Arjen Poutsma * @author Rossen Stoyanchev */ -public interface HttpHandler { +public interface ServerHttpHandler { Publisher handle(ServerHttpRequest request, ServerHttpResponse response); diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/ServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/ServerHttpRequest.java similarity index 94% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/ServerHttpRequest.java rename to spring-web-reactive/src/main/java/org/springframework/reactive/web/http/ServerHttpRequest.java index 060e2cfd4b..c2e24d2f9c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/ServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/ServerHttpRequest.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.reactive.web; +package org.springframework.reactive.web.http; import org.reactivestreams.Publisher; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/ServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/ServerHttpResponse.java similarity index 95% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/ServerHttpResponse.java rename to spring-web-reactive/src/main/java/org/springframework/reactive/web/http/ServerHttpResponse.java index 7ff7c6af5f..a485b898eb 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/ServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/ServerHttpResponse.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.reactive.web; +package org.springframework.reactive.web.http; import org.reactivestreams.Publisher; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/rxnetty/RequestHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RequestHandlerAdapter.java similarity index 87% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/rxnetty/RequestHandlerAdapter.java rename to spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RequestHandlerAdapter.java index 2a7b7d7413..ec79fa3b4e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/rxnetty/RequestHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RequestHandlerAdapter.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.reactive.web.rxnetty; +package org.springframework.reactive.web.http.rxnetty; import io.netty.buffer.ByteBuf; import io.reactivex.netty.protocol.http.server.HttpServerRequest; @@ -23,7 +23,7 @@ import org.reactivestreams.Publisher; import rx.Observable; import rx.RxReactiveStreams; -import org.springframework.reactive.web.HttpHandler; +import org.springframework.reactive.web.http.ServerHttpHandler; import org.springframework.util.Assert; /** @@ -31,10 +31,10 @@ import org.springframework.util.Assert; */ public class RequestHandlerAdapter implements RequestHandler { - private final HttpHandler httpHandler; + private final ServerHttpHandler httpHandler; - public RequestHandlerAdapter(HttpHandler httpHandler) { + public RequestHandlerAdapter(ServerHttpHandler httpHandler) { Assert.notNull(httpHandler, "'httpHandler' is required."); this.httpHandler = httpHandler; } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/rxnetty/RxNettyServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpRequest.java similarity index 94% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/rxnetty/RxNettyServerHttpRequest.java rename to spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpRequest.java index 5dd01a400f..4c80e78c50 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/rxnetty/RxNettyServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpRequest.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.reactive.web.rxnetty; +package org.springframework.reactive.web.http.rxnetty; import java.net.URI; import java.net.URISyntaxException; @@ -25,7 +25,7 @@ import rx.Observable; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; -import org.springframework.reactive.web.ServerHttpRequest; +import org.springframework.reactive.web.http.ServerHttpRequest; import org.springframework.util.Assert; /** diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/rxnetty/RxNettyServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpResponse.java similarity index 94% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/rxnetty/RxNettyServerHttpResponse.java rename to spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpResponse.java index d40c07463a..9c41996801 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/rxnetty/RxNettyServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpResponse.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.reactive.web.rxnetty; +package org.springframework.reactive.web.http.rxnetty; import io.netty.handler.codec.http.HttpResponseStatus; import io.reactivex.netty.protocol.http.server.HttpServerResponse; @@ -23,7 +23,7 @@ import rx.RxReactiveStreams; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; -import org.springframework.reactive.web.ServerHttpResponse; +import org.springframework.reactive.web.http.ServerHttpResponse; import org.springframework.util.Assert; /** diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/AsyncContextSynchronizer.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/AsyncContextSynchronizer.java similarity index 98% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/AsyncContextSynchronizer.java rename to spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/AsyncContextSynchronizer.java index cbb0427e9a..064a12456c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/AsyncContextSynchronizer.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/AsyncContextSynchronizer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.web.servlet; +package org.springframework.reactive.web.http.servlet; import java.io.IOException; import java.util.concurrent.atomic.AtomicInteger; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/HttpHandlerServlet.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/HttpHandlerServlet.java similarity index 93% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/HttpHandlerServlet.java rename to spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/HttpHandlerServlet.java index 31ff0a7f17..a43a21ceed 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/HttpHandlerServlet.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/HttpHandlerServlet.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.web.servlet; +package org.springframework.reactive.web.http.servlet; import java.io.IOException; import javax.servlet.AsyncContext; @@ -29,7 +29,7 @@ import org.apache.commons.logging.LogFactory; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; -import org.springframework.reactive.web.HttpHandler; +import org.springframework.reactive.web.http.ServerHttpHandler; /** * @author Arjen Poutsma @@ -43,10 +43,10 @@ public class HttpHandlerServlet extends HttpServlet { private static Log logger = LogFactory.getLog(HttpHandlerServlet.class); - private HttpHandler handler; + private ServerHttpHandler handler; - public void setHandler(HttpHandler handler) { + public void setHandler(ServerHttpHandler handler) { this.handler = handler; } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/RequestBodyPublisher.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/RequestBodyPublisher.java similarity index 98% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/RequestBodyPublisher.java rename to spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/RequestBodyPublisher.java index 05bf19828c..385efe326e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/RequestBodyPublisher.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/RequestBodyPublisher.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.web.servlet; +package org.springframework.reactive.web.http.servlet; import java.io.IOException; import java.nio.charset.Charset; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/ResponseBodySubscriber.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ResponseBodySubscriber.java similarity index 97% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/ResponseBodySubscriber.java rename to spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ResponseBodySubscriber.java index c4a6eefd37..39aaf86974 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/ResponseBodySubscriber.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ResponseBodySubscriber.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.web.servlet; +package org.springframework.reactive.web.http.servlet; import java.io.IOException; import javax.servlet.ServletOutputStream; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/ServletServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpRequest.java similarity index 97% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/ServletServerHttpRequest.java rename to spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpRequest.java index f2b1596a30..484d8bd0d8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/ServletServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpRequest.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.reactive.web.servlet; +package org.springframework.reactive.web.http.servlet; import java.net.URI; import java.net.URISyntaxException; @@ -28,7 +28,7 @@ import org.reactivestreams.Publisher; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; -import org.springframework.reactive.web.ServerHttpRequest; +import org.springframework.reactive.web.http.ServerHttpRequest; import org.springframework.util.Assert; import org.springframework.util.LinkedCaseInsensitiveMap; import org.springframework.util.StringUtils; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/ServletServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpResponse.java similarity index 96% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/ServletServerHttpResponse.java rename to spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpResponse.java index 6b8b42c536..f4c43fada8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/servlet/ServletServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpResponse.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.reactive.web.servlet; +package org.springframework.reactive.web.http.servlet; import java.util.List; import java.util.Map; @@ -23,7 +23,7 @@ import org.reactivestreams.Publisher; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; -import org.springframework.reactive.web.ServerHttpResponse; +import org.springframework.reactive.web.http.ServerHttpResponse; import org.springframework.util.Assert; /** diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/DispatcherApp.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/DispatcherApp.java similarity index 91% rename from spring-web-reactive/src/test/java/org/springframework/reactive/web/DispatcherApp.java rename to spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/DispatcherApp.java index 6fac51f692..5004d620b6 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/DispatcherApp.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/DispatcherApp.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.reactive.web; +package org.springframework.reactive.web.dispatch; import java.nio.charset.Charset; import java.util.HashMap; @@ -25,7 +25,9 @@ import org.reactivestreams.Publisher; import reactor.rx.Streams; import org.springframework.http.MediaType; -import org.springframework.reactive.web.rxnetty.RequestHandlerAdapter; +import org.springframework.reactive.web.http.ServerHttpRequest; +import org.springframework.reactive.web.http.ServerHttpResponse; +import org.springframework.reactive.web.http.rxnetty.RequestHandlerAdapter; import org.springframework.web.context.support.StaticWebApplicationContext; /** @@ -44,7 +46,7 @@ public class DispatcherApp { SimpleUrlHandlerMapping handlerMapping = wac.getBean(SimpleUrlHandlerMapping.class); handlerMapping.addHandler("/text", new HelloWorldTextHandler()); - DispatcherHttpHandler dispatcherHandler = new DispatcherHttpHandler(); + DispatcherHandler dispatcherHandler = new DispatcherHandler(); dispatcherHandler.initStrategies(wac); RequestHandlerAdapter requestHandler = new RequestHandlerAdapter(dispatcherHandler); diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/AbstractHttpHandlerIntegrationTestCase.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/AbstractHttpHandlerIntegrationTestCase.java similarity index 98% rename from spring-web-reactive/src/test/java/org/springframework/reactive/web/AbstractHttpHandlerIntegrationTestCase.java rename to spring-web-reactive/src/test/java/org/springframework/reactive/web/http/AbstractHttpHandlerIntegrationTestCase.java index d818fde298..ff3694992a 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/AbstractHttpHandlerIntegrationTestCase.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/AbstractHttpHandlerIntegrationTestCase.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.web; +package org.springframework.reactive.web.http; import java.net.URI; import java.util.Random; diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/CountingHttpHandler.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/CountingHandler.java similarity index 89% rename from spring-web-reactive/src/test/java/org/springframework/reactive/web/CountingHttpHandler.java rename to spring-web-reactive/src/test/java/org/springframework/reactive/web/http/CountingHandler.java index e82512b3b5..f25c994072 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/CountingHttpHandler.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/CountingHandler.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.web; +package org.springframework.reactive.web.http; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -25,9 +25,9 @@ import org.reactivestreams.Subscription; /** * @author Arjen Poutsma */ -public class CountingHttpHandler implements HttpHandler { +public class CountingHandler implements ServerHttpHandler { - private static final Log logger = LogFactory.getLog(CountingHttpHandler.class); + private static final Log logger = LogFactory.getLog(CountingHandler.class); @Override public Publisher handle(ServerHttpRequest request, ServerHttpResponse response) { diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/EchoHandler.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/EchoHandler.java similarity index 74% rename from spring-web-reactive/src/test/java/org/springframework/reactive/web/EchoHandler.java rename to spring-web-reactive/src/test/java/org/springframework/reactive/web/http/EchoHandler.java index fd4f94a510..3649b384ed 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/EchoHandler.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/EchoHandler.java @@ -14,14 +14,18 @@ * limitations under the License. */ -package org.springframework.reactive.web; +package org.springframework.reactive.web.http; import org.reactivestreams.Publisher; +import org.springframework.reactive.web.http.ServerHttpHandler; +import org.springframework.reactive.web.http.ServerHttpRequest; +import org.springframework.reactive.web.http.ServerHttpResponse; + /** * @author Arjen Poutsma */ -public class EchoHandler implements HttpHandler { +public class EchoHandler implements ServerHttpHandler { @Override public Publisher handle(ServerHttpRequest request, ServerHttpResponse response) { diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/rxnetty/HttpHandlerRxNettyIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/rxnetty/HttpHandlerRxNettyIntegrationTests.java similarity index 86% rename from spring-web-reactive/src/test/java/org/springframework/reactive/web/rxnetty/HttpHandlerRxNettyIntegrationTests.java rename to spring-web-reactive/src/test/java/org/springframework/reactive/web/http/rxnetty/HttpHandlerRxNettyIntegrationTests.java index eef884033d..8cd0276f22 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/rxnetty/HttpHandlerRxNettyIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/rxnetty/HttpHandlerRxNettyIntegrationTests.java @@ -13,15 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.reactive.web.rxnetty; +package org.springframework.reactive.web.http.rxnetty; import io.netty.buffer.ByteBuf; import io.reactivex.netty.protocol.http.server.HttpServer; import org.junit.AfterClass; import org.junit.BeforeClass; -import org.springframework.reactive.web.EchoHandler; -import org.springframework.reactive.web.AbstractHttpHandlerIntegrationTestCase; +import org.springframework.reactive.web.http.EchoHandler; +import org.springframework.reactive.web.http.AbstractHttpHandlerIntegrationTestCase; /** * @author Rossen Stoyanchev diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/servlet/AsyncContextSynchronizerTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/servlet/AsyncContextSynchronizerTests.java similarity index 96% rename from spring-web-reactive/src/test/java/org/springframework/reactive/web/servlet/AsyncContextSynchronizerTests.java rename to spring-web-reactive/src/test/java/org/springframework/reactive/web/http/servlet/AsyncContextSynchronizerTests.java index 0003f99608..5dc40c306a 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/servlet/AsyncContextSynchronizerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/servlet/AsyncContextSynchronizerTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.web.servlet; +package org.springframework.reactive.web.http.servlet; import javax.servlet.AsyncContext; diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/servlet/HttpHandlerJettyIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/servlet/HttpHandlerJettyIntegrationTests.java similarity index 90% rename from spring-web-reactive/src/test/java/org/springframework/reactive/web/servlet/HttpHandlerJettyIntegrationTests.java rename to spring-web-reactive/src/test/java/org/springframework/reactive/web/http/servlet/HttpHandlerJettyIntegrationTests.java index df1c945b85..f68c4eef8a 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/servlet/HttpHandlerJettyIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/servlet/HttpHandlerJettyIntegrationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.web.servlet; +package org.springframework.reactive.web.http.servlet; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; @@ -23,8 +23,8 @@ import org.eclipse.jetty.servlet.ServletHolder; import org.junit.AfterClass; import org.junit.BeforeClass; -import org.springframework.reactive.web.AbstractHttpHandlerIntegrationTestCase; -import org.springframework.reactive.web.EchoHandler; +import org.springframework.reactive.web.http.AbstractHttpHandlerIntegrationTestCase; +import org.springframework.reactive.web.http.EchoHandler; /** * @author Arjen Poutsma diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/servlet/HttpHandlerTomcatIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/servlet/HttpHandlerTomcatIntegrationTests.java similarity index 90% rename from spring-web-reactive/src/test/java/org/springframework/reactive/web/servlet/HttpHandlerTomcatIntegrationTests.java rename to spring-web-reactive/src/test/java/org/springframework/reactive/web/http/servlet/HttpHandlerTomcatIntegrationTests.java index 3482bfe50a..9c8bb0b6d7 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/servlet/HttpHandlerTomcatIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/servlet/HttpHandlerTomcatIntegrationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.web.servlet; +package org.springframework.reactive.web.http.servlet; import java.io.File; @@ -24,8 +24,8 @@ import org.apache.catalina.startup.Tomcat; import org.junit.AfterClass; import org.junit.BeforeClass; -import org.springframework.reactive.web.AbstractHttpHandlerIntegrationTestCase; -import org.springframework.reactive.web.EchoHandler; +import org.springframework.reactive.web.http.AbstractHttpHandlerIntegrationTestCase; +import org.springframework.reactive.web.http.EchoHandler; /** * @author Arjen Poutsma From d6fd27477174ba0f24479fc4117cac2ec5f935ca Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 24 Aug 2015 16:33:22 -0400 Subject: [PATCH 018/498] Add HttpServer and Jetty/Tomcat/RxNetty implementations This is a refactoring of the existing "echo" integration test with the goal to make it easier to add further integration tests. The HttpServer abstraction is on the test source side for now. --- .../reactive/web/http/CountingHandler.java | 63 ------------- ....java => EchoHandlerIntegrationTests.java} | 51 +++++++--- .../reactive/web/http/HttpServer.java | 31 +++++++ .../reactive/web/http/HttpServerSupport.java | 45 +++++++++ .../reactive/web/http/JettyHttpServer.java | 90 ++++++++++++++++++ .../reactive/web/http/RxNettyHttpServer.java | 71 ++++++++++++++ .../reactive/web/http/TomcatHttpServer.java | 93 +++++++++++++++++++ .../HttpHandlerRxNettyIntegrationTests.java | 46 --------- .../HttpHandlerJettyIntegrationTests.java | 63 ------------- .../HttpHandlerTomcatIntegrationTests.java | 66 ------------- 10 files changed, 370 insertions(+), 249 deletions(-) delete mode 100644 spring-web-reactive/src/test/java/org/springframework/reactive/web/http/CountingHandler.java rename spring-web-reactive/src/test/java/org/springframework/reactive/web/http/{AbstractHttpHandlerIntegrationTestCase.java => EchoHandlerIntegrationTests.java} (65%) create mode 100644 spring-web-reactive/src/test/java/org/springframework/reactive/web/http/HttpServer.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/reactive/web/http/HttpServerSupport.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/reactive/web/http/JettyHttpServer.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/reactive/web/http/RxNettyHttpServer.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/reactive/web/http/TomcatHttpServer.java delete mode 100644 spring-web-reactive/src/test/java/org/springframework/reactive/web/http/rxnetty/HttpHandlerRxNettyIntegrationTests.java delete mode 100644 spring-web-reactive/src/test/java/org/springframework/reactive/web/http/servlet/HttpHandlerJettyIntegrationTests.java delete mode 100644 spring-web-reactive/src/test/java/org/springframework/reactive/web/http/servlet/HttpHandlerTomcatIntegrationTests.java diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/CountingHandler.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/CountingHandler.java deleted file mode 100644 index f25c994072..0000000000 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/CountingHandler.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2002-2015 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.reactive.web.http; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.reactivestreams.Publisher; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; - -/** - * @author Arjen Poutsma - */ -public class CountingHandler implements ServerHttpHandler { - - private static final Log logger = LogFactory.getLog(CountingHandler.class); - - @Override - public Publisher handle(ServerHttpRequest request, ServerHttpResponse response) { - request.getBody().subscribe(new Subscriber() { - private Subscription subscription; - - private int byteCount = 0; - - @Override - public void onSubscribe(Subscription s) { - this.subscription = s; - this.subscription.request(1); - } - - @Override - public void onNext(byte[] bytes) { - byteCount += bytes.length; - this.subscription.request(1); - } - - @Override - public void onError(Throwable t) { - logger.error("CountingHttpHandler Error", t); - } - - @Override - public void onComplete() { - logger.info("Processed " + byteCount + " bytes"); - } - }); - return null; - } -} diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/AbstractHttpHandlerIntegrationTestCase.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/EchoHandlerIntegrationTests.java similarity index 65% rename from spring-web-reactive/src/test/java/org/springframework/reactive/web/http/AbstractHttpHandlerIntegrationTestCase.java rename to spring-web-reactive/src/test/java/org/springframework/reactive/web/http/EchoHandlerIntegrationTests.java index ff3694992a..2195eb5b3b 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/AbstractHttpHandlerIntegrationTestCase.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/EchoHandlerIntegrationTests.java @@ -19,7 +19,11 @@ package org.springframework.reactive.web.http; import java.net.URI; import java.util.Random; +import org.junit.After; +import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; @@ -29,41 +33,67 @@ import org.springframework.web.client.RestTemplate; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; -public abstract class AbstractHttpHandlerIntegrationTestCase { + +@RunWith(Parameterized.class) +public class EchoHandlerIntegrationTests { private static final int REQUEST_SIZE = 4096 * 3; - protected static int port = SocketUtils.findAvailableTcpPort(); + private static int port = SocketUtils.findAvailableTcpPort(); + + + @Parameterized.Parameter(0) + public HttpServer server; private Random rnd = new Random(); + @Parameterized.Parameters(name = "server [{0}]") + public static Object[][] arguments() { + return new Object[][] { + {new JettyHttpServer()}, + {new TomcatHttpServer()}, + {new RxNettyHttpServer()} + }; + } + + + @Before + public void setup() throws Exception { + this.server.setPort(port); + this.server.setHandler(new EchoHandler()); + this.server.afterPropertiesSet(); + this.server.start(); + } + + @After + public void tearDown() throws Exception { + this.server.stop(); + } + + @Test - public void bytes() throws Exception { + public void echoBytes() throws Exception { RestTemplate restTemplate = new RestTemplate(); byte[] body = randomBytes(); - RequestEntity request = RequestEntity.post(new URI(url())).body(body); + RequestEntity request = RequestEntity.post(new URI("http://localhost:" + port)).body(body); ResponseEntity response = restTemplate.exchange(request, byte[].class); assertArrayEquals(body, response.getBody()); } @Test - public void string() throws Exception { + public void echoString() throws Exception { RestTemplate restTemplate = new RestTemplate(); String body = randomString(); - RequestEntity request = RequestEntity.post(new URI(url())).body(body); + RequestEntity request = RequestEntity.post(new URI("http://localhost:" + port)).body(body); ResponseEntity response = restTemplate.exchange(request, String.class); assertEquals(body, response.getBody()); } - protected static String url() { - return "http://localhost:" + port + "/rx"; - } - private String randomString() { StringBuilder builder = new StringBuilder(); int i = 1; @@ -90,5 +120,4 @@ public abstract class AbstractHttpHandlerIntegrationTestCase { return buffer; } - } diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/HttpServer.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/HttpServer.java new file mode 100644 index 0000000000..5562ed03b5 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/HttpServer.java @@ -0,0 +1,31 @@ +/* + * Copyright 2002-2015 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.reactive.web.http; + + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.Lifecycle; + +/** + * @author Rossen Stoyanchev + */ +public interface HttpServer extends InitializingBean, Lifecycle { + + void setPort(int port); + + void setHandler(ServerHttpHandler handler); + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/HttpServerSupport.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/HttpServerSupport.java new file mode 100644 index 0000000000..e9887b4654 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/HttpServerSupport.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2015 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.reactive.web.http; + + +/** + * @author Rossen Stoyanchev + */ +public class HttpServerSupport { + + private int port = -1; + + private ServerHttpHandler httpHandler; + + + public void setPort(int port) { + this.port = port; + } + + public int getPort() { + return this.port; + } + + public void setHandler(ServerHttpHandler handler) { + this.httpHandler = handler; + } + + public ServerHttpHandler getHttpHandler() { + return this.httpHandler; + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/JettyHttpServer.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/JettyHttpServer.java new file mode 100644 index 0000000000..f2f45e6248 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/JettyHttpServer.java @@ -0,0 +1,90 @@ +/* + * Copyright 2002-2015 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.reactive.web.http; + +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.reactive.web.http.servlet.HttpHandlerServlet; +import org.springframework.util.SocketUtils; + +/** + * @author Rossen Stoyanchev + */ +public class JettyHttpServer extends HttpServerSupport implements InitializingBean, HttpServer { + + private Server jettyServer; + + private boolean running; + + + @Override + public boolean isRunning() { + return this.running; + } + + @Override + public void afterPropertiesSet() throws Exception { + + if (getPort() == -1) { + setPort(SocketUtils.findAvailableTcpPort(8080)); + } + + this.jettyServer = new Server(); + + HttpHandlerServlet servlet = new HttpHandlerServlet(); + servlet.setHandler(new EchoHandler()); + ServletHolder servletHolder = new ServletHolder(servlet); + + ServletContextHandler contextHandler = new ServletContextHandler(this.jettyServer, "", false, false); + contextHandler.addServlet(servletHolder, "/"); + + ServerConnector connector = new ServerConnector(this.jettyServer); + connector.setPort(getPort()); + this.jettyServer.addConnector(connector); + } + + @Override + public void start() { + if (!this.running) { + try { + this.running = true; + this.jettyServer.start(); + } + catch (Exception ex) { + throw new IllegalStateException(ex); + } + } + } + + @Override + public void stop() { + if (this.running) { + try { + this.running = false; + jettyServer.stop(); + jettyServer.destroy(); + } + catch (Exception ex) { + throw new IllegalStateException(ex); + } + } + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/RxNettyHttpServer.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/RxNettyHttpServer.java new file mode 100644 index 0000000000..d3eb174c03 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/RxNettyHttpServer.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2015 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.reactive.web.http; + +import io.netty.buffer.ByteBuf; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.reactive.web.http.rxnetty.RequestHandlerAdapter; +import org.springframework.util.Assert; + + +/** + * @author Rossen Stoyanchev + */ +public class RxNettyHttpServer extends HttpServerSupport implements InitializingBean, HttpServer { + + private RequestHandlerAdapter rxNettyHandler; + + private io.reactivex.netty.protocol.http.server.HttpServer rxNettyServer; + + private boolean running; + + + @Override + public boolean isRunning() { + return this.running; + } + + + @Override + public void afterPropertiesSet() throws Exception { + + Assert.notNull(getHttpHandler()); + this.rxNettyHandler = new RequestHandlerAdapter(getHttpHandler()); + + this.rxNettyServer = (getPort() != -1 ? + io.reactivex.netty.protocol.http.server.HttpServer.newServer(getPort()) : + io.reactivex.netty.protocol.http.server.HttpServer.newServer()); + } + + + @Override + public void start() { + if (!this.running) { + this.running = true; + this.rxNettyServer.start(this.rxNettyHandler); + } + } + + @Override + public void stop() { + if (this.running) { + this.running = false; + this.rxNettyServer.shutdown(); + } + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/TomcatHttpServer.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/TomcatHttpServer.java new file mode 100644 index 0000000000..18716d4e97 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/TomcatHttpServer.java @@ -0,0 +1,93 @@ +/* + * Copyright 2002-2015 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.reactive.web.http; + +import java.io.File; + +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.startup.Tomcat; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.reactive.web.http.servlet.HttpHandlerServlet; +import org.springframework.util.Assert; +import org.springframework.util.SocketUtils; + + +/** + * @author Rossen Stoyanchev + */ +public class TomcatHttpServer extends HttpServerSupport implements InitializingBean, HttpServer { + + private Tomcat tomcatServer; + + private boolean running; + + + @Override + public boolean isRunning() { + return this.running; + } + + @Override + public void afterPropertiesSet() throws Exception { + + if (getPort() == -1) { + setPort(SocketUtils.findAvailableTcpPort(8080)); + } + + this.tomcatServer = new Tomcat(); + this.tomcatServer.setPort(getPort()); + + Assert.notNull(getHttpHandler()); + HttpHandlerServlet servlet = new HttpHandlerServlet(); + servlet.setHandler(getHttpHandler()); + + File base = new File(System.getProperty("java.io.tmpdir")); + Context rootContext = tomcatServer.addContext("", base.getAbsolutePath()); + Tomcat.addServlet(rootContext, "httpHandlerServlet", servlet); + rootContext.addServletMapping("/", "httpHandlerServlet"); + } + + + @Override + public void start() { + if (!this.running) { + try { + this.running = true; + this.tomcatServer.start(); + } + catch (LifecycleException ex) { + throw new IllegalStateException(ex); + } + } + } + + @Override + public void stop() { + if (this.running) { + try { + this.running = false; + this.tomcatServer.stop(); + this.tomcatServer.destroy(); + } + catch (LifecycleException ex) { + throw new IllegalStateException(ex); + } + } + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/rxnetty/HttpHandlerRxNettyIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/rxnetty/HttpHandlerRxNettyIntegrationTests.java deleted file mode 100644 index 8cd0276f22..0000000000 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/rxnetty/HttpHandlerRxNettyIntegrationTests.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2002-2015 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.reactive.web.http.rxnetty; - -import io.netty.buffer.ByteBuf; -import io.reactivex.netty.protocol.http.server.HttpServer; -import org.junit.AfterClass; -import org.junit.BeforeClass; - -import org.springframework.reactive.web.http.EchoHandler; -import org.springframework.reactive.web.http.AbstractHttpHandlerIntegrationTestCase; - -/** - * @author Rossen Stoyanchev - */ -public class HttpHandlerRxNettyIntegrationTests extends AbstractHttpHandlerIntegrationTestCase { - - private static HttpServer httpServer; - - - @BeforeClass - public static void startServer() throws Exception { - RequestHandlerAdapter requestHandler = new RequestHandlerAdapter(new EchoHandler()); - httpServer = HttpServer.newServer(port); - httpServer.start(requestHandler::handle); - } - - @AfterClass - public static void stopServer() throws Exception { - httpServer.shutdown(); - } - -} \ No newline at end of file diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/servlet/HttpHandlerJettyIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/servlet/HttpHandlerJettyIntegrationTests.java deleted file mode 100644 index f68c4eef8a..0000000000 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/servlet/HttpHandlerJettyIntegrationTests.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2002-2015 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.reactive.web.http.servlet; - -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.ServerConnector; -import org.eclipse.jetty.servlet.ServletContextHandler; -import org.eclipse.jetty.servlet.ServletHolder; -import org.junit.AfterClass; -import org.junit.BeforeClass; - -import org.springframework.reactive.web.http.AbstractHttpHandlerIntegrationTestCase; -import org.springframework.reactive.web.http.EchoHandler; - -/** - * @author Arjen Poutsma - */ -public class HttpHandlerJettyIntegrationTests - extends AbstractHttpHandlerIntegrationTestCase { - - private static Server jettyServer; - - @BeforeClass - public static void startServer() throws Exception { - jettyServer = new Server(); - ServerConnector connector = new ServerConnector(jettyServer); - connector.setPort(port); - ServletContextHandler handler = new ServletContextHandler(jettyServer, "", false, false); - HttpHandlerServlet servlet = new HttpHandlerServlet(); - servlet.setHandler(new EchoHandler()); - ServletHolder servletHolder = new ServletHolder(servlet); - handler.addServlet(servletHolder, "/rx"); - jettyServer.addConnector(connector); - jettyServer.start(); - } - - @AfterClass - public static void stopServer() throws Exception { - jettyServer.stop(); - jettyServer.destroy(); - } - - public static void main(String[] args) throws Exception { - startServer(); - System.out.println("Jetty running at: " + url()); - - } - -} \ No newline at end of file diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/servlet/HttpHandlerTomcatIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/servlet/HttpHandlerTomcatIntegrationTests.java deleted file mode 100644 index 9c8bb0b6d7..0000000000 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/servlet/HttpHandlerTomcatIntegrationTests.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2002-2015 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.reactive.web.http.servlet; - -import java.io.File; - -import org.apache.catalina.Context; -import org.apache.catalina.LifecycleException; -import org.apache.catalina.startup.Tomcat; -import org.junit.AfterClass; -import org.junit.BeforeClass; - -import org.springframework.reactive.web.http.AbstractHttpHandlerIntegrationTestCase; -import org.springframework.reactive.web.http.EchoHandler; - -/** - * @author Arjen Poutsma - */ -public class HttpHandlerTomcatIntegrationTests extends AbstractHttpHandlerIntegrationTestCase { - - private static Tomcat tomcatServer; - - - @BeforeClass - public static void startServer() throws LifecycleException, InterruptedException { - tomcatServer = new Tomcat(); - tomcatServer.setPort(port); - File base = new File(System.getProperty("java.io.tmpdir")); - Context rootCtx = tomcatServer.addContext("", base.getAbsolutePath()); - - HttpHandlerServlet servlet = new HttpHandlerServlet(); - servlet.setHandler(new EchoHandler()); - - Tomcat.addServlet(rootCtx, "handlerServlet", servlet); - rootCtx.addServletMapping("/rx", "handlerServlet"); - - tomcatServer.start(); - } - - @AfterClass - public static void stopServer() throws Exception { - tomcatServer.stop(); - tomcatServer.destroy(); - } - - public static void main(String[] args) throws Exception { - startServer(); - System.out.println("Tomcat running at: " + url()); - tomcatServer.getServer().await(); - } - -} \ No newline at end of file From bc7a5acd509f1d66039bac742a71d9b1b8474127 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 24 Aug 2015 17:30:50 -0400 Subject: [PATCH 019/498] Add simple URL mapping and handling This commit adds support for simple URL handler mapping (exact path match) and an adapter for the HttpHandler interface to be used to handle the request. The SimpleUrlHandlerMappingIntegrationTests then maps the URLs "/foo" and "/bar" to two different handlers. --- .../web/dispatch/DispatcherHandler.java | 8 +- .../dispatch/handler/HttpHandlerAdapter.java | 53 ++++++++ .../handler/SimpleUrlHandlerMapping.java | 46 +++++++ ...erverHttpHandler.java => HttpHandler.java} | 4 +- .../reactive/web/http/HttpMessage.java | 3 - .../http/rxnetty/RequestHandlerAdapter.java | 6 +- .../web/http/servlet/HttpHandlerServlet.java | 6 +- .../http/servlet/RequestBodyPublisher.java | 9 +- .../servlet/ServletServerHttpRequest.java | 1 - ...mpleUrlHandlerMappingIntegrationTests.java | 113 ++++++++++++++++++ .../AbstractHttpHandlerIntegrationTests.java | 61 ++++++++++ .../reactive/web/http/EchoHandler.java | 6 +- .../web/http/EchoHandlerIntegrationTests.java | 38 +----- .../reactive/web/http/HttpServer.java | 2 +- .../reactive/web/http/HttpServerSupport.java | 6 +- .../reactive/web/http/JettyHttpServer.java | 4 +- 16 files changed, 301 insertions(+), 65 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/handler/HttpHandlerAdapter.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/handler/SimpleUrlHandlerMapping.java rename spring-web-reactive/src/main/java/org/springframework/reactive/web/http/{ServerHttpHandler.java => HttpHandler.java} (83%) create mode 100644 spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/SimpleUrlHandlerMappingIntegrationTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/reactive/web/http/AbstractHttpHandlerIntegrationTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/DispatcherHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/DispatcherHandler.java index 84cb8cfb7f..a7ba0c565b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/DispatcherHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/DispatcherHandler.java @@ -19,21 +19,19 @@ import java.util.ArrayList; import java.util.List; import org.reactivestreams.Publisher; -import reactor.core.reactivestreams.PublisherFactory; -import reactor.core.reactivestreams.SubscriberWithContext; import reactor.rx.Streams; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.context.ApplicationContext; import org.springframework.http.HttpStatus; -import org.springframework.reactive.web.http.ServerHttpHandler; +import org.springframework.reactive.web.http.HttpHandler; import org.springframework.reactive.web.http.ServerHttpRequest; import org.springframework.reactive.web.http.ServerHttpResponse; /** * @author Rossen Stoyanchev */ -public class DispatcherHandler implements ServerHttpHandler { +public class DispatcherHandler implements HttpHandler { private List handlerMappings; @@ -62,7 +60,7 @@ public class DispatcherHandler implements ServerHttpHandler { if (handler == null) { // No exception handling mechanism yet response.setStatusCode(HttpStatus.NOT_FOUND); - return PublisherFactory.forEach(SubscriberWithContext::onComplete); + return Streams.empty(); } HandlerAdapter handlerAdapter = getHandlerAdapter(handler); diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/handler/HttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/handler/HttpHandlerAdapter.java new file mode 100644 index 0000000000..e6a05cbe1c --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/handler/HttpHandlerAdapter.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2015 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.reactive.web.dispatch.handler; + +import org.reactivestreams.Publisher; +import reactor.rx.Streams; + +import org.springframework.reactive.web.dispatch.HandlerAdapter; +import org.springframework.reactive.web.dispatch.HandlerResult; +import org.springframework.reactive.web.http.HttpHandler; +import org.springframework.reactive.web.http.ServerHttpRequest; +import org.springframework.reactive.web.http.ServerHttpResponse; + + +/** + * Support use of {@link HttpHandler} with + * {@link org.springframework.reactive.web.dispatch.DispatcherHandler + * DispatcherHandler} (which implements the same contract). + * The use of {@code DispatcherHandler} this way enables routing requests to + * one of many {@code HttpHandler} instances depending on the configured + * handler mappings. + * + * @author Rossen Stoyanchev + */ +public class HttpHandlerAdapter implements HandlerAdapter { + + + @Override + public boolean supports(Object handler) { + return HttpHandler.class.isAssignableFrom(handler.getClass()); + } + + @Override + public Publisher handle(ServerHttpRequest request, ServerHttpResponse response, Object handler) { + HttpHandler httpHandler = (HttpHandler) handler; + Publisher publisher = httpHandler.handle(request, response); + return Streams.wrap(publisher).map(aVoid -> null); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/handler/SimpleUrlHandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/handler/SimpleUrlHandlerMapping.java new file mode 100644 index 0000000000..64bae7f0ae --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/handler/SimpleUrlHandlerMapping.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2015 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.reactive.web.dispatch.handler; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.reactive.web.dispatch.HandlerMapping; +import org.springframework.reactive.web.http.ServerHttpRequest; + + +/** + * @author Rossen Stoyanchev + */ +public class SimpleUrlHandlerMapping implements HandlerMapping { + + private final Map handlerMap = new HashMap<>(); + + + public void setHandlers(Map handlers) { + this.handlerMap.clear(); + if (handlers != null) { + this.handlerMap.putAll(handlers); + } + } + + + @Override + public Object getHandler(ServerHttpRequest request) { + return this.handlerMap.get(request.getURI().getPath()); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/ServerHttpHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/HttpHandler.java similarity index 83% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/http/ServerHttpHandler.java rename to spring-web-reactive/src/main/java/org/springframework/reactive/web/http/HttpHandler.java index d393e45f3d..77897de4e7 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/ServerHttpHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/HttpHandler.java @@ -18,14 +18,12 @@ package org.springframework.reactive.web.http; import org.reactivestreams.Publisher; -import org.springframework.reactive.web.http.ServerHttpRequest; -import org.springframework.reactive.web.http.ServerHttpResponse; /** * @author Arjen Poutsma * @author Rossen Stoyanchev */ -public interface ServerHttpHandler { +public interface HttpHandler { Publisher handle(ServerHttpRequest request, ServerHttpResponse response); diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/HttpMessage.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/HttpMessage.java index 0c5477c077..a3c6092d31 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/HttpMessage.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/HttpMessage.java @@ -15,10 +15,7 @@ */ package org.springframework.reactive.web.http; -import java.net.URI; - import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; /** * @author Rossen Stoyanchev diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RequestHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RequestHandlerAdapter.java index ec79fa3b4e..0b22686e0d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RequestHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RequestHandlerAdapter.java @@ -23,7 +23,7 @@ import org.reactivestreams.Publisher; import rx.Observable; import rx.RxReactiveStreams; -import org.springframework.reactive.web.http.ServerHttpHandler; +import org.springframework.reactive.web.http.HttpHandler; import org.springframework.util.Assert; /** @@ -31,10 +31,10 @@ import org.springframework.util.Assert; */ public class RequestHandlerAdapter implements RequestHandler { - private final ServerHttpHandler httpHandler; + private final HttpHandler httpHandler; - public RequestHandlerAdapter(ServerHttpHandler httpHandler) { + public RequestHandlerAdapter(HttpHandler httpHandler) { Assert.notNull(httpHandler, "'httpHandler' is required."); this.httpHandler = httpHandler; } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/HttpHandlerServlet.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/HttpHandlerServlet.java index a43a21ceed..b8be362f0c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/HttpHandlerServlet.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/HttpHandlerServlet.java @@ -29,7 +29,7 @@ import org.apache.commons.logging.LogFactory; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; -import org.springframework.reactive.web.http.ServerHttpHandler; +import org.springframework.reactive.web.http.HttpHandler; /** * @author Arjen Poutsma @@ -43,10 +43,10 @@ public class HttpHandlerServlet extends HttpServlet { private static Log logger = LogFactory.getLog(HttpHandlerServlet.class); - private ServerHttpHandler handler; + private HttpHandler handler; - public void setHandler(ServerHttpHandler handler) { + public void setHandler(HttpHandler handler) { this.handler = handler; } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/RequestBodyPublisher.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/RequestBodyPublisher.java index 385efe326e..6c1f44b9d9 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/RequestBodyPublisher.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/RequestBodyPublisher.java @@ -55,7 +55,6 @@ public class RequestBodyPublisher implements ReadListener, Publisher { @Override public void subscribe(Subscriber s) { this.subscriber = s; - this.subscriber.onSubscribe(new RequestBodySubscription()); } @@ -99,13 +98,17 @@ public class RequestBodyPublisher implements ReadListener, Publisher { public void onAllDataRead() throws IOException { logger.debug("All data read"); this.synchronizer.readComplete(); - this.subscriber.onComplete(); + if (this.subscriber != null) { + this.subscriber.onComplete(); + } } @Override public void onError(Throwable t) { logger.error("RequestBodyPublisher Error", t); - this.subscriber.onError(t); + if (this.subscriber != null) { + this.subscriber.onError(t); + } } private class RequestBodySubscription implements Subscription { diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpRequest.java index 484d8bd0d8..583449e235 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpRequest.java @@ -20,7 +20,6 @@ import java.net.URISyntaxException; import java.nio.charset.Charset; import java.util.Enumeration; import java.util.Map; - import javax.servlet.http.HttpServletRequest; import org.reactivestreams.Publisher; diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/SimpleUrlHandlerMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/SimpleUrlHandlerMappingIntegrationTests.java new file mode 100644 index 0000000000..8b3e42a5c6 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/SimpleUrlHandlerMappingIntegrationTests.java @@ -0,0 +1,113 @@ +/* + * Copyright 2002-2015 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.reactive.web.dispatch; + +import java.net.URI; +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Test; +import org.reactivestreams.Publisher; +import reactor.rx.Streams; + +import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.reactive.web.dispatch.handler.HttpHandlerAdapter; +import org.springframework.reactive.web.dispatch.handler.SimpleUrlHandlerMapping; +import org.springframework.reactive.web.http.AbstractHttpHandlerIntegrationTests; +import org.springframework.reactive.web.http.HttpHandler; +import org.springframework.reactive.web.http.ServerHttpRequest; +import org.springframework.reactive.web.http.ServerHttpResponse; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.context.support.StaticWebApplicationContext; + +import static org.junit.Assert.assertArrayEquals; + + +/** + * @author Rossen Stoyanchev + */ +public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandlerIntegrationTests { + + private static final Charset CHARSET = Charset.forName("UTF-8"); + + + @Override + protected HttpHandler createHttpHandler() { + + StaticWebApplicationContext wac = new StaticWebApplicationContext(); + wac.registerSingleton("hm", TestHandlerMapping.class); + wac.registerSingleton("ha", HttpHandlerAdapter.class); + wac.refresh(); + + DispatcherHandler dispatcherHandler = new DispatcherHandler(); + dispatcherHandler.initStrategies(wac); + return dispatcherHandler; + } + + @Test + public void testFoo() throws Exception { + + RestTemplate restTemplate = new RestTemplate(); + + URI url = new URI("http://localhost:" + port + "/foo"); + RequestEntity request = RequestEntity.get(url).build(); + ResponseEntity response = restTemplate.exchange(request, byte[].class); + + assertArrayEquals("foo".getBytes(CHARSET), response.getBody()); + } + + @Test + public void testBar() throws Exception { + + RestTemplate restTemplate = new RestTemplate(); + + URI url = new URI("http://localhost:" + port + "/bar"); + RequestEntity request = RequestEntity.get(url).build(); + ResponseEntity response = restTemplate.exchange(request, byte[].class); + + assertArrayEquals("bar".getBytes(CHARSET), response.getBody()); + } + + + private static class TestHandlerMapping extends SimpleUrlHandlerMapping { + + public TestHandlerMapping() { + Map map = new HashMap<>(); + map.put("/foo", new FooHandler()); + map.put("/bar", new BarHandler()); + setHandlers(map); + } + } + + private static class FooHandler implements HttpHandler { + + @Override + public Publisher handle(ServerHttpRequest request, ServerHttpResponse response) { + return response.writeWith(Streams.just("foo".getBytes(CHARSET))); + } + } + + private static class BarHandler implements HttpHandler { + + @Override + public Publisher handle(ServerHttpRequest request, ServerHttpResponse response) { + return response.writeWith(Streams.just("bar".getBytes(CHARSET))); + } + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/AbstractHttpHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/AbstractHttpHandlerIntegrationTests.java new file mode 100644 index 0000000000..76512ed0c6 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/AbstractHttpHandlerIntegrationTests.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2015 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.reactive.web.http; + +import org.junit.After; +import org.junit.Before; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import org.springframework.util.SocketUtils; + + +@RunWith(Parameterized.class) +public abstract class AbstractHttpHandlerIntegrationTests { + + protected static int port = SocketUtils.findAvailableTcpPort(); + + @Parameterized.Parameter(0) + public HttpServer server; + + + @Parameterized.Parameters(name = "server [{0}]") + public static Object[][] arguments() { + return new Object[][] { + {new JettyHttpServer()}, + {new TomcatHttpServer()}, + {new RxNettyHttpServer()} + }; + } + + + @Before + public void setup() throws Exception { + this.server.setPort(port); + this.server.setHandler(createHttpHandler()); + this.server.afterPropertiesSet(); + this.server.start(); + } + + protected abstract HttpHandler createHttpHandler(); + + @After + public void tearDown() throws Exception { + this.server.stop(); + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/EchoHandler.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/EchoHandler.java index 3649b384ed..a5241cc934 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/EchoHandler.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/EchoHandler.java @@ -18,14 +18,10 @@ package org.springframework.reactive.web.http; import org.reactivestreams.Publisher; -import org.springframework.reactive.web.http.ServerHttpHandler; -import org.springframework.reactive.web.http.ServerHttpRequest; -import org.springframework.reactive.web.http.ServerHttpResponse; - /** * @author Arjen Poutsma */ -public class EchoHandler implements ServerHttpHandler { +public class EchoHandler implements HttpHandler { @Override public Publisher handle(ServerHttpRequest request, ServerHttpResponse response) { diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/EchoHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/EchoHandlerIntegrationTests.java index 2195eb5b3b..270aa1a9da 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/EchoHandlerIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/EchoHandlerIntegrationTests.java @@ -19,56 +19,26 @@ package org.springframework.reactive.web.http; import java.net.URI; import java.util.Random; -import org.junit.After; -import org.junit.Before; import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; -import org.springframework.util.SocketUtils; import org.springframework.web.client.RestTemplate; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; -@RunWith(Parameterized.class) -public class EchoHandlerIntegrationTests { +public class EchoHandlerIntegrationTests extends AbstractHttpHandlerIntegrationTests { private static final int REQUEST_SIZE = 4096 * 3; - private static int port = SocketUtils.findAvailableTcpPort(); - - - @Parameterized.Parameter(0) - public HttpServer server; - private Random rnd = new Random(); - @Parameterized.Parameters(name = "server [{0}]") - public static Object[][] arguments() { - return new Object[][] { - {new JettyHttpServer()}, - {new TomcatHttpServer()}, - {new RxNettyHttpServer()} - }; - } - - - @Before - public void setup() throws Exception { - this.server.setPort(port); - this.server.setHandler(new EchoHandler()); - this.server.afterPropertiesSet(); - this.server.start(); - } - - @After - public void tearDown() throws Exception { - this.server.stop(); + @Override + protected EchoHandler createHttpHandler() { + return new EchoHandler(); } diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/HttpServer.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/HttpServer.java index 5562ed03b5..7d0e436377 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/HttpServer.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/HttpServer.java @@ -26,6 +26,6 @@ public interface HttpServer extends InitializingBean, Lifecycle { void setPort(int port); - void setHandler(ServerHttpHandler handler); + void setHandler(HttpHandler handler); } diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/HttpServerSupport.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/HttpServerSupport.java index e9887b4654..ae5b1a820c 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/HttpServerSupport.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/HttpServerSupport.java @@ -23,7 +23,7 @@ public class HttpServerSupport { private int port = -1; - private ServerHttpHandler httpHandler; + private HttpHandler httpHandler; public void setPort(int port) { @@ -34,11 +34,11 @@ public class HttpServerSupport { return this.port; } - public void setHandler(ServerHttpHandler handler) { + public void setHandler(HttpHandler handler) { this.httpHandler = handler; } - public ServerHttpHandler getHttpHandler() { + public HttpHandler getHttpHandler() { return this.httpHandler; } diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/JettyHttpServer.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/JettyHttpServer.java index f2f45e6248..ce2bc19e8b 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/JettyHttpServer.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/JettyHttpServer.java @@ -22,6 +22,7 @@ import org.eclipse.jetty.servlet.ServletHolder; import org.springframework.beans.factory.InitializingBean; import org.springframework.reactive.web.http.servlet.HttpHandlerServlet; +import org.springframework.util.Assert; import org.springframework.util.SocketUtils; /** @@ -48,8 +49,9 @@ public class JettyHttpServer extends HttpServerSupport implements InitializingBe this.jettyServer = new Server(); + Assert.notNull(getHttpHandler()); HttpHandlerServlet servlet = new HttpHandlerServlet(); - servlet.setHandler(new EchoHandler()); + servlet.setHandler(getHttpHandler()); ServletHolder servletHolder = new ServletHolder(servlet); ServletContextHandler contextHandler = new ServletContextHandler(this.jettyServer, "", false, false); From 9df3dd449554f0ed4485eaed5bee1f1af66f3777 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Tue, 25 Aug 2015 14:39:36 +0200 Subject: [PATCH 020/498] Polish build.gradle --- spring-web-reactive/build.gradle | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index c657c13445..f40272ad40 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -1,6 +1,6 @@ buildscript { repositories { - maven { url 'http://repo.springsource.org/plugins-release' } + maven { url 'https://repo.spring.io/plugins-release' } } dependencies { classpath 'org.springframework.build.gradle:propdeps-plugin:0.0.7' @@ -22,8 +22,7 @@ dependencies { compile "org.springframework:spring-core:4.2.0.RELEASE" compile "org.springframework:spring-web:4.2.0.RELEASE" compile "org.reactivestreams:reactive-streams:1.0.0" - compile "io.projectreactor:reactor-core:2.0.5.RELEASE" - compile "io.projectreactor:reactor-net:2.0.5.RELEASE" + compile "io.projectreactor:reactor-stream:2.0.5.RELEASE" compile "org.slf4j:slf4j-api:1.7.6" compile "ch.qos.logback:logback-classic:1.1.2" From 202825554c022e4262fe3bd20a34faf01ab9d620 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 25 Aug 2015 12:35:43 -0400 Subject: [PATCH 021/498] Add basic support for @RequestMapping Just enough for a test with an @ResponseBody method that accepts an @RequestParam String arg and returning Publisher or String. See RequestMappingIntegrationTests. --- spring-web-reactive/build.gradle | 6 +- .../web/dispatch/DispatcherHandler.java | 43 +++- .../reactive/web/dispatch/HandlerResult.java | 18 +- .../method/HandlerMethodArgumentResolver.java | 32 +++ .../method/InvocableHandlerMethod.java | 184 ++++++++++++++++++ .../RequestMappingHandlerAdapter.java | 79 ++++++++ .../RequestMappingHandlerMapping.java | 91 +++++++++ .../RequestParamArgumentResolver.java | 49 +++++ .../annotation/ResponseBodyResultHandler.java | 99 ++++++++++ .../rxnetty/RxNettyServerHttpResponse.java | 4 +- .../web/http/servlet/HttpHandlerServlet.java | 9 +- .../src/main/resources/log4j.properties | 8 +- .../BlockingByteBufQueuePublisherTests.java | 5 +- .../util/BlockingByteBufQueueTests.java | 4 +- .../reactive/web/dispatch/DispatcherApp.java | 118 ----------- ...mpleUrlHandlerMappingIntegrationTests.java | 17 +- .../RequestMappingIntegrationTests.java | 87 +++++++++ 17 files changed, 703 insertions(+), 150 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/HandlerMethodArgumentResolver.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/InvocableHandlerMethod.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerMapping.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestParamArgumentResolver.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java delete mode 100644 spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/DispatcherApp.java rename spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/{ => handler}/SimpleUrlHandlerMappingIntegrationTests.java (83%) create mode 100644 spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingIntegrationTests.java diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index f40272ad40..07501e6280 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -23,8 +23,7 @@ dependencies { compile "org.springframework:spring-web:4.2.0.RELEASE" compile "org.reactivestreams:reactive-streams:1.0.0" compile "io.projectreactor:reactor-stream:2.0.5.RELEASE" - compile "org.slf4j:slf4j-api:1.7.6" - compile "ch.qos.logback:logback-classic:1.1.2" + compile "commons-logging:commons-logging:1.2" optional "io.reactivex:rxnetty:0.5.0-SNAPSHOT" optional "io.reactivex:rxjava-reactive-streams:1.0.1" @@ -40,7 +39,10 @@ dependencies { testCompile 'org.eclipse.jetty:jetty-server:9.3.2.v20150730' testCompile 'org.eclipse.jetty:jetty-servlet:9.3.2.v20150730' + testCompile "org.slf4j:slf4j-jcl:1.7.12" + testCompile "org.slf4j:jul-to-slf4j:1.7.12" testCompile("log4j:log4j:1.2.16") + testCompile("org.mockito:mockito-core:1.10.19") { exclude group: 'org.hamcrest', module: 'hamcrest-core' } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/DispatcherHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/DispatcherHandler.java index a7ba0c565b..3393e3d463 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/DispatcherHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/DispatcherHandler.java @@ -17,12 +17,18 @@ package org.springframework.reactive.web.dispatch; import java.util.ArrayList; import java.util.List; +import java.util.Map; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.reactivestreams.Publisher; import reactor.rx.Streams; +import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.http.HttpStatus; import org.springframework.reactive.web.http.HttpHandler; import org.springframework.reactive.web.http.ServerHttpRequest; @@ -31,7 +37,10 @@ import org.springframework.reactive.web.http.ServerHttpResponse; /** * @author Rossen Stoyanchev */ -public class DispatcherHandler implements HttpHandler { +public class DispatcherHandler implements HttpHandler, ApplicationContextAware { + + private static final Log logger = LogFactory.getLog(DispatcherHandler.class); + private List handlerMappings; @@ -40,22 +49,40 @@ public class DispatcherHandler implements HttpHandler { private List resultHandlers; + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + initStrategies(applicationContext); + } + protected void initStrategies(ApplicationContext context) { - this.handlerMappings = new ArrayList<>(BeanFactoryUtils.beansOfTypeIncludingAncestors( - context, HandlerMapping.class, true, false).values()); + Map mappingBeans = + BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false); - this.handlerAdapters = new ArrayList<>(BeanFactoryUtils.beansOfTypeIncludingAncestors( - context, HandlerAdapter.class, true, false).values()); + this.handlerMappings = new ArrayList<>(mappingBeans.values()); + AnnotationAwareOrderComparator.sort(this.handlerMappings); - this.resultHandlers = new ArrayList<>(BeanFactoryUtils.beansOfTypeIncludingAncestors( - context, HandlerResultHandler.class, true, false).values()); + Map adapterBeans = + BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerAdapter.class, true, false); + + this.handlerAdapters = new ArrayList<>(adapterBeans.values()); + AnnotationAwareOrderComparator.sort(this.handlerAdapters); + + Map beans = + BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerResultHandler.class, true, false); + + this.resultHandlers = new ArrayList<>(beans.values()); + AnnotationAwareOrderComparator.sort(this.resultHandlers); } @Override public Publisher handle(ServerHttpRequest request, ServerHttpResponse response) { + if (logger.isDebugEnabled()) { + logger.debug("Processing " + request.getMethod() + " request for [" + request.getURI() + "]"); + } + Object handler = getHandler(request); if (handler == null) { // No exception handling mechanism yet @@ -73,7 +100,7 @@ public class DispatcherHandler implements HttpHandler { } } return Streams.fail(new IllegalStateException( - "No HandlerResultHandler for " + result.getReturnValue())); + "No HandlerResultHandler for " + result.getValue())); }); } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerResult.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerResult.java index 29a02c00e8..4a6ec2dc3f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerResult.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerResult.java @@ -15,21 +15,29 @@ */ package org.springframework.reactive.web.dispatch; + /** * @author Rossen Stoyanchev */ public class HandlerResult { - private final Object returnValue; + private final Object handler; + + private final Object value; - public HandlerResult(Object returnValue) { - this.returnValue = returnValue; + public HandlerResult(Object handler, Object value) { + this.handler = handler; + this.value = value; } - public Object getReturnValue() { - return this.returnValue; + public Object getHandler() { + return this.handler; + } + + public Object getValue() { + return this.value; } } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/HandlerMethodArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/HandlerMethodArgumentResolver.java new file mode 100644 index 0000000000..e4d77d3dbc --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/HandlerMethodArgumentResolver.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2015 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.reactive.web.dispatch.method; + + +import org.springframework.core.MethodParameter; +import org.springframework.reactive.web.http.ServerHttpRequest; + + +/** + * @author Rossen Stoyanchev + */ +public interface HandlerMethodArgumentResolver { + + boolean supportsParameter(MethodParameter parameter); + + Object resolveArgument(MethodParameter parameter, ServerHttpRequest request); + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/InvocableHandlerMethod.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/InvocableHandlerMethod.java new file mode 100644 index 0000000000..ab49c6b6e1 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/InvocableHandlerMethod.java @@ -0,0 +1,184 @@ +/* + * Copyright 2002-2015 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.reactive.web.dispatch.method; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.GenericTypeResolver; +import org.springframework.core.MethodParameter; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.reactive.web.http.ServerHttpRequest; +import org.springframework.util.ReflectionUtils; +import org.springframework.web.method.HandlerMethod; + + +/** + * 90% overlap with the existing one in spring-web except for the different + * HandlerMethodArgumentResolver contract. + * + * @author Rossen Stoyanchev + */ +public class InvocableHandlerMethod extends HandlerMethod { + + private List argumentResolvers = new ArrayList<>(); + + private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); + + + public InvocableHandlerMethod(HandlerMethod handlerMethod) { + super(handlerMethod); + } + + + public void setHandlerMethodArgumentResolvers(List resolvers) { + this.argumentResolvers.clear(); + this.argumentResolvers.addAll(resolvers); + } + + + public Object invokeForRequest(ServerHttpRequest request, Object... providedArgs) throws Exception { + Object[] args = getMethodArgumentValues(request, providedArgs); + if (logger.isTraceEnabled()) { + logger.trace("Invoking [" + getBeanType().getSimpleName() + "." + + getMethod().getName() + "] method with arguments " + Arrays.asList(args)); + } + Object returnValue = doInvoke(args); + if (logger.isTraceEnabled()) { + logger.trace("Method [" + getMethod().getName() + "] returned [" + returnValue + "]"); + } + return returnValue; + } + + private Object[] getMethodArgumentValues(ServerHttpRequest request, Object... providedArgs) throws Exception { + MethodParameter[] parameters = getMethodParameters(); + Object[] args = new Object[parameters.length]; + for (int i = 0; i < parameters.length; i++) { + MethodParameter parameter = parameters[i]; + parameter.initParameterNameDiscovery(this.parameterNameDiscoverer); + GenericTypeResolver.resolveParameterType(parameter, getBean().getClass()); + args[i] = resolveProvidedArgument(parameter, providedArgs); + if (args[i] != null) { + continue; + } + for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) { + if (resolver.supportsParameter(parameter)) { + try { + args[i] = resolver.resolveArgument(parameter, request); + break; + } + catch (Exception ex) { + if (logger.isDebugEnabled()) { + logger.debug(getArgumentResolutionErrorMessage("Error resolving argument", i), ex); + } + throw ex; + } + } + } + if (args[i] == null) { + String msg = getArgumentResolutionErrorMessage("No suitable resolver for argument", i); + throw new IllegalStateException(msg); + } + } + return args; + } + + private String getArgumentResolutionErrorMessage(String message, int index) { + MethodParameter param = getMethodParameters()[index]; + message += " [" + index + "] [type=" + param.getParameterType().getName() + "]"; + return getDetailedErrorMessage(message); + } + + protected String getDetailedErrorMessage(String message) { + return message + "\n" + "HandlerMethod details: \n" + + "Controller [" + getBeanType().getName() + "]\n" + + "Method [" + getBridgedMethod().toGenericString() + "]\n"; + } + + private Object resolveProvidedArgument(MethodParameter parameter, Object... providedArgs) { + if (providedArgs == null) { + return null; + } + for (Object providedArg : providedArgs) { + if (parameter.getParameterType().isInstance(providedArg)) { + return providedArg; + } + } + return null; + } + + protected Object doInvoke(Object... args) throws Exception { + ReflectionUtils.makeAccessible(getBridgedMethod()); + try { + return getBridgedMethod().invoke(getBean(), args); + } + catch (IllegalArgumentException ex) { + assertTargetBean(getBridgedMethod(), getBean(), args); + throw new IllegalStateException(getInvocationErrorMessage(ex.getMessage(), args), ex); + } + catch (InvocationTargetException ex) { + // Unwrap for HandlerExceptionResolvers ... + Throwable targetException = ex.getTargetException(); + if (targetException instanceof RuntimeException) { + throw (RuntimeException) targetException; + } + else if (targetException instanceof Error) { + throw (Error) targetException; + } + else if (targetException instanceof Exception) { + throw (Exception) targetException; + } + else { + String msg = getInvocationErrorMessage("Failed to invoke controller method", args); + throw new IllegalStateException(msg, targetException); + } + } + } + + private void assertTargetBean(Method method, Object targetBean, Object[] args) { + Class methodDeclaringClass = method.getDeclaringClass(); + Class targetBeanClass = targetBean.getClass(); + if (!methodDeclaringClass.isAssignableFrom(targetBeanClass)) { + String msg = "The mapped controller method class '" + methodDeclaringClass.getName() + + "' is not an instance of the actual controller bean instance '" + + targetBeanClass.getName() + "'. If the controller requires proxying " + + "(e.g. due to @Transactional), please use class-based proxying."; + throw new IllegalStateException(getInvocationErrorMessage(msg, args)); + } + } + + private String getInvocationErrorMessage(String message, Object[] resolvedArgs) { + StringBuilder sb = new StringBuilder(getDetailedErrorMessage(message)); + sb.append("Resolved arguments: \n"); + for (int i=0; i < resolvedArgs.length; i++) { + sb.append("[").append(i).append("] "); + if (resolvedArgs[i] == null) { + sb.append("[null] \n"); + } + else { + sb.append("[type=").append(resolvedArgs[i].getClass().getName()).append("] "); + sb.append("[value=").append(resolvedArgs[i]).append("]\n"); + } + } + return sb.toString(); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java new file mode 100644 index 0000000000..d57e9e9413 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java @@ -0,0 +1,79 @@ +/* + * Copyright 2002-2015 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.reactive.web.dispatch.method.annotation; + +import java.util.ArrayList; +import java.util.List; + +import org.reactivestreams.Publisher; +import reactor.rx.Streams; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.reactive.web.dispatch.HandlerAdapter; +import org.springframework.reactive.web.dispatch.HandlerResult; +import org.springframework.reactive.web.dispatch.method.HandlerMethodArgumentResolver; +import org.springframework.reactive.web.dispatch.method.InvocableHandlerMethod; +import org.springframework.reactive.web.http.ServerHttpRequest; +import org.springframework.reactive.web.http.ServerHttpResponse; +import org.springframework.web.method.HandlerMethod; + + +/** + * @author Rossen Stoyanchev + */ +public class RequestMappingHandlerAdapter implements HandlerAdapter, InitializingBean { + + private List argumentResolvers; + + + public void setHandlerMethodArgumentResolvers(List resolvers) { + this.argumentResolvers.clear(); + this.argumentResolvers.addAll(resolvers); + } + + + @Override + public void afterPropertiesSet() throws Exception { + if (this.argumentResolvers == null) { + this.argumentResolvers = new ArrayList<>(); + this.argumentResolvers.add(new RequestParamArgumentResolver()); + } + } + + @Override + public boolean supports(Object handler) { + return HandlerMethod.class.equals(handler.getClass()); + } + + @Override + public Publisher handle(ServerHttpRequest request, ServerHttpResponse response, + Object handler) { + + final InvocableHandlerMethod invocable = new InvocableHandlerMethod((HandlerMethod) handler); + invocable.setHandlerMethodArgumentResolvers(this.argumentResolvers); + + Object result; + try { + result = invocable.invokeForRequest(request); + } + catch (Exception ex) { + return Streams.fail(ex); + } + + return Streams.just(new HandlerResult(invocable, result)); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerMapping.java new file mode 100644 index 0000000000..76e175742c --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerMapping.java @@ -0,0 +1,91 @@ +/* + * Copyright 2002-2015 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.reactive.web.dispatch.method.annotation; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.reactive.web.dispatch.HandlerMapping; +import org.springframework.reactive.web.http.ServerHttpRequest; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.method.HandlerMethodSelector; + + +/** + * @author Rossen Stoyanchev + */ +public class RequestMappingHandlerMapping implements HandlerMapping, + ApplicationContextAware, InitializingBean { + + private static final Log logger = LogFactory.getLog(RequestMappingHandlerMapping.class); + + + private final Map methodMap = new LinkedHashMap<>(); + + private ApplicationContext applicationContext; + + + @Override + public void setApplicationContext(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + + @Override + public void afterPropertiesSet() throws Exception { + for (Object bean : this.applicationContext.getBeansOfType(Object.class).values()) { + detectHandlerMethods(bean); + } + } + + protected void detectHandlerMethods(final Object bean) { + final Class beanType = bean.getClass(); + if (AnnotationUtils.findAnnotation(beanType, Controller.class) != null) { + HandlerMethodSelector.selectMethods(beanType, method -> { + RequestMapping annotation = AnnotationUtils.findAnnotation(method, RequestMapping.class); + if (annotation != null && annotation.value().length > 0) { + String path = annotation.value()[0]; + HandlerMethod handlerMethod = new HandlerMethod(bean, method); + if (logger.isInfoEnabled()) { + logger.info("Mapped \"" + path + "\" onto " + handlerMethod); + } + methodMap.put(path, handlerMethod); + } + return false; + }); + } + } + + @Override + public Object getHandler(ServerHttpRequest request) { + String path = request.getURI().getPath(); + HandlerMethod handlerMethod = this.methodMap.get(path); + if (logger.isDebugEnabled()) { + logger.debug("Mapped " + path + " to [" + handlerMethod + "]"); + } + return handlerMethod; + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestParamArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestParamArgumentResolver.java new file mode 100644 index 0000000000..e3aaed2238 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestParamArgumentResolver.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2015 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.reactive.web.dispatch.method.annotation; + + +import org.springframework.core.MethodParameter; +import org.springframework.reactive.web.dispatch.method.HandlerMethodArgumentResolver; +import org.springframework.reactive.web.http.ServerHttpRequest; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; + + +/** + * Support {@code @RequestParam} but for query params only. + * + * @author Rossen Stoyanchev + */ +public class RequestParamArgumentResolver implements HandlerMethodArgumentResolver { + + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(RequestParam.class); + } + + + @Override + public Object resolveArgument(MethodParameter param, ServerHttpRequest request) { + RequestParam annotation = param.getParameterAnnotation(RequestParam.class); + String name = (annotation.value().length() != 0 ? annotation.value() : param.getParameterName()); + UriComponents uriComponents = UriComponentsBuilder.fromUri(request.getURI()).build(); + return uriComponents.getQueryParams().getFirst(name); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java new file mode 100644 index 0000000000..21d8ddb0d0 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java @@ -0,0 +1,99 @@ +/* + * Copyright 2002-2015 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.reactive.web.dispatch.method.annotation; + +import java.lang.reflect.Method; +import java.nio.charset.Charset; + +import org.reactivestreams.Publisher; +import reactor.rx.Streams; + +import org.springframework.core.MethodParameter; +import org.springframework.core.Ordered; +import org.springframework.core.ResolvableType; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.http.MediaType; +import org.springframework.reactive.web.dispatch.HandlerResult; +import org.springframework.reactive.web.dispatch.HandlerResultHandler; +import org.springframework.reactive.web.http.ServerHttpRequest; +import org.springframework.reactive.web.http.ServerHttpResponse; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.method.HandlerMethod; + + +/** + * For now a simple {@code String} or {@code Publisher} to + * "text/plain;charset=UTF-8" conversion. + * + * @author Rossen Stoyanchev + */ +public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered { + + private static final Charset UTF_8 = Charset.forName("UTF-8"); + + + private int order = Ordered.LOWEST_PRECEDENCE; + + + public void setOrder(int order) { + this.order = order; + } + + @Override + public int getOrder() { + return this.order; + } + + + @Override + public boolean supports(HandlerResult result) { + Object handler = result.getHandler(); + if (handler instanceof HandlerMethod) { + Method method = ((HandlerMethod) handler).getMethod(); + return AnnotatedElementUtils.isAnnotated(method, ResponseBody.class.getName()); + } + return false; + } + + @Override + public Publisher handleResult(ServerHttpRequest request, ServerHttpResponse response, + HandlerResult result) { + + Object value = result.getValue(); + HandlerMethod handlerMethod = (HandlerMethod) result.getHandler(); + MethodParameter returnType = handlerMethod.getReturnValueType(value); + + if (value == null) { + return Streams.empty(); + } + + if (value instanceof String) { + response.getHeaders().setContentType(new MediaType("text", "plain", UTF_8)); + return response.writeWith(Streams.just(((String) value).getBytes(UTF_8))); + } + else if (value instanceof Publisher) { + Class type = ResolvableType.forMethodParameter(returnType).resolveGeneric(0); + if (String.class.equals(type)) { + @SuppressWarnings("unchecked") + Publisher content = (Publisher) value; + return response.writeWith(Streams.wrap(content).map(value1 -> value1.getBytes(UTF_8))); + } + } + + return Streams.fail(new IllegalStateException("Return value type not supported: " + returnType)); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpResponse.java index 9c41996801..ca41e01e55 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpResponse.java @@ -65,7 +65,9 @@ public class RxNettyServerHttpResponse implements ServerHttpResponse { private void writeHeaders() { if (!this.headersWritten) { for (String name : this.headers.keySet()) { - this.response.setHeader(name, this.headers.get(name)); + for (String value : this.headers.get(name)) { + this.response.addHeader(name, value); + } } } } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/HttpHandlerServlet.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/HttpHandlerServlet.java index b8be362f0c..98fcb61abe 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/HttpHandlerServlet.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/HttpHandlerServlet.java @@ -29,6 +29,7 @@ import org.apache.commons.logging.LogFactory; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; +import org.springframework.http.HttpStatus; import org.springframework.reactive.web.http.HttpHandler; /** @@ -66,7 +67,7 @@ public class HttpHandlerServlet extends HttpServlet { response.getOutputStream().setWriteListener(responseSubscriber); ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response, responseSubscriber); - HandlerResultSubscriber resultSubscriber = new HandlerResultSubscriber(contextSynchronizer); + HandlerResultSubscriber resultSubscriber = new HandlerResultSubscriber(contextSynchronizer, httpResponse); this.handler.handle(httpRequest, httpResponse).subscribe(resultSubscriber); } @@ -75,9 +76,12 @@ public class HttpHandlerServlet extends HttpServlet { private final AsyncContextSynchronizer synchronizer; + private final ServletServerHttpResponse response; - public HandlerResultSubscriber(AsyncContextSynchronizer synchronizer) { + + public HandlerResultSubscriber(AsyncContextSynchronizer synchronizer, ServletServerHttpResponse response) { this.synchronizer = synchronizer; + this.response = response; } @@ -94,6 +98,7 @@ public class HttpHandlerServlet extends HttpServlet { @Override public void onError(Throwable ex) { logger.error("Error from request handling. Completing the request.", ex); + this.response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR); this.synchronizer.complete(); } diff --git a/spring-web-reactive/src/main/resources/log4j.properties b/spring-web-reactive/src/main/resources/log4j.properties index a8eb1d4774..8e3253db66 100644 --- a/spring-web-reactive/src/main/resources/log4j.properties +++ b/spring-web-reactive/src/main/resources/log4j.properties @@ -1,6 +1,8 @@ -log4j.rootCategory=INFO, stdout -log4j.logger.org.springframework.rx=DEBUG +log4j.rootCategory=WARN, stdout + +log4j.logger.org.springframework.reactive=DEBUG +log4j.logger.org.springframework.web=DEBUG log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout -log4j.appender.stdout.layout.ConversionPattern=%d %p [%25.25c{1}] <%t> - %m%n \ No newline at end of file +log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] <%t> - %m%n \ No newline at end of file diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/util/BlockingByteBufQueuePublisherTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/util/BlockingByteBufQueuePublisherTests.java index 6db97850ee..6f332c62d8 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/util/BlockingByteBufQueuePublisherTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/util/BlockingByteBufQueuePublisherTests.java @@ -26,7 +26,10 @@ import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; /** * @author Arjen Poutsma diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/util/BlockingByteBufQueueTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/util/BlockingByteBufQueueTests.java index a04b618487..d39772d851 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/util/BlockingByteBufQueueTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/util/BlockingByteBufQueueTests.java @@ -19,7 +19,9 @@ package org.springframework.reactive.util; import org.junit.Before; import org.junit.Test; -import static org.junit.Assert.*; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; /** * @author Arjen Poutsma diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/DispatcherApp.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/DispatcherApp.java deleted file mode 100644 index 5004d620b6..0000000000 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/DispatcherApp.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright 2002-2015 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.reactive.web.dispatch; - -import java.nio.charset.Charset; -import java.util.HashMap; -import java.util.Map; - -import io.netty.buffer.ByteBuf; -import io.reactivex.netty.protocol.http.server.HttpServer; -import org.reactivestreams.Publisher; -import reactor.rx.Streams; - -import org.springframework.http.MediaType; -import org.springframework.reactive.web.http.ServerHttpRequest; -import org.springframework.reactive.web.http.ServerHttpResponse; -import org.springframework.reactive.web.http.rxnetty.RequestHandlerAdapter; -import org.springframework.web.context.support.StaticWebApplicationContext; - -/** - * @author Rossen Stoyanchev - */ -public class DispatcherApp { - - public static void main(String[] args) { - - StaticWebApplicationContext wac = new StaticWebApplicationContext(); - wac.registerSingleton("handlerMapping", SimpleUrlHandlerMapping.class); - wac.registerSingleton("handlerAdapter", PlainTextHandlerAdapter.class); - wac.registerSingleton("resultHandler", PlainTextResultHandler.class); - wac.refresh(); - - SimpleUrlHandlerMapping handlerMapping = wac.getBean(SimpleUrlHandlerMapping.class); - handlerMapping.addHandler("/text", new HelloWorldTextHandler()); - - DispatcherHandler dispatcherHandler = new DispatcherHandler(); - dispatcherHandler.initStrategies(wac); - - RequestHandlerAdapter requestHandler = new RequestHandlerAdapter(dispatcherHandler); - HttpServer server = HttpServer.newServer(8080); - server.start(requestHandler::handle); - server.awaitShutdown(); - } - - - private static class SimpleUrlHandlerMapping implements HandlerMapping { - - private final Map handlerMap = new HashMap<>(); - - - public void addHandler(String path, Object handler) { - this.handlerMap.put(path, handler); - } - - @Override - public Object getHandler(ServerHttpRequest request) { - return this.handlerMap.get(request.getURI().getPath()); - } - } - - private interface PlainTextHandler { - - Publisher handle(ServerHttpRequest request, ServerHttpResponse response); - - } - - private static class HelloWorldTextHandler implements PlainTextHandler { - - @Override - public Publisher handle(ServerHttpRequest request, ServerHttpResponse response) { - return Streams.just("Hello world."); - } - } - - private static class PlainTextHandlerAdapter implements HandlerAdapter { - - @Override - public boolean supports(Object handler) { - return PlainTextHandler.class.isAssignableFrom(handler.getClass()); - } - - @Override - public Publisher handle(ServerHttpRequest request, ServerHttpResponse response, Object handler) { - Publisher publisher = ((PlainTextHandler) handler).handle(request, response); - return Streams.wrap(publisher).map(HandlerResult::new); - } - } - - private static class PlainTextResultHandler implements HandlerResultHandler { - - @Override - public boolean supports(HandlerResult result) { - Object value = result.getReturnValue(); - return (value != null && String.class.equals(value.getClass())); - } - - @Override - public Publisher handleResult(ServerHttpRequest request, ServerHttpResponse response, HandlerResult result) { - response.getHeaders().setContentType(MediaType.TEXT_PLAIN); - byte[] bytes = ((String) result.getReturnValue()).getBytes(Charset.forName("UTF-8")); - return response.writeWith(Streams.just(bytes)); - } - } - -} diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/SimpleUrlHandlerMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/handler/SimpleUrlHandlerMappingIntegrationTests.java similarity index 83% rename from spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/SimpleUrlHandlerMappingIntegrationTests.java rename to spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/handler/SimpleUrlHandlerMappingIntegrationTests.java index 8b3e42a5c6..8bbe9841fb 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/SimpleUrlHandlerMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/handler/SimpleUrlHandlerMappingIntegrationTests.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.reactive.web.dispatch; +package org.springframework.reactive.web.dispatch.handler; import java.net.URI; import java.nio.charset.Charset; @@ -26,8 +26,7 @@ import reactor.rx.Streams; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; -import org.springframework.reactive.web.dispatch.handler.HttpHandlerAdapter; -import org.springframework.reactive.web.dispatch.handler.SimpleUrlHandlerMapping; +import org.springframework.reactive.web.dispatch.DispatcherHandler; import org.springframework.reactive.web.http.AbstractHttpHandlerIntegrationTests; import org.springframework.reactive.web.http.HttpHandler; import org.springframework.reactive.web.http.ServerHttpRequest; @@ -43,7 +42,7 @@ import static org.junit.Assert.assertArrayEquals; */ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandlerIntegrationTests { - private static final Charset CHARSET = Charset.forName("UTF-8"); + private static final Charset UTF_8 = Charset.forName("UTF-8"); @Override @@ -55,7 +54,7 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler wac.refresh(); DispatcherHandler dispatcherHandler = new DispatcherHandler(); - dispatcherHandler.initStrategies(wac); + dispatcherHandler.setApplicationContext(wac); return dispatcherHandler; } @@ -68,7 +67,7 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler RequestEntity request = RequestEntity.get(url).build(); ResponseEntity response = restTemplate.exchange(request, byte[].class); - assertArrayEquals("foo".getBytes(CHARSET), response.getBody()); + assertArrayEquals("foo".getBytes(UTF_8), response.getBody()); } @Test @@ -80,7 +79,7 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler RequestEntity request = RequestEntity.get(url).build(); ResponseEntity response = restTemplate.exchange(request, byte[].class); - assertArrayEquals("bar".getBytes(CHARSET), response.getBody()); + assertArrayEquals("bar".getBytes(UTF_8), response.getBody()); } @@ -98,7 +97,7 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler @Override public Publisher handle(ServerHttpRequest request, ServerHttpResponse response) { - return response.writeWith(Streams.just("foo".getBytes(CHARSET))); + return response.writeWith(Streams.just("foo".getBytes(UTF_8))); } } @@ -106,7 +105,7 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler @Override public Publisher handle(ServerHttpRequest request, ServerHttpResponse response) { - return response.writeWith(Streams.just("bar".getBytes(CHARSET))); + return response.writeWith(Streams.just("bar".getBytes(UTF_8))); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingIntegrationTests.java new file mode 100644 index 0000000000..52f2dacc4f --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingIntegrationTests.java @@ -0,0 +1,87 @@ +/* + * Copyright 2002-2015 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.reactive.web.dispatch.method.annotation; + + +import java.net.URI; +import java.nio.charset.Charset; + +import org.junit.Test; +import org.reactivestreams.Publisher; +import reactor.rx.Streams; + +import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.reactive.web.dispatch.DispatcherHandler; +import org.springframework.reactive.web.http.AbstractHttpHandlerIntegrationTests; +import org.springframework.reactive.web.http.HttpHandler; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.context.support.StaticWebApplicationContext; + +import static org.junit.Assert.assertArrayEquals; + +/** + * @author Rossen Stoyanchev + */ +public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrationTests { + + private static final Charset UTF_8 = Charset.forName("UTF-8"); + + + @Override + protected HttpHandler createHttpHandler() { + + StaticWebApplicationContext wac = new StaticWebApplicationContext(); + wac.registerSingleton("handlerMapping", RequestMappingHandlerMapping.class); + wac.registerSingleton("handlerAdapter", RequestMappingHandlerAdapter.class); + wac.registerSingleton("responseBodyResultHandler", ResponseBodyResultHandler.class); + wac.registerSingleton("controller", TestController.class); + wac.refresh(); + + DispatcherHandler dispatcherHandler = new DispatcherHandler(); + dispatcherHandler.setApplicationContext(wac); + return dispatcherHandler; + } + + @Test + public void helloWithQueryParam() throws Exception { + + RestTemplate restTemplate = new RestTemplate(); + + URI url = new URI("http://localhost:" + port + "/param?name=George"); + RequestEntity request = RequestEntity.get(url).build(); + ResponseEntity response = restTemplate.exchange(request, byte[].class); + + assertArrayEquals("Hello George!".getBytes(UTF_8), response.getBody()); + } + + + @Controller + @SuppressWarnings("unused") + private static class TestController { + + @RequestMapping("/param") + @ResponseBody + public Publisher handleWithParam(@RequestParam String name) { + return Streams.just("Hello ", name, "!"); + } + } + +} From c1f179677aa1fd9bcea736518114453d37039ce6 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Thu, 27 Aug 2015 20:16:23 +0200 Subject: [PATCH 022/498] Polishing --- .../reactive/io/ByteArrayPublisherOutputStream.java | 6 +++--- .../springframework/reactive/util/BlockingSignalQueue.java | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteArrayPublisherOutputStream.java b/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteArrayPublisherOutputStream.java index beda227f88..7cd84c6085 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteArrayPublisherOutputStream.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteArrayPublisherOutputStream.java @@ -10,18 +10,18 @@ import org.springframework.reactive.util.BlockingSignalQueue; /** * {@code OutputStream} implementation that stores all written bytes, to be retrieved - * using {@link #toByteBufPublisher()}. + * using {@link #toByteArrayPublisher()}. * @author Arjen Poutsma */ public class ByteArrayPublisherOutputStream extends OutputStream { - private final BlockingSignalQueue queue = new BlockingSignalQueue(); + private final BlockingSignalQueue queue = new BlockingSignalQueue<>(); /** * Returns the written data as a {@code Publisher}. * @return a publisher for the written bytes */ - public Publisher toByteBufPublisher() { + public Publisher toByteArrayPublisher() { return this.queue.publisher(); } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/util/BlockingSignalQueue.java b/spring-web-reactive/src/main/java/org/springframework/reactive/util/BlockingSignalQueue.java index 67e81601d2..e8406f4dd7 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/util/BlockingSignalQueue.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/util/BlockingSignalQueue.java @@ -26,7 +26,7 @@ import org.reactivestreams.Subscription; import org.springframework.util.Assert; /** - * A {@link BlockingQueue} aimed at working with {@code Publisher} instances. + * A {@link BlockingQueue} aimed at working with {@code Publisher} instances. * Mainly meant to bridge between reactive and non-reactive APIs, such as blocking * streams. * From 0ec29d1c6703b583a06e12138d33955bb30cad5d Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Fri, 4 Sep 2015 13:33:02 +0200 Subject: [PATCH 023/498] Fixed stalling issue in RequestBodyPublisher. --- .../servlet/AsyncContextSynchronizer.java | 3 + .../http/servlet/RequestBodyPublisher.java | 11 ++- .../reactive/web/http/RandomHandler.java | 83 +++++++++++++++++++ .../http/RandomHandlerIntegrationTests.java | 65 +++++++++++++++ 4 files changed, 159 insertions(+), 3 deletions(-) create mode 100644 spring-web-reactive/src/test/java/org/springframework/reactive/web/http/RandomHandler.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/reactive/web/http/RandomHandlerIntegrationTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/AsyncContextSynchronizer.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/AsyncContextSynchronizer.java index 064a12456c..65240b963c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/AsyncContextSynchronizer.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/AsyncContextSynchronizer.java @@ -98,6 +98,9 @@ final class AsyncContextSynchronizer { } } + /** + * Completes both the reading and writing side of the asynchronous operation. + */ public void complete() { readComplete(); writeComplete(); diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/RequestBodyPublisher.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/RequestBodyPublisher.java index 6c1f44b9d9..84769f9943 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/RequestBodyPublisher.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/RequestBodyPublisher.java @@ -47,6 +47,8 @@ public class RequestBodyPublisher implements ReadListener, Publisher { private Subscriber subscriber; + private boolean stalled; + public RequestBodyPublisher(AsyncContextSynchronizer synchronizer, int bufferSize) { this.synchronizer = synchronizer; this.buffer = new byte[bufferSize]; @@ -61,16 +63,18 @@ public class RequestBodyPublisher implements ReadListener, Publisher { @Override public void onDataAvailable() throws IOException { ServletInputStream input = this.synchronizer.getInputStream(); + logger.debug("onDataAvailable: " + input); while (true) { logger.debug("Demand: " + this.demand); if (!demand.hasDemand()) { + stalled = true; break; } boolean ready = input.isReady(); - logger.debug("Input " + ready + "/" + input.isFinished()); + logger.debug("Input ready: " + ready + " finished: " + input.isFinished()); if (!ready) { break; @@ -117,11 +121,12 @@ public class RequestBodyPublisher implements ReadListener, Publisher { public void request(long n) { logger.debug("Updating demand " + demand + " by " + n); - boolean stalled = !demand.hasDemand(); - demand.increase(n); + logger.debug("Stalled: " + stalled); + if (stalled) { + stalled = false; try { onDataAvailable(); } diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/RandomHandler.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/RandomHandler.java new file mode 100644 index 0000000000..111b00b70b --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/RandomHandler.java @@ -0,0 +1,83 @@ +/* + * Copyright 2002-2015 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.reactive.web.http; + +import java.util.Random; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import reactor.rx.Streams; + +import static org.junit.Assert.assertEquals; + +/** + * @author Arjen Poutsma + */ +public class RandomHandler implements HttpHandler { + + private static final Log logger = LogFactory.getLog(RandomHandler.class); + + public static final int RESPONSE_SIZE = 4096 * 3; + + private final Random rnd = new Random(); + + @Override + public Publisher handle(ServerHttpRequest request, ServerHttpResponse response) { + + request.getBody().subscribe(new Subscriber() { + private Subscription s; + + private int requestSize = 0; + + @Override + public void onSubscribe(Subscription s) { + this.s = s; + s.request(Long.MAX_VALUE); + } + + @Override + public void onNext(byte[] bytes) { + requestSize += bytes.length; + } + + @Override + public void onError(Throwable t) { + logger.error(t); + + } + + @Override + public void onComplete() { + logger.debug("Complete"); + assertEquals(RandomHandlerIntegrationTests.REQUEST_SIZE, requestSize); + } + }); + + response.getHeaders().setContentLength(RESPONSE_SIZE); + return response.writeWith(Streams.just(randomBytes())); + } + + private byte[] randomBytes() { + byte[] buffer = new byte[RESPONSE_SIZE]; + rnd.nextBytes(buffer); + return buffer; + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/RandomHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/RandomHandlerIntegrationTests.java new file mode 100644 index 0000000000..3d3e0fdf45 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/RandomHandlerIntegrationTests.java @@ -0,0 +1,65 @@ +/* + * Copyright 2002-2015 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.reactive.web.http; + +import java.net.URI; +import java.util.Random; + +import org.junit.Test; + +import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestTemplate; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class RandomHandlerIntegrationTests extends AbstractHttpHandlerIntegrationTests { + + public static final int REQUEST_SIZE = 4096 * 3; + + private Random rnd = new Random(); + + + @Override + protected RandomHandler createHttpHandler() { + return new RandomHandler(); + } + + + @Test + public void random() throws Exception { + RestTemplate restTemplate = new RestTemplate(); + + byte[] body = randomBytes(); + RequestEntity request = RequestEntity.post(new URI("http://localhost:" + port)).body(body); + ResponseEntity response = restTemplate.exchange(request, byte[].class); + + assertNotNull(response.getBody()); + assertEquals(RandomHandler.RESPONSE_SIZE, + response.getHeaders().getContentLength()); + assertEquals(RandomHandler.RESPONSE_SIZE, response.getBody().length); + } + + + private byte[] randomBytes() { + byte[] buffer = new byte[REQUEST_SIZE]; + rnd.nextBytes(buffer); + return buffer; + } + +} From edb0fcefafd8e094ae89147228c29da3900ca94a Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Mon, 7 Sep 2015 15:53:36 +0200 Subject: [PATCH 024/498] Update RxNetty artifactId to use their latest Snapshot --- spring-web-reactive/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index 07501e6280..03917f319a 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -25,7 +25,7 @@ dependencies { compile "io.projectreactor:reactor-stream:2.0.5.RELEASE" compile "commons-logging:commons-logging:1.2" - optional "io.reactivex:rxnetty:0.5.0-SNAPSHOT" + optional "io.reactivex:rxnetty-http:0.5.0-SNAPSHOT" optional "io.reactivex:rxjava-reactive-streams:1.0.1" provided "javax.servlet:javax.servlet-api:3.1.0" From 357ce225f33f55f99f14a32df7a61341451b0c06 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Mon, 7 Sep 2015 15:54:33 +0200 Subject: [PATCH 025/498] Polishing --- .../io/ByteArrayPublisherInputStream.java | 4 +++- .../io/ByteArrayPublisherOutputStream.java | 16 ++++++++++++++++ ...a => ByteArrayPublisherInputStreamTests.java} | 2 +- 3 files changed, 20 insertions(+), 2 deletions(-) rename spring-web-reactive/src/test/java/org/springframework/reactive/io/{ByteBufPublisherInputStreamTests.java => ByteArrayPublisherInputStreamTests.java} (97%) diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteArrayPublisherInputStream.java b/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteArrayPublisherInputStream.java index 03b81aafa3..4411d953d2 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteArrayPublisherInputStream.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteArrayPublisherInputStream.java @@ -1,4 +1,4 @@ -package org.springframework.reactive.io;/* +/* * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,6 +14,8 @@ package org.springframework.reactive.io;/* * limitations under the License. */ +package org.springframework.reactive.io; + import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteArrayPublisherOutputStream.java b/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteArrayPublisherOutputStream.java index 7cd84c6085..9ba1a3de85 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteArrayPublisherOutputStream.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteArrayPublisherOutputStream.java @@ -1,3 +1,19 @@ +/* + * Copyright 2002-2015 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.reactive.io; import java.io.IOException; diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/io/ByteBufPublisherInputStreamTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/io/ByteArrayPublisherInputStreamTests.java similarity index 97% rename from spring-web-reactive/src/test/java/org/springframework/reactive/io/ByteBufPublisherInputStreamTests.java rename to spring-web-reactive/src/test/java/org/springframework/reactive/io/ByteArrayPublisherInputStreamTests.java index 45ae841646..ae872593a1 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/io/ByteBufPublisherInputStreamTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/io/ByteArrayPublisherInputStreamTests.java @@ -27,7 +27,7 @@ import static org.junit.Assert.assertEquals; /** * @author Arjen Poutsma */ -public class ByteBufPublisherInputStreamTests { +public class ByteArrayPublisherInputStreamTests { private BlockingSignalQueue queue; From e47069775ca0fef2ab147141f8018e0f203ba478 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Tue, 8 Sep 2015 14:23:35 +0200 Subject: [PATCH 026/498] Update dependencies (Spring 4.2.1, Tomcat 8.0.26, Jetty 9.3.3) This commit also polishes build.gradle to use double quotes consistently. --- spring-web-reactive/build.gradle | 37 ++++++++++++++++---------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index 03917f319a..d6e0eaee6a 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -1,29 +1,30 @@ buildscript { repositories { - maven { url 'https://repo.spring.io/plugins-release' } + maven { url "https://repo.spring.io/plugins-release" } } dependencies { - classpath 'org.springframework.build.gradle:propdeps-plugin:0.0.7' + classpath "org.springframework.build.gradle:propdeps-plugin:0.0.7" } } -apply plugin: 'java' -apply plugin: 'propdeps' -apply plugin: 'propdeps-idea' -apply plugin: 'propdeps-maven' +apply plugin: "java" +apply plugin: "propdeps" +apply plugin: "propdeps-idea" +apply plugin: "propdeps-maven" repositories { mavenCentral() - maven { url 'https://oss.jfrog.org/libs-snapshot' } // RxNetty 0.5.x snapshots + maven { url "https://oss.jfrog.org/libs-snapshot" } // RxNetty 0.5.x snapshots } dependencies { - compile "org.springframework:spring-core:4.2.0.RELEASE" - compile "org.springframework:spring-web:4.2.0.RELEASE" + compile "org.springframework:spring-core:4.2.1.RELEASE" + compile "org.springframework:spring-web:4.2.1.RELEASE" compile "org.reactivestreams:reactive-streams:1.0.0" compile "io.projectreactor:reactor-stream:2.0.5.RELEASE" compile "commons-logging:commons-logging:1.2" + compile "com.fasterxml.jackson.core:jackson-databind:2.6.1" optional "io.reactivex:rxnetty-http:0.5.0-SNAPSHOT" optional "io.reactivex:rxjava-reactive-streams:1.0.1" @@ -31,22 +32,20 @@ dependencies { provided "javax.servlet:javax.servlet-api:3.1.0" testCompile "junit:junit:4.12" - testCompile "org.springframework:spring-test:4.2.0.RELEASE" + testCompile "org.springframework:spring-test:4.2.1.RELEASE" - testCompile 'org.apache.tomcat:tomcat-util:8.0.24' - testCompile 'org.apache.tomcat.embed:tomcat-embed-core:8.0.24' + testCompile "org.apache.tomcat:tomcat-util:8.0.26" + testCompile "org.apache.tomcat.embed:tomcat-embed-core:8.0.26" - testCompile 'org.eclipse.jetty:jetty-server:9.3.2.v20150730' - testCompile 'org.eclipse.jetty:jetty-servlet:9.3.2.v20150730' + testCompile "org.eclipse.jetty:jetty-server:9.3.3.v20150827" + testCompile "org.eclipse.jetty:jetty-servlet:9.3.3.v20150827" testCompile "org.slf4j:slf4j-jcl:1.7.12" testCompile "org.slf4j:jul-to-slf4j:1.7.12" - testCompile("log4j:log4j:1.2.16") + testCompile "log4j:log4j:1.2.16" testCompile("org.mockito:mockito-core:1.10.19") { - exclude group: 'org.hamcrest', module: 'hamcrest-core' + exclude group: "org.hamcrest", module: "hamcrest-core" } - testCompile("org.hamcrest:hamcrest-all:1.3") + testCompile "org.hamcrest:hamcrest-all:1.3" } - - From 5bbeb9c204055eb3024ea31d77d86beda609dbf3 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Thu, 10 Sep 2015 11:05:30 +0200 Subject: [PATCH 027/498] Removed BlockingSignalQueue in favor of PublisherSignal. --- .../io/ByteArrayPublisherInputStream.java | 130 +++++++-- .../io/ByteArrayPublisherOutputStream.java | 46 +-- .../reactive/util/BlockingSignalQueue.java | 268 ------------------ .../reactive/util/OnComplete.java | 54 ---- .../reactive/util/OnError.java | 57 ---- .../springframework/reactive/util/OnNext.java | 57 ---- .../reactive/util/PublisherSignal.java | 154 ++++++++++ .../ByteArrayPublisherInputStreamTests.java | 108 +++++++ .../io/ByteBufPublisherInputStreamTests.java | 96 ------- .../BlockingByteBufQueuePublisherTests.java | 231 --------------- .../util/BlockingByteBufQueueTests.java | 78 ----- 11 files changed, 394 insertions(+), 885 deletions(-) delete mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/util/BlockingSignalQueue.java delete mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/util/OnComplete.java delete mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/util/OnError.java delete mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/util/OnNext.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/util/PublisherSignal.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/reactive/io/ByteArrayPublisherInputStreamTests.java delete mode 100644 spring-web-reactive/src/test/java/org/springframework/reactive/io/ByteBufPublisherInputStreamTests.java delete mode 100644 spring-web-reactive/src/test/java/org/springframework/reactive/util/BlockingByteBufQueuePublisherTests.java delete mode 100644 spring-web-reactive/src/test/java/org/springframework/reactive/util/BlockingByteBufQueueTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteArrayPublisherInputStream.java b/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteArrayPublisherInputStream.java index 03b81aafa3..2156f654bf 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteArrayPublisherInputStream.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteArrayPublisherInputStream.java @@ -1,4 +1,4 @@ -package org.springframework.reactive.io;/* +/* * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,57 +14,77 @@ package org.springframework.reactive.io;/* * limitations under the License. */ +package org.springframework.reactive.io; + import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; -import org.springframework.reactive.util.BlockingSignalQueue; +import org.springframework.reactive.util.PublisherSignal; import org.springframework.util.Assert; /** * {@code InputStream} implementation based on a byte array {@link Publisher}. - * * @author Arjen Poutsma */ public class ByteArrayPublisherInputStream extends InputStream { - private final BlockingSignalQueue queue; + private final BlockingQueue> queue = + new LinkedBlockingQueue<>(); private ByteArrayInputStream currentStream; + private boolean completed; + /** * Creates a new {@code ByteArrayPublisherInputStream} based on the given publisher. * @param publisher the publisher to use */ public ByteArrayPublisherInputStream(Publisher publisher) { + this(publisher, 1); + } + + /** + * Creates a new {@code ByteArrayPublisherInputStream} based on the given publisher. + * @param publisher the publisher to use + * @param requestSize the {@linkplain Subscription#request(long) request size} to use + * on the publisher + */ + public ByteArrayPublisherInputStream(Publisher publisher, long requestSize) { Assert.notNull(publisher, "'publisher' must not be null"); - this.queue = new BlockingSignalQueue(); - publisher.subscribe(this.queue.subscriber()); + publisher.subscribe(new BlockingQueueSubscriber(requestSize)); } - ByteArrayPublisherInputStream(BlockingSignalQueue queue) { - Assert.notNull(queue, "'queue' must not be null"); - this.queue = queue; - } @Override public int available() throws IOException { + if (completed) { + return 0; + } InputStream is = currentStream(); return is != null ? is.available() : 0; } @Override public int read() throws IOException { + if (completed) { + return -1; + } InputStream is = currentStream(); while (is != null) { int ch = is.read(); if (ch != -1) { return ch; - } else { + } + else { is = currentStream(); } } @@ -73,6 +93,9 @@ public class ByteArrayPublisherInputStream extends InputStream { @Override public int read(byte[] b, int off, int len) throws IOException { + if (completed) { + return -1; + } InputStream is = currentStream(); if (is == null) { return -1; @@ -105,23 +128,84 @@ public class ByteArrayPublisherInputStream extends InputStream { if (this.currentStream != null && this.currentStream.available() > 0) { return this.currentStream; } - else if (this.queue.isComplete()) { - return null; - } - else if (this.queue.isHeadSignal()) { - byte[] current = this.queue.pollSignal(); - this.currentStream = new ByteArrayInputStream(current); - return this.currentStream; - } - else if (this.queue.isHeadError()) { - Throwable t = this.queue.pollError(); - throw t instanceof IOException ? (IOException) t : new IOException(t); + else { + // take() blocks, but that's OK since this is a *blocking* InputStream + PublisherSignal signal = this.queue.take(); + + if (signal.isData()) { + byte[] data = signal.data(); + this.currentStream = new ByteArrayInputStream(data); + return this.currentStream; + } + else if (signal.isComplete()) { + this.completed = true; + return null; + } + else if (signal.isError()) { + Throwable error = signal.error(); + this.completed = true; + if (error instanceof IOException) { + throw (IOException) error; + } + else { + throw new IOException(error); + } + } } } catch (InterruptedException ex) { Thread.currentThread().interrupt(); } - return null; + throw new IOException(); + } + + private class BlockingQueueSubscriber implements Subscriber { + + private final long requestSize; + + private Subscription subscription; + + public BlockingQueueSubscriber(long requestSize) { + this.requestSize = requestSize; + } + + @Override + public void onSubscribe(Subscription subscription) { + this.subscription = subscription; + + this.subscription.request(this.requestSize); + } + + @Override + public void onNext(byte[] bytes) { + try { + queue.put(PublisherSignal.data(bytes)); + this.subscription.request(requestSize); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + + @Override + public void onError(Throwable t) { + try { + queue.put(PublisherSignal.error(t)); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + + @Override + public void onComplete() { + try { + queue.put(PublisherSignal.complete()); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } } } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteArrayPublisherOutputStream.java b/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteArrayPublisherOutputStream.java index 7cd84c6085..806a13c238 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteArrayPublisherOutputStream.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteArrayPublisherOutputStream.java @@ -1,12 +1,28 @@ +/* + * Copyright 2002-2015 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.reactive.io; import java.io.IOException; import java.io.OutputStream; -import java.util.Arrays; +import java.util.ArrayList; +import java.util.List; import org.reactivestreams.Publisher; - -import org.springframework.reactive.util.BlockingSignalQueue; +import reactor.rx.Streams; /** * {@code OutputStream} implementation that stores all written bytes, to be retrieved @@ -15,14 +31,15 @@ import org.springframework.reactive.util.BlockingSignalQueue; */ public class ByteArrayPublisherOutputStream extends OutputStream { - private final BlockingSignalQueue queue = new BlockingSignalQueue<>(); + private final List buffers = new ArrayList<>(); + /** * Returns the written data as a {@code Publisher}. * @return a publisher for the written bytes */ public Publisher toByteArrayPublisher() { - return this.queue.publisher(); + return Streams.from(buffers); } @Override @@ -32,22 +49,9 @@ public class ByteArrayPublisherOutputStream extends OutputStream { @Override public void write(byte[] b, int off, int len) throws IOException { - byte[] copy = Arrays.copyOf(b, len); - try { - this.queue.putSignal(copy); - } - catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - } + byte[] copy = new byte[len - off]; + System.arraycopy(b, off, copy, 0, len); + buffers.add(copy); } - @Override - public void close() throws IOException { - try { - this.queue.complete(); - } - catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - } - } } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/util/BlockingSignalQueue.java b/spring-web-reactive/src/main/java/org/springframework/reactive/util/BlockingSignalQueue.java deleted file mode 100644 index e8406f4dd7..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/util/BlockingSignalQueue.java +++ /dev/null @@ -1,268 +0,0 @@ -/* - * Copyright 2002-2015 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.reactive.util; - -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; - -import org.reactivestreams.Publisher; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; - -import org.springframework.util.Assert; - -/** - * A {@link BlockingQueue} aimed at working with {@code Publisher} instances. - * Mainly meant to bridge between reactive and non-reactive APIs, such as blocking - * streams. - * - *

Typically, this class will be used by two threads: one thread to put new elements on - * the stack by calling {@link #putSignal(Object)}, possibly {@link #putError(Throwable)} - * and finally {@link #complete()}. The other thread will read elements by calling {@link - * #isHeadSignal()}/{@link #pollSignal()} and {@link #isHeadError()}/{@link #pollError()}, - * while keeping an eye on {@link #isComplete()}. - * @author Arjen Poutsma - */ -public class BlockingSignalQueue { - - private static final int DEFAULT_REQUEST_SIZE_SUBSCRIBER = 1; - - private final BlockingQueue> queue = new LinkedBlockingQueue>(); - - - /** - * Inserts the specified signal into this queue, waiting if necessary for space to - * become available. - * @param t the signal to add - */ - public void putSignal(T t) throws InterruptedException { - Assert.notNull(t, "'t' must not be null"); - Assert.state(!isComplete(), "Cannot put signal in queue after complete()"); - this.queue.put(new OnNext(t)); - } - - /** - * Inserts the specified error into this queue, waiting if necessary for space to - * become available. - * @param error the error to add - */ - public void putError(Throwable error) throws InterruptedException { - Assert.notNull(error, "'error' must not be null"); - Assert.state(!isComplete(), "Cannot putSignal errors in queue after complete()"); - this.queue.put(new OnError(error)); - } - - /** - * Marks the queue as complete. - */ - @SuppressWarnings("unchecked") - public void complete() throws InterruptedException { - this.queue.put(OnComplete.INSTANCE); - } - - /** - * Indicates whether the current head of this queue is a signal. - * @return {@code true} if the current head is a signal; {@code false} otherwise - */ - public boolean isHeadSignal() { - Signal signal = this.queue.peek(); - return signal != null && signal.isOnNext(); - } - - /** - * Indicates whether the current head of this queue is a {@link Throwable}. - * @return {@code true} if the current head is an error; {@code false} otherwise - */ - public boolean isHeadError() { - Signal signal = this.queue.peek(); - return signal != null && signal.isOnError(); - } - - /** - * Indicates whether there are more buffers or errors in this queue. - * @return {@code true} if there more elements in this queue; {@code false} otherwise - */ - public boolean isComplete() { - Signal signal = this.queue.peek(); - return signal != null && signal.isComplete(); - } - - /** - * Retrieves and removes the signal head of this queue. Should only be called after - * {@link #isHeadSignal()} returns {@code true}. - * @return the head of the queue - * @throws IllegalStateException if the current head of this queue is not a buffer - * @see #isHeadSignal() - */ - public T pollSignal() throws InterruptedException { - Signal signal = this.queue.take(); - return signal != null ? signal.next() : null; - } - - /** - * Retrieves and removes the buffer error of this queue. Should only be called after - * {@link #isHeadError()} returns {@code true}. - * @return the head of the queue, as error - * @throws IllegalStateException if the current head of this queue is not a error - * @see #isHeadError() - */ - public Throwable pollError() throws InterruptedException { - Signal signal = this.queue.take(); - return signal != null ? signal.error() : null; - } - - /** - * Returns a {@code Publisher} backed by this queue. - */ - public Publisher publisher() { - return new BlockingSignalQueuePublisher(); - } - - /** - * Returns a {@code Subscriber} backed by this queue. - */ - public Subscriber subscriber() { - return subscriber(DEFAULT_REQUEST_SIZE_SUBSCRIBER); - } - - /** - * Returns a {@code Subscriber} backed by this queue, with the given request size. - * @see Subscription#request(long) - */ - public Subscriber subscriber(long requestSize) { - return new BlockingSignalQueueSubscriber(requestSize); - } - - private class BlockingSignalQueuePublisher implements Publisher { - - private Subscriber subscriber; - - private final Object subscriberMutex = new Object(); - - @Override - public void subscribe(Subscriber subscriber) { - synchronized (this.subscriberMutex) { - if (this.subscriber != null) { - subscriber.onError( - new IllegalStateException("Only one subscriber allowed")); - } - else { - this.subscriber = subscriber; - final SubscriptionThread thread = new SubscriptionThread(); - this.subscriber.onSubscribe(new Subscription() { - @Override - public void request(long n) { - thread.request(n); - } - - @Override - public void cancel() { - thread.cancel(); - } - }); - thread.start(); - } - } - } - - private class SubscriptionThread extends Thread { - - private final DemandCounter demand = new DemandCounter(); - - @Override - public void run() { - try { - while (!Thread.currentThread().isInterrupted()) { - if (this.demand.hasDemand() && isHeadSignal()) { - subscriber.onNext(pollSignal()); - this.demand.decrement(); - } - else if (isHeadError()) { - subscriber.onError(pollError()); - break; - } - else if (isComplete()) { - subscriber.onComplete(); - break; - } - } - } - catch (InterruptedException ex) { - // Allow thread to exit - } - } - - public void request(long n) { - this.demand.increase(n); - } - - public void cancel() { - interrupt(); - } - } - } - - private class BlockingSignalQueueSubscriber implements Subscriber { - - private final long requestSize; - - private Subscription subscription; - - public BlockingSignalQueueSubscriber(long requestSize) { - this.requestSize = requestSize; - } - - @Override - public void onSubscribe(Subscription subscription) { - this.subscription = subscription; - - this.subscription.request(this.requestSize); - } - - @Override - public void onNext(T t) { - try { - putSignal(t); - } - catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - } - this.subscription.request(requestSize); - } - - @Override - public void onError(Throwable t) { - try { - putError(t); - } - catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - } - this.subscription.request(requestSize); - } - - @Override - public void onComplete() { - try { - complete(); - } - catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - } - } - } -} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/util/OnComplete.java b/spring-web-reactive/src/main/java/org/springframework/reactive/util/OnComplete.java deleted file mode 100644 index 47aeb326a8..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/util/OnComplete.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2002-2015 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.reactive.util; - -/** - * @author Arjen Poutsma - */ -class OnComplete implements Signal { - - public static final OnComplete INSTANCE = new OnComplete(); - - private OnComplete() { - } - - @Override - public boolean isComplete() { - return true; - } - - @Override - public boolean isOnNext() { - return false; - } - - @Override - public T next() { - throw new IllegalStateException(); - } - - @Override - public boolean isOnError() { - return false; - } - - @Override - public Throwable error() { - throw new IllegalStateException(); - } - -} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/util/OnError.java b/spring-web-reactive/src/main/java/org/springframework/reactive/util/OnError.java deleted file mode 100644 index 0286ac06a1..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/util/OnError.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2002-2015 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.reactive.util; - -import org.springframework.util.Assert; - -/** - * @author Arjen Poutsma - */ -final class OnError implements Signal { - - private final Throwable error; - - public OnError(Throwable error) { - Assert.notNull(error, "'error' must not be null"); - this.error = error; - } - - @Override - public boolean isOnError() { - return true; - } - - @Override - public Throwable error() { - return error; - } - - @Override - public boolean isOnNext() { - return false; - } - - @Override - public T next() { - throw new IllegalStateException(); - } - - @Override - public boolean isComplete() { - return false; - } -} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/util/OnNext.java b/spring-web-reactive/src/main/java/org/springframework/reactive/util/OnNext.java deleted file mode 100644 index f8e48e4b65..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/util/OnNext.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2002-2015 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.reactive.util; - -import org.springframework.util.Assert; - -/** - * @author Arjen Poutsma - */ -class OnNext implements Signal { - - private final T next; - - public OnNext(T next) { - Assert.notNull(next, "'next' must not be null"); - this.next = next; - } - - @Override - public boolean isOnNext() { - return true; - } - - @Override - public T next() { - return next; - } - - @Override - public boolean isOnError() { - return false; - } - - @Override - public Throwable error() { - throw new IllegalStateException(); - } - - @Override - public boolean isComplete() { - return false; - } -} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/util/PublisherSignal.java b/spring-web-reactive/src/main/java/org/springframework/reactive/util/PublisherSignal.java new file mode 100644 index 0000000000..9fdd51c963 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/util/PublisherSignal.java @@ -0,0 +1,154 @@ +/* + * Copyright 2002-2015 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.reactive.util; + +import org.springframework.util.Assert; + +/** + * Represents a signal value object, useful for wrapping signals as published by a {@link + * #Publisher()}. Mostly used to store signals in buffers. + * @author Arjen Poutsma + */ +public abstract class PublisherSignal { + + protected PublisherSignal() { + } + + /** + * Indicates whether this signal is an data signal, i.e. if {@link #data()} can be + * called safely. + * @return {@code true} if this signal contains data; {@code false} otherwise + */ + public boolean isData() { + return false; + } + + /** + * Returns the data contained in this signal. Can only be safely called after {@link + * #isData()} returns {@code true}. + * @return the data + * @throws IllegalStateException if this signal does not contain data + */ + public T data() { + throw new IllegalStateException(); + } + + /** + * Indicates whether this signal is an error signal, i.e. if {@link #error()} can be + * called safely. + * @return {@code true} if this signal contains an error; {@code false} otherwise + */ + public boolean isError() { + return false; + } + + /** + * Returns the error contained in this signal. Can only be safely called after {@link + * #isError()} returns {@code true}. + * @return the error + * @throws IllegalStateException if this signal does not contain an error + */ + public Throwable error() { + throw new IllegalStateException(); + } + + /** + * Indicates whether this signal completes the stream. + * @return {@code true} if this signal completes the stream; {@code false} otherwise + */ + public boolean isComplete() { + return false; + } + + /** + * Creates a new data signal with the given {@code t}. + * @param t the data to base the signal on + * @return the newly created signal + */ + public static PublisherSignal data(T t) { + Assert.notNull(t, "'t' must not be null"); + return new DataSignal<>(t); + } + + /** + * Creates a new error signal with the given {@code Throwable}. + * @param t the exception to base the signal on + * @return the newly created signal + */ + public static PublisherSignal error(Throwable t) { + Assert.notNull(t, "'t' must not be null"); + return new ErrorSignal<>(t); + } + + /** + * Returns the complete signal, typically the last signal in a stream. + */ + @SuppressWarnings("unchecked") + public static PublisherSignal complete() { + return (PublisherSignal)ON_COMPLETE; + } + + private static final class DataSignal extends PublisherSignal { + + private final T data; + + public DataSignal(T data) { + this.data = data; + } + + @Override + public boolean isData() { + return true; + } + + @Override + public T data() { + return data; + } + } + + private static final class ErrorSignal extends PublisherSignal { + + private final Throwable error; + + public ErrorSignal(Throwable error) { + this.error = error; + } + + @Override + public boolean isError() { + return true; + } + + @Override + public Throwable error() { + return error; + } + + } + + @SuppressWarnings("rawtypes") + private static final PublisherSignal ON_COMPLETE = new PublisherSignal() { + + @Override + public boolean isComplete() { + return true; + } + + }; + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/io/ByteArrayPublisherInputStreamTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/io/ByteArrayPublisherInputStreamTests.java new file mode 100644 index 0000000000..134b07b501 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/io/ByteArrayPublisherInputStreamTests.java @@ -0,0 +1,108 @@ +/* + * Copyright 2002-2015 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.reactive.io; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import reactor.rx.Stream; +import reactor.rx.Streams; + +import org.springframework.util.FileCopyUtils; + +import static org.junit.Assert.*; + +/** + * @author Arjen Poutsma + */ +public class ByteArrayPublisherInputStreamTests { + + + private ByteArrayPublisherInputStream is; + + @Before + public void createStream() { + Stream stream = + Streams.just(new byte[]{'a', 'b', 'c'}, new byte[]{'d', 'e'}); + + is = new ByteArrayPublisherInputStream(stream); + } + + @Test + public void reactor() throws Exception { + assertEquals(3, is.available()); + + int ch = is.read(); + assertEquals('a', ch); + ch = is.read(); + assertEquals('b', ch); + ch = is.read(); + assertEquals('c', ch); + + assertEquals(2, is.available()); + ch = is.read(); + assertEquals('d', ch); + ch = is.read(); + assertEquals('e', ch); + + ch = is.read(); + assertEquals(-1, ch); + + assertEquals(0, is.available()); + } + + @Test + public void copy() throws Exception { + ByteArrayPublisherOutputStream os = new ByteArrayPublisherOutputStream(); + + FileCopyUtils.copy(is, os); + + Publisher publisher = os.toByteArrayPublisher(); + + publisher.subscribe(new Subscriber() { + List result = new ArrayList<>(); + + @Override + public void onSubscribe(Subscription s) { + s.request(Long.MAX_VALUE); + } + + @Override + public void onNext(byte[] bytes) { + result.add(bytes); + } + + @Override + public void onError(Throwable t) { + fail(t.getMessage()); + } + + @Override + public void onComplete() { + assertArrayEquals(result.get(0), new byte[]{'a', 'b', 'c'}); + assertArrayEquals(result.get(0), new byte[]{'d', 'e'}); + } + }); + + } + +} \ No newline at end of file diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/io/ByteBufPublisherInputStreamTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/io/ByteBufPublisherInputStreamTests.java deleted file mode 100644 index 45ae841646..0000000000 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/io/ByteBufPublisherInputStreamTests.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright 2002-2015 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.reactive.io; - -import org.junit.Before; -import org.junit.Test; - -import org.springframework.reactive.util.BlockingSignalQueue; - -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; - -/** - * @author Arjen Poutsma - */ -public class ByteBufPublisherInputStreamTests { - - private BlockingSignalQueue queue; - - private ByteArrayPublisherInputStream is; - - - @Before - public void setUp() throws Exception { - queue = new BlockingSignalQueue(); - is = new ByteArrayPublisherInputStream(queue); - - } - - @Test - public void readSingleByte() throws Exception { - queue.putSignal(new byte[]{'a', 'b', 'c'}); - queue.putSignal(new byte[]{'d', 'e', 'f'}); - queue.complete(); - - - int ch = is.read(); - assertEquals('a', ch); - ch = is.read(); - assertEquals('b', ch); - ch = is.read(); - assertEquals('c', ch); - - ch = is.read(); - assertEquals('d', ch); - ch = is.read(); - assertEquals('e', ch); - ch = is.read(); - assertEquals('f', ch); - - ch = is.read(); - assertEquals(-1, ch); - } - - @Test - public void readBytes() throws Exception { - queue.putSignal(new byte[]{'a', 'b', 'c'}); - queue.putSignal(new byte[]{'d', 'e', 'f'}); - queue.complete(); - - byte[] buf = new byte[2]; - int read = this.is.read(buf); - assertEquals(2, read); - assertArrayEquals(new byte[] { 'a', 'b'}, buf); - - read = this.is.read(buf); - assertEquals(1, read); - assertEquals('c', buf[0]); - - read = this.is.read(buf); - assertEquals(2, read); - assertArrayEquals(new byte[] { 'd', 'e'}, buf); - - read = this.is.read(buf); - assertEquals(1, read); - assertEquals('f', buf[0]); - - read = this.is.read(buf); - assertEquals(-1, read); - } - -} \ No newline at end of file diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/util/BlockingByteBufQueuePublisherTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/util/BlockingByteBufQueuePublisherTests.java deleted file mode 100644 index 6f332c62d8..0000000000 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/util/BlockingByteBufQueuePublisherTests.java +++ /dev/null @@ -1,231 +0,0 @@ -/* - * Copyright 2002-2015 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.reactive.util; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; - -import org.junit.Before; -import org.junit.Test; -import org.reactivestreams.Publisher; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -/** - * @author Arjen Poutsma - */ -public class BlockingByteBufQueuePublisherTests { - - private BlockingSignalQueue queue; - - private Publisher publisher; - - @Before - public void setUp() throws Exception { - queue = new BlockingSignalQueue(); - publisher = queue.publisher(); - } - - @Test - public void normal() throws Exception { - byte[] abc = new byte[]{'a', 'b', 'c'}; - byte[] def = new byte[]{'d', 'e', 'f'}; - - queue.putSignal(abc); - queue.putSignal(def); - queue.complete(); - - final AtomicBoolean complete = new AtomicBoolean(false); - final List received = new ArrayList(2); - - publisher.subscribe(new Subscriber() { - private Subscription subscription; - - @Override - public void onSubscribe(Subscription s) { - s.request(1); - this.subscription = s; - } - - @Override - public void onNext(byte[] bytes) { - received.add(bytes); - this.subscription.request(1); - } - - @Override - public void onError(Throwable t) { - fail("onError not expected"); - } - - @Override - public void onComplete() { - complete.set(true); - } - }); - - while (!complete.get()) { - } - - assertEquals(2, received.size()); - assertSame(abc, received.get(0)); - assertSame(def, received.get(1)); - } - - @Test - public void unbounded() throws Exception { - byte[] abc = new byte[]{'a', 'b', 'c'}; - byte[] def = new byte[]{'d', 'e', 'f'}; - - queue.putSignal(abc); - queue.putSignal(def); - queue.complete(); - - final AtomicBoolean complete = new AtomicBoolean(false); - final List received = new ArrayList(2); - - publisher.subscribe(new Subscriber() { - @Override - public void onSubscribe(Subscription s) { - s.request(Long.MAX_VALUE); - } - - @Override - public void onNext(byte[] bytes) { - received.add(bytes); - } - - @Override - public void onError(Throwable t) { - fail("onError not expected"); - } - - @Override - public void onComplete() { - complete.set(true); - } - }); - - while (!complete.get()) { - } - - assertEquals(2, received.size()); - assertSame(abc, received.get(0)); - assertSame(def, received.get(1)); - } - - @Test - public void multipleSubscribe() throws Exception { - publisher.subscribe(new Subscriber() { - @Override - public void onSubscribe(Subscription s) { - - } - - @Override - public void onNext(byte[] bytes) { - - } - - @Override - public void onError(Throwable t) { - - } - - @Override - public void onComplete() { - - } - }); - publisher.subscribe(new Subscriber() { - @Override - public void onSubscribe(Subscription s) { - fail("onSubscribe not expected"); - } - - @Override - public void onNext(byte[] bytes) { - fail("onNext not expected"); - } - - @Override - public void onError(Throwable t) { - assertTrue(t instanceof IllegalStateException); - } - - @Override - public void onComplete() { - fail("onComplete not expected"); - } - }); - } - - @Test - public void cancel() throws Exception { - byte[] abc = new byte[]{'a', 'b', 'c'}; - byte[] def = new byte[]{'d', 'e', 'f'}; - - queue.putSignal(abc); - queue.putSignal(def); - queue.complete(); - - final AtomicBoolean complete = new AtomicBoolean(false); - final List received = new ArrayList(1); - - publisher.subscribe(new Subscriber() { - - private Subscription subscription; - - @Override - public void onSubscribe(Subscription s) { - s.request(1); - this.subscription = s; - } - - @Override - public void onNext(byte[] bytes) { - received.add(bytes); - this.subscription.cancel(); - complete.set(true); - } - - @Override - public void onError(Throwable t) { - fail("onError not expected"); - } - - @Override - public void onComplete() { - } - }); - - while (!complete.get()) { - } - - assertEquals(1, received.size()); - assertSame(abc, received.get(0)); - } - - - -} \ No newline at end of file diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/util/BlockingByteBufQueueTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/util/BlockingByteBufQueueTests.java deleted file mode 100644 index d39772d851..0000000000 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/util/BlockingByteBufQueueTests.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2002-2015 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.reactive.util; - -import org.junit.Before; -import org.junit.Test; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; - -/** - * @author Arjen Poutsma - */ -public class BlockingByteBufQueueTests { - - private BlockingSignalQueue queue; - - @Before - public void setUp() throws Exception { - queue = new BlockingSignalQueue(); - } - - @Test - public void normal() throws Exception { - byte[] abc = new byte[]{'a', 'b', 'c'}; - byte[] def = new byte[]{'d', 'e', 'f'}; - - queue.putSignal(abc); - queue.putSignal(def); - queue.complete(); - - assertTrue(queue.isHeadSignal()); - assertFalse(queue.isHeadError()); - assertSame(abc, queue.pollSignal()); - - assertTrue(queue.isHeadSignal()); - assertFalse(queue.isHeadError()); - assertSame(def, queue.pollSignal()); - - assertTrue(queue.isComplete()); - } - - - @Test - public void error() throws Exception { - byte[] abc = new byte[]{'a', 'b', 'c'}; - Throwable error = new IllegalStateException(); - - queue.putSignal(abc); - queue.putError(error); - queue.complete(); - - assertTrue(queue.isHeadSignal()); - assertFalse(queue.isHeadError()); - assertSame(abc, queue.pollSignal()); - - assertTrue(queue.isHeadError()); - assertFalse(queue.isHeadSignal()); - assertSame(error, queue.pollError()); - - assertTrue(queue.isComplete()); - } -} \ No newline at end of file From 0b19fca73a2e502423dbba71bc7eaf84824b9360 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Thu, 10 Sep 2015 11:16:21 +0200 Subject: [PATCH 028/498] Added cancellation logic to RequestBodyPublisher. --- .../springframework/reactive/util/Signal.java | 33 ------- .../http/servlet/RequestBodyPublisher.java | 29 ++++++- .../reactive/web/http/XMLHandler.java | 81 +++++++++++++++++ .../web/http/XmlHandlerIntegrationTests.java | 87 +++++++++++++++++++ 4 files changed, 195 insertions(+), 35 deletions(-) delete mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/util/Signal.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/reactive/web/http/XMLHandler.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/reactive/web/http/XmlHandlerIntegrationTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/util/Signal.java b/spring-web-reactive/src/main/java/org/springframework/reactive/util/Signal.java deleted file mode 100644 index a0cb153e6e..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/util/Signal.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2002-2015 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.reactive.util; - -/** - * @author Arjen Poutsma - */ -interface Signal { - - boolean isOnNext(); - - T next(); - - boolean isOnError(); - - Throwable error(); - - boolean isComplete(); -} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/RequestBodyPublisher.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/RequestBodyPublisher.java index 84769f9943..35cc27fdcb 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/RequestBodyPublisher.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/RequestBodyPublisher.java @@ -49,19 +49,30 @@ public class RequestBodyPublisher implements ReadListener, Publisher { private boolean stalled; + private boolean cancelled; + public RequestBodyPublisher(AsyncContextSynchronizer synchronizer, int bufferSize) { this.synchronizer = synchronizer; this.buffer = new byte[bufferSize]; } @Override - public void subscribe(Subscriber s) { - this.subscriber = s; + public void subscribe(Subscriber subscriber) { + if (subscriber == null) { + throw new NullPointerException(); + } + else if (this.subscriber != null) { + subscriber.onError(new IllegalStateException("Only one subscriber allowed")); + } + this.subscriber = subscriber; this.subscriber.onSubscribe(new RequestBodySubscription()); } @Override public void onDataAvailable() throws IOException { + if (cancelled) { + return; + } ServletInputStream input = this.synchronizer.getInputStream(); logger.debug("onDataAvailable: " + input); @@ -100,6 +111,9 @@ public class RequestBodyPublisher implements ReadListener, Publisher { @Override public void onAllDataRead() throws IOException { + if (cancelled) { + return; + } logger.debug("All data read"); this.synchronizer.readComplete(); if (this.subscriber != null) { @@ -109,7 +123,11 @@ public class RequestBodyPublisher implements ReadListener, Publisher { @Override public void onError(Throwable t) { + if (cancelled) { + return; + } logger.error("RequestBodyPublisher Error", t); + this.synchronizer.readComplete(); if (this.subscriber != null) { this.subscriber.onError(t); } @@ -119,6 +137,9 @@ public class RequestBodyPublisher implements ReadListener, Publisher { @Override public void request(long n) { + if (cancelled) { + return; + } logger.debug("Updating demand " + demand + " by " + n); demand.increase(n); @@ -138,6 +159,10 @@ public class RequestBodyPublisher implements ReadListener, Publisher { @Override public void cancel() { + if (cancelled) { + return; + } + cancelled = true; synchronizer.readComplete(); demand.reset(); } diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/XMLHandler.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/XMLHandler.java new file mode 100644 index 0000000000..391f6acbff --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/XMLHandler.java @@ -0,0 +1,81 @@ +/* + * Copyright 2002-2015 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.reactive.web.http; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.Marshaller; +import javax.xml.bind.Unmarshaller; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.reactivestreams.Publisher; +import reactor.rx.Streams; + +import org.springframework.http.MediaType; +import org.springframework.reactive.io.ByteArrayPublisherInputStream; +import org.springframework.reactive.io.ByteArrayPublisherOutputStream; + +import static org.junit.Assert.fail; + +/** + * @author Arjen Poutsma + */ +public class XmlHandler implements HttpHandler { + + private static final Log logger = LogFactory.getLog(XmlHandler.class); + + @Override + public Publisher handle(ServerHttpRequest request, + ServerHttpResponse response) { + try { + JAXBContext jaxbContext = JAXBContext.newInstance(XmlHandlerIntegrationTests.Person.class); + Unmarshaller unmarshaller = jaxbContext.createUnmarshaller(); + Marshaller marshaller = jaxbContext.createMarshaller(); + + Runnable r = () -> { + try { + ByteArrayPublisherInputStream bis = new ByteArrayPublisherInputStream(request.getBody()); + + XmlHandlerIntegrationTests.Person johnDoe = + (XmlHandlerIntegrationTests.Person) unmarshaller.unmarshal(bis); + + logger.info("Read: " + johnDoe); + + } + catch (Exception e) { + logger.error(e, e); + } + }; + + Thread t = new Thread(r); + t.start(); + + response.getHeaders().setContentType(MediaType.APPLICATION_XML); + XmlHandlerIntegrationTests.Person janeDoe = new XmlHandlerIntegrationTests.Person("Jane Doe"); + ByteArrayPublisherOutputStream bos = new ByteArrayPublisherOutputStream(); + marshaller.marshal(janeDoe, bos); + bos.close(); + + return response.writeWith(bos.toByteArrayPublisher()); + } + catch (Exception ex) { + logger.error(ex, ex); + fail(ex.getMessage()); + return null; + } + } +} diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/XmlHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/XmlHandlerIntegrationTests.java new file mode 100644 index 0000000000..d703582ea0 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/XmlHandlerIntegrationTests.java @@ -0,0 +1,87 @@ +/* + * Copyright 2002-2015 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.reactive.web.http; + +import java.net.URI; +import javax.xml.bind.annotation.XmlRootElement; + +import org.junit.Test; + +import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestTemplate; + +/** + * @author Arjen Poutsma + */ +public class XmlHandlerIntegrationTests extends AbstractHttpHandlerIntegrationTests { + + @Override + protected HttpHandler createHttpHandler() { + return new XmlHandler(); + } + + @Test + public void xml() throws Exception { + RestTemplate restTemplate = new RestTemplate(); + + Person johnDoe = new Person("John Doe"); + + RequestEntity request = RequestEntity.post(new URI("http://localhost:" + port)).body( + johnDoe); + ResponseEntity response = restTemplate.exchange(request, Person.class); + System.out.println(response.getBody()); + + + } + + @XmlRootElement + static class Person { + + private String name; + + public Person() { + } + + public Person(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public boolean equals(Object o) { + return name.equals(((Person) o).name); + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public String toString() { + return name; + } + } +} From 01ef90f1ca3ff6f9b8725d2d54483d29770e6731 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Thu, 10 Sep 2015 11:36:14 +0200 Subject: [PATCH 029/498] Fixed tests. --- .../io/ByteArrayPublisherInputStreamTests.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/io/ByteArrayPublisherInputStreamTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/io/ByteArrayPublisherInputStreamTests.java index 134b07b501..362355b089 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/io/ByteArrayPublisherInputStreamTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/io/ByteArrayPublisherInputStreamTests.java @@ -18,6 +18,7 @@ package org.springframework.reactive.io; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; import org.junit.Before; import org.junit.Test; @@ -77,9 +78,10 @@ public class ByteArrayPublisherInputStreamTests { FileCopyUtils.copy(is, os); Publisher publisher = os.toByteArrayPublisher(); + List result = new ArrayList<>(); + AtomicBoolean complete = new AtomicBoolean(); publisher.subscribe(new Subscriber() { - List result = new ArrayList<>(); @Override public void onSubscribe(Subscription s) { @@ -98,11 +100,16 @@ public class ByteArrayPublisherInputStreamTests { @Override public void onComplete() { - assertArrayEquals(result.get(0), new byte[]{'a', 'b', 'c'}); - assertArrayEquals(result.get(0), new byte[]{'d', 'e'}); + complete.set(true); } }); + while (!complete.get()) { + + } + assertArrayEquals(result.get(0), new byte[]{'a', 'b', 'c'}); + assertArrayEquals(result.get(1), new byte[]{'d', 'e'}); + } } \ No newline at end of file From 5ddbbf46731c2e10848ab73996dedae31a187668 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Thu, 10 Sep 2015 13:31:42 +0200 Subject: [PATCH 030/498] Rename XMLHandler.java to XmlHandler.java --- .../reactive/web/http/{XMLHandler.java => XmlHandler.java} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename spring-web-reactive/src/test/java/org/springframework/reactive/web/http/{XMLHandler.java => XmlHandler.java} (100%) diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/XMLHandler.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/XmlHandler.java similarity index 100% rename from spring-web-reactive/src/test/java/org/springframework/reactive/web/http/XMLHandler.java rename to spring-web-reactive/src/test/java/org/springframework/reactive/web/http/XmlHandler.java From 881db0688b5448768e1b3196fe5365a3d41d9342 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Tue, 8 Sep 2015 14:10:18 +0200 Subject: [PATCH 031/498] Add Codec support This commit adds support for Publisher based codecs that allows to convert byte stream to object stream and vice & versa. Jackson, JAXB2 and String codec implementations are provided. --- spring-web-reactive/build.gradle | 1 + .../reactive/codec/CodecException.java | 36 +++ .../codec/decoder/ByteToMessageDecoder.java | 56 ++++ .../codec/decoder/JacksonJsonDecoder.java | 70 +++++ .../reactive/codec/decoder/Jaxb2Decoder.java | 132 +++++++++ .../codec/decoder/JsonObjectDecoder.java | 263 ++++++++++++++++++ .../reactive/codec/decoder/StringDecoder.java | 54 ++++ .../codec/encoder/JacksonJsonEncoder.java | 76 +++++ .../reactive/codec/encoder/Jaxb2Encoder.java | 108 +++++++ .../codec/encoder/JsonObjectEncoder.java | 61 ++++ .../codec/encoder/MessageToByteEncoder.java | 58 ++++ .../reactive/codec/encoder/StringEncoder.java | 53 ++++ .../reactive/codec/support/HintUtils.java | 54 ++++ .../reactive/io/BufferOutputStream.java | 48 ++++ .../reactive/io/ByteBufferInputStream.java | 48 ++++ .../RequestBodyArgumentResolver.java | 134 +++++++++ .../RequestMappingHandlerAdapter.java | 5 + .../annotation/ResponseBodyResultHandler.java | 85 +++++- .../decoder/JacksonJsonDecoderTests.java | 56 ++++ .../codec/decoder/Jaxb2DecoderTests.java | 57 ++++ .../codec/decoder/JsonObjectDecoderTests.java | 62 +++++ .../codec/decoder/StringDecoderTests.java | 57 ++++ .../encoder/JacksonJsonEncoderTests.java | 58 ++++ .../codec/encoder/Jaxb2EncoderTests.java | 60 ++++ .../codec/encoder/JsonObjectEncoderTests.java | 47 ++++ .../codec/encoder/StringEncoderTests.java | 55 ++++ .../reactive/converter/Pojo.java | 55 ++++ .../RequestMappingIntegrationTests.java | 180 +++++++++++- 28 files changed, 2008 insertions(+), 21 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/codec/CodecException.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/ByteToMessageDecoder.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoder.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/Jaxb2Decoder.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JsonObjectDecoder.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/StringDecoder.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoder.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/Jaxb2Encoder.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JsonObjectEncoder.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/MessageToByteEncoder.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/StringEncoder.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/codec/support/HintUtils.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/io/BufferOutputStream.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteBufferInputStream.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoderTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/Jaxb2DecoderTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/JsonObjectDecoderTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/StringDecoderTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoderTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/Jaxb2EncoderTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/JsonObjectEncoderTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/StringEncoderTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/reactive/converter/Pojo.java diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index 07501e6280..2e6c3722eb 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -24,6 +24,7 @@ dependencies { compile "org.reactivestreams:reactive-streams:1.0.0" compile "io.projectreactor:reactor-stream:2.0.5.RELEASE" compile "commons-logging:commons-logging:1.2" + compile "com.fasterxml.jackson.core:jackson-databind:2.6.1" optional "io.reactivex:rxnetty:0.5.0-SNAPSHOT" optional "io.reactivex:rxjava-reactive-streams:1.0.1" diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/CodecException.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/CodecException.java new file mode 100644 index 0000000000..3ed102ea04 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/CodecException.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2015 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.reactive.codec; + +import org.springframework.core.NestedRuntimeException; + +/** + * Codec related exception, usually used as a wrapper for a cause exception. + * + * @author Sebastien Deleuze + */ +public class CodecException extends NestedRuntimeException { + + public CodecException(String msg, Throwable cause) { + super(msg, cause); + } + + public CodecException(String msg) { + super(msg); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/ByteToMessageDecoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/ByteToMessageDecoder.java new file mode 100644 index 0000000000..888bb69571 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/ByteToMessageDecoder.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2015 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.reactive.codec.decoder; + +import java.nio.ByteBuffer; + +import org.reactivestreams.Publisher; + +import org.springframework.core.ResolvableType; +import org.springframework.http.MediaType; +import org.springframework.reactive.codec.encoder.MessageToByteEncoder; + +/** + * Decode from a bytes stream to a message stream. + * + * @author Sebastien Deleuze + * @see MessageToByteEncoder + */ +public interface ByteToMessageDecoder { + + /** + * Indicate whether the given type and media type can be processed by this decoder. + * @param type the (potentially generic) type to ultimately decode to. + * Could be different from {@code T} type. + * @param mediaType the media type to decode from. + * Typically the value of a {@code Content-Type} header for HTTP request. + * @param hints Additional information about how to do decode, optional. + * @return {@code true} if decodable; {@code false} otherwise + */ + boolean canDecode(ResolvableType type, MediaType mediaType, Object... hints); + + /** + * Decode a bytes stream to a message stream. + * @param inputStream the input stream that represent the whole object to decode. + * @param type the (potentially generic) type to ultimately decode to. + * Could be different from {@code T} type. + * @param hints Additional information about how to do decode, optional. + * @return the decoded message stream + */ + Publisher decode(Publisher inputStream, ResolvableType type, MediaType mediaType, Object... hints); + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoder.java new file mode 100644 index 0000000000..150bf33a6e --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoder.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2015 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.reactive.codec.decoder; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; +import org.reactivestreams.Publisher; +import reactor.rx.Streams; + +import org.springframework.core.ResolvableType; +import org.springframework.http.MediaType; +import org.springframework.reactive.codec.CodecException; +import org.springframework.reactive.codec.encoder.JacksonJsonEncoder; +import org.springframework.reactive.io.ByteBufferInputStream; + +/** + * Decode from a bytes stream of JSON objects to a stream of {@code Object} (POJO). + * + * @author Sebastien Deleuze + * @see JacksonJsonEncoder + */ +public class JacksonJsonDecoder implements ByteToMessageDecoder { + + private final ObjectMapper mapper; + + public JacksonJsonDecoder() { + this(new ObjectMapper()); + } + + public JacksonJsonDecoder(ObjectMapper mapper) { + this.mapper = mapper; + } + + @Override + public boolean canDecode(ResolvableType type, MediaType mediaType, Object... hints) { + return mediaType.isCompatibleWith(MediaType.APPLICATION_JSON); + } + + @Override + public Publisher decode(Publisher inputStream, ResolvableType type, MediaType mediaType, Object... hints) { + ObjectReader reader = mapper.readerFor(type.getRawClass()); + return Streams.wrap(inputStream) + .map(chunk -> { + try { + return reader.readValue(new ByteBufferInputStream(chunk)); + } + catch (IOException e) { + throw new CodecException("Error while reading the data", e); + } + }); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/Jaxb2Decoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/Jaxb2Decoder.java new file mode 100644 index 0000000000..462baab1c9 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/Jaxb2Decoder.java @@ -0,0 +1,132 @@ +/* + * Copyright 2002-2015 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.reactive.codec.decoder; + +import java.nio.ByteBuffer; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBElement; +import javax.xml.bind.JAXBException; +import javax.xml.bind.UnmarshalException; +import javax.xml.bind.Unmarshaller; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.transform.Source; +import javax.xml.transform.sax.SAXSource; +import javax.xml.transform.stream.StreamSource; + +import org.reactivestreams.Publisher; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.XMLReader; +import org.xml.sax.helpers.XMLReaderFactory; +import reactor.io.buffer.Buffer; +import reactor.rx.Stream; +import reactor.rx.Streams; + +import org.springframework.core.ResolvableType; +import org.springframework.http.MediaType; +import org.springframework.reactive.codec.CodecException; +import org.springframework.reactive.codec.encoder.Jaxb2Encoder; +import org.springframework.reactive.io.ByteArrayPublisherInputStream; +import org.springframework.util.Assert; + +/** + * Decode from a bytes stream of XML elements to a stream of {@code Object} (POJO). + * + * @author Sebastien Deleuze + * @see Jaxb2Encoder + */ +public class Jaxb2Decoder implements ByteToMessageDecoder { + + private final ConcurrentMap, JAXBContext> jaxbContexts = new ConcurrentHashMap, JAXBContext>(64); + + + @Override + public boolean canDecode(ResolvableType type, MediaType mediaType, Object... hints) { + return mediaType.isCompatibleWith(MediaType.APPLICATION_XML) || mediaType.isCompatibleWith(MediaType.TEXT_XML); + } + + @Override + public Publisher decode(Publisher inputStream, ResolvableType type, MediaType mediaType, Object... hints) { + Stream stream = Streams.wrap(inputStream).map(chunk -> new Buffer(chunk).asBytes()); + Class outputClass = type.getRawClass(); + try { + Source source = processSource(new StreamSource(new ByteArrayPublisherInputStream(stream))); + Unmarshaller unmarshaller = createUnmarshaller(outputClass); + if (outputClass.isAnnotationPresent(XmlRootElement.class)) { + return Streams.just(unmarshaller.unmarshal(source)); + } + else { + JAXBElement jaxbElement = unmarshaller.unmarshal(source, outputClass); + return Streams.just(jaxbElement.getValue()); + } + } + catch (UnmarshalException ex) { + return Streams.fail(new CodecException("Could not unmarshal to [" + outputClass + "]: " + ex.getMessage(), ex)); + } + catch (JAXBException ex) { + return Streams.fail(new CodecException("Could not instantiate JAXBContext: " + ex.getMessage(), ex)); + } + } + + protected Source processSource(Source source) { + if (source instanceof StreamSource) { + StreamSource streamSource = (StreamSource) source; + InputSource inputSource = new InputSource(streamSource.getInputStream()); + try { + XMLReader xmlReader = XMLReaderFactory.createXMLReader(); + return new SAXSource(xmlReader, inputSource); + } + catch (SAXException ex) { + throw new CodecException("Error while processing the source", ex); + } + } + else { + return source; + } + } + + protected final Unmarshaller createUnmarshaller(Class clazz) throws JAXBException { + try { + JAXBContext jaxbContext = getJaxbContext(clazz); + Unmarshaller unmarshaller = jaxbContext.createUnmarshaller(); + return unmarshaller; + } + catch (JAXBException ex) { + throw new CodecException( + "Could not create Unmarshaller for class [" + clazz + "]: " + ex.getMessage(), ex); + } + } + + protected final JAXBContext getJaxbContext(Class clazz) { + Assert.notNull(clazz, "'clazz' must not be null"); + JAXBContext jaxbContext = this.jaxbContexts.get(clazz); + if (jaxbContext == null) { + try { + jaxbContext = JAXBContext.newInstance(clazz); + this.jaxbContexts.putIfAbsent(clazz, jaxbContext); + } + catch (JAXBException ex) { + throw new CodecException( + "Could not instantiate JAXBContext for class [" + clazz + "]: " + ex.getMessage(), ex); + } + } + return jaxbContext; + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JsonObjectDecoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JsonObjectDecoder.java new file mode 100644 index 0000000000..6a1f5db21d --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JsonObjectDecoder.java @@ -0,0 +1,263 @@ +/* + * Copyright 2002-2015 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.reactive.codec.decoder; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import org.reactivestreams.Publisher; +import reactor.fn.Function; +import reactor.rx.Streams; +import rx.Observable; + +import org.springframework.core.ResolvableType; +import org.springframework.http.MediaType; +import org.springframework.reactive.codec.encoder.JsonObjectEncoder; + +/** + * Decode an arbitrary split byte stream representing JSON objects to a bye stream + * where each chunk is a well-formed JSON object. + * + * If {@code Hints.STREAM_ARRAY_ELEMENTS} is enabled, each element of top level JSON array + * will be streamed as an individual JSON object. + * + * This class does not do any real parsing or validation. A sequence of bytes is considered a JSON object/array + * if it contains a matching number of opening and closing braces/brackets. + * + * Based on Netty {@code JsonObjectDecoder} + * + * @author Sebastien Deleuze + * @see JsonObjectEncoder + */ +public class JsonObjectDecoder implements ByteToMessageDecoder { + + private static final int ST_CORRUPTED = -1; + private static final int ST_INIT = 0; + private static final int ST_DECODING_NORMAL = 1; + private static final int ST_DECODING_ARRAY_STREAM = 2; + + private final int maxObjectLength; + private final boolean streamArrayElements; + + public JsonObjectDecoder() { + // 1 MB + this(1024 * 1024); + } + + public JsonObjectDecoder(int maxObjectLength) { + this(maxObjectLength, true); + } + + public JsonObjectDecoder(boolean streamArrayElements) { + this(1024 * 1024, streamArrayElements); + } + + /** + * @param maxObjectLength maximum number of bytes a JSON object/array may use (including braces and all). + * Objects exceeding this length are dropped and an {@link IllegalStateException} + * is thrown. + * @param streamArrayElements if set to true and the "top level" JSON object is an array, each of its entries + * is passed through the pipeline individually and immediately after it was fully + * received, allowing for arrays with "infinitely" many elements. + * + */ + public JsonObjectDecoder(int maxObjectLength, boolean streamArrayElements) { + if (maxObjectLength < 1) { + throw new IllegalArgumentException("maxObjectLength must be a positive int"); + } + this.maxObjectLength = maxObjectLength; + this.streamArrayElements = streamArrayElements; + } + + + @Override + public boolean canDecode(ResolvableType type, MediaType mediaType, Object... hints) { + return mediaType.isCompatibleWith(MediaType.APPLICATION_JSON) && + (Observable.class.isAssignableFrom(type.getRawClass()) || Publisher.class.isAssignableFrom(type.getRawClass())); + } + + @Override + public Publisher decode(Publisher inputStream, ResolvableType type, MediaType mediaType, Object... hints) { + + return Streams.wrap(inputStream).flatMap(new Function>() { + + int openBraces; + int idx; + int state; + boolean insideString; + ByteBuf in; + Integer wrtIdx; + + @Override + public Publisher apply(ByteBuffer b) { + List chunks = new ArrayList<>(); + + if (in == null) { + in = Unpooled.copiedBuffer(b); + wrtIdx = in.writerIndex(); + } + else { + in = Unpooled.copiedBuffer(in, Unpooled.copiedBuffer(b)); + wrtIdx = in.writerIndex(); + } + if (state == ST_CORRUPTED) { + in.skipBytes(in.readableBytes()); + return Streams.fail(new IllegalStateException("Corrupted stream")); + } + + if (wrtIdx > maxObjectLength) { + // buffer size exceeded maxObjectLength; discarding the complete buffer. + in.skipBytes(in.readableBytes()); + reset(); + return Streams.fail(new IllegalStateException( + "object length exceeds " + maxObjectLength + ": " + + wrtIdx + + " bytes discarded")); + } + + for (/* use current idx */; idx < wrtIdx; idx++) { + byte c = in.getByte(idx); + if (state == ST_DECODING_NORMAL) { + decodeByte(c, in, idx); + + // All opening braces/brackets have been closed. That's enough to conclude + // that the JSON object/array is complete. + if (openBraces == 0) { + ByteBuf json = extractObject(in, in.readerIndex(), + idx + 1 - in.readerIndex()); + if (json != null) { + chunks.add(json.nioBuffer()); + } + + // The JSON object/array was extracted => discard the bytes from + // the input buffer. + in.readerIndex(idx + 1); + // Reset the object state to get ready for the next JSON object/text + // coming along the byte stream. + reset(); + } + } + else if (state == ST_DECODING_ARRAY_STREAM) { + decodeByte(c, in, idx); + + if (!insideString && (openBraces == 1 && c == ',' || + openBraces == 0 && c == ']')) { + // skip leading spaces. No range check is needed and the loop will terminate + // because the byte at position idx is not a whitespace. + for (int i = in.readerIndex(); Character.isWhitespace(in.getByte(i)); i++) { + in.skipBytes(1); + } + + // skip trailing spaces. + int idxNoSpaces = idx - 1; + while (idxNoSpaces >= in.readerIndex() && + Character.isWhitespace(in.getByte(idxNoSpaces))) { + idxNoSpaces--; + } + + ByteBuf json = extractObject(in, in.readerIndex(), + idxNoSpaces + 1 - in.readerIndex()); + if (json != null) { + chunks.add(json.nioBuffer()); + } + + in.readerIndex(idx + 1); + + if (c == ']') { + reset(); + } + } + // JSON object/array detected. Accumulate bytes until all braces/brackets are closed. + } + else if (c == '{' || c == '[') { + initDecoding(c, streamArrayElements); + + if (state == ST_DECODING_ARRAY_STREAM) { + // Discard the array bracket + in.skipBytes(1); + } + // Discard leading spaces in front of a JSON object/array. + } + else if (Character.isWhitespace(c)) { + in.skipBytes(1); + } + else { + state = ST_CORRUPTED; + return Streams.fail(new IllegalStateException( + "invalid JSON received at byte position " + idx + + ": " + ByteBufUtil.hexDump(in))); + } + } + + if (in.readableBytes() == 0) { + idx = 0; + } + return Streams.from(chunks); + } + + /** + * Override this method if you want to filter the json objects/arrays that get passed through the pipeline. + */ + @SuppressWarnings("UnusedParameters") + protected ByteBuf extractObject(ByteBuf buffer, int index, int length) { + return buffer.slice(index, length).retain(); + } + + private void decodeByte(byte c, ByteBuf in, int idx) { + if ((c == '{' || c == '[') && !insideString) { + openBraces++; + } + else if ((c == '}' || c == ']') && !insideString) { + openBraces--; + } + else if (c == '"') { + // start of a new JSON string. It's necessary to detect strings as they may + // also contain braces/brackets and that could lead to incorrect results. + if (!insideString) { + insideString = true; + // If the double quote wasn't escaped then this is the end of a string. + } + else if (in.getByte(idx - 1) != '\\') { + insideString = false; + } + } + } + + private void initDecoding(byte openingBrace, boolean streamArrayElements) { + openBraces = 1; + if (openingBrace == '[' && streamArrayElements) { + state = ST_DECODING_ARRAY_STREAM; + } + else { + state = ST_DECODING_NORMAL; + } + } + + private void reset() { + insideString = false; + state = ST_INIT; + openBraces = 0; + } + + }); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/StringDecoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/StringDecoder.java new file mode 100644 index 0000000000..3943bf9359 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/StringDecoder.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2015 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.reactive.codec.decoder; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +import org.reactivestreams.Publisher; +import reactor.io.buffer.Buffer; +import reactor.rx.Streams; + +import org.springframework.core.ResolvableType; +import org.springframework.http.MediaType; +import org.springframework.reactive.codec.encoder.StringEncoder; +import org.springframework.reactive.codec.support.HintUtils; + +/** + * Decode from a bytes stream to a String stream. + * + * @author Sebastien Deleuze + * @see StringEncoder + */ +public class StringDecoder implements ByteToMessageDecoder { + + public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; + + + @Override + public boolean canDecode(ResolvableType type, MediaType mediaType, Object... hints) { + return mediaType.isCompatibleWith(MediaType.TEXT_PLAIN); + } + + @Override + public Publisher decode(Publisher inputStream, ResolvableType type, MediaType mediaType, Object... hints) { + Charset charset = HintUtils.getHintByClass(Charset.class, hints, DEFAULT_CHARSET); + return Streams.wrap(inputStream).map(chunk -> new String(new Buffer(chunk).asBytes(), charset)); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoder.java new file mode 100644 index 0000000000..931b853f56 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoder.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2015 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.reactive.codec.encoder; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.reactivestreams.Publisher; +import reactor.io.buffer.Buffer; +import reactor.rx.Stream; +import reactor.rx.Streams; + +import org.springframework.core.ResolvableType; +import org.springframework.http.MediaType; +import org.springframework.reactive.codec.CodecException; +import org.springframework.reactive.codec.decoder.JacksonJsonDecoder; +import org.springframework.reactive.io.BufferOutputStream; + +/** + * Encode from an {@code Object} stream to a byte stream of JSON objects. + * + * @author Sebastien Deleuze + * @see JacksonJsonDecoder + */ +public class JacksonJsonEncoder implements MessageToByteEncoder { + + private final ObjectMapper mapper; + + + public JacksonJsonEncoder() { + this(new ObjectMapper()); + } + + public JacksonJsonEncoder(ObjectMapper mapper) { + this.mapper = mapper; + } + + + @Override + public boolean canEncode(ResolvableType type, MediaType mediaType, Object... hints) { + return mediaType.isCompatibleWith(MediaType.APPLICATION_JSON); + } + + @Override + public Publisher encode(Publisher messageStream, ResolvableType type, MediaType mediaType, Object... hints) { + Stream stream = Streams.wrap(messageStream).map(value -> { + Buffer buffer = new Buffer(); + BufferOutputStream outputStream = new BufferOutputStream(buffer); + try { + this.mapper.writeValue(outputStream, value); + } + catch (IOException e) { + throw new CodecException("Error while writing the data", e); + } + buffer.flip(); + return buffer.byteBuffer(); + }); + return stream; + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/Jaxb2Encoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/Jaxb2Encoder.java new file mode 100644 index 0000000000..b0adac193f --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/Jaxb2Encoder.java @@ -0,0 +1,108 @@ +/* + * Copyright 2002-2015 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.reactive.codec.encoder; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.MarshalException; +import javax.xml.bind.Marshaller; + +import org.reactivestreams.Publisher; +import reactor.io.buffer.Buffer; +import reactor.rx.Streams; + +import org.springframework.core.ResolvableType; +import org.springframework.http.MediaType; +import org.springframework.reactive.codec.CodecException; +import org.springframework.reactive.codec.decoder.Jaxb2Decoder; +import org.springframework.reactive.io.BufferOutputStream; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Encode from an {@code Object} stream to a byte stream of XML elements. + * + * @author Sebastien Deleuze + * @see Jaxb2Decoder + */ +public class Jaxb2Encoder implements MessageToByteEncoder { + + private final ConcurrentMap, JAXBContext> jaxbContexts = new ConcurrentHashMap, JAXBContext>(64); + + + @Override + public boolean canEncode(ResolvableType type, MediaType mediaType, Object... hints) { + return mediaType.isCompatibleWith(MediaType.APPLICATION_XML) || mediaType.isCompatibleWith(MediaType.TEXT_XML); + } + + @Override + public Publisher encode(Publisher messageStream, ResolvableType type, MediaType mediaType, Object... hints) { + return Streams.wrap(messageStream).map(value -> { + try { + Buffer buffer = new Buffer(); + BufferOutputStream outputStream = new BufferOutputStream(buffer); + Class clazz = ClassUtils.getUserClass(value); + Marshaller marshaller = createMarshaller(clazz); + marshaller.setProperty(Marshaller.JAXB_ENCODING, StandardCharsets.UTF_8.name()); + marshaller.marshal(value, outputStream); + buffer.flip(); + return buffer.byteBuffer(); + } + catch (MarshalException ex) { + throw new CodecException( + "Could not marshal [" + value + "]: " + ex.getMessage(), ex); + } + catch (JAXBException ex) { + throw new CodecException( + "Could not instantiate JAXBContext: " + ex.getMessage(), ex); + } + }); + } + + protected final Marshaller createMarshaller(Class clazz) { + try { + JAXBContext jaxbContext = getJaxbContext(clazz); + Marshaller marshaller = jaxbContext.createMarshaller(); + return marshaller; + } + catch (JAXBException ex) { + throw new CodecException( + "Could not create Marshaller for class [" + clazz + "]: " + ex.getMessage(), ex); + } + } + + protected final JAXBContext getJaxbContext(Class clazz) { + Assert.notNull(clazz, "'clazz' must not be null"); + JAXBContext jaxbContext = this.jaxbContexts.get(clazz); + if (jaxbContext == null) { + try { + jaxbContext = JAXBContext.newInstance(clazz); + this.jaxbContexts.putIfAbsent(clazz, jaxbContext); + } + catch (JAXBException ex) { + throw new CodecException( + "Could not instantiate JAXBContext for class [" + clazz + "]: " + ex.getMessage(), ex); + } + } + return jaxbContext; + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JsonObjectEncoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JsonObjectEncoder.java new file mode 100644 index 0000000000..974f00b943 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JsonObjectEncoder.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2015 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.reactive.codec.encoder; + +import java.nio.ByteBuffer; + +import org.reactivestreams.Publisher; + +import rx.Observable; +import rx.RxReactiveStreams; + +import org.springframework.core.ResolvableType; +import org.springframework.http.MediaType; +import org.springframework.reactive.codec.decoder.JsonObjectDecoder; + +/** + * Encode a bye stream of individual JSON element to a byte stream representing a single + * JSON array when {@code Hints.ENCODE_AS_ARRAY} is enabled. + * + * @author Sebastien Deleuze + * @see JsonObjectDecoder + */ +public class JsonObjectEncoder implements MessageToByteEncoder { + + private final ByteBuffer START_ARRAY = ByteBuffer.wrap("[".getBytes()); + private final ByteBuffer END_ARRAY = ByteBuffer.wrap("]".getBytes()); + private final ByteBuffer COMMA = ByteBuffer.wrap(",".getBytes()); + + + @Override + public boolean canEncode(ResolvableType type, MediaType mediaType, Object... hints) { + return mediaType.isCompatibleWith(MediaType.APPLICATION_JSON) && + (Observable.class.isAssignableFrom(type.getRawClass()) || Publisher.class.isAssignableFrom(type.getRawClass())); + } + + @Override + public Publisher encode(Publisher messageStream, ResolvableType type, MediaType mediaType, Object... hints) { + // TODO We use RxJava Observable because there is no skipLast() operator in Reactor + // TODO Merge some chunks, there is no need to have chunks with only '[', ']' or ',' characters + return RxReactiveStreams.toPublisher( + Observable.concat( + Observable.just(START_ARRAY), + RxReactiveStreams.toObservable(messageStream).flatMap(b -> Observable.just(b, COMMA)).skipLast(1), + Observable.just(END_ARRAY))); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/MessageToByteEncoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/MessageToByteEncoder.java new file mode 100644 index 0000000000..9a1b67162f --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/MessageToByteEncoder.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2015 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.reactive.codec.encoder; + +import java.nio.ByteBuffer; + +import org.reactivestreams.Publisher; + +import org.springframework.core.ResolvableType; +import org.springframework.http.MediaType; +import org.springframework.reactive.codec.decoder.ByteToMessageDecoder; + +/** + * Encode from a message stream to a bytes stream. + * + * @author Sebastien Deleuze + * @see ByteToMessageDecoder + */ +public interface MessageToByteEncoder { + + /** + * Indicate whether the given type and media type can be processed by this encoder. + * @param type the (potentially generic) type to ultimately encode from. + * Could be different from {@code T} type. + * @param mediaType the media type to encode. + * Typically the value of an {@code Accept} header for HTTP request. + * @param hints Additional information about how to encode, optional. + * @return {@code true} if encodable; {@code false} otherwise + */ + boolean canEncode(ResolvableType type, MediaType mediaType, Object... hints); + + /** + * Encode a given message stream to the given output byte stream. + * @param messageStream the message stream to encode. + * @param type the (potentially generic) type to ultimately encode from. + * Could be different from {@code T} type. + * @param mediaType the media type to encode. + * Typically the value of an {@code Accept} header for HTTP request. + * @param hints Additional information about how to encode, optional. + * @return the encoded bytes stream + */ + Publisher encode(Publisher messageStream, ResolvableType type, MediaType mediaType, Object... hints); + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/StringEncoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/StringEncoder.java new file mode 100644 index 0000000000..a27adf6e36 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/StringEncoder.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2015 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.reactive.codec.encoder; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +import org.reactivestreams.Publisher; +import reactor.rx.Streams; + +import org.springframework.core.ResolvableType; +import org.springframework.http.MediaType; +import org.springframework.reactive.codec.decoder.StringDecoder; +import org.springframework.reactive.codec.support.HintUtils; + +/** + * Encode from a String stream to a bytes stream. + * + * @author Sebastien Deleuze + * @see StringDecoder + */ +public class StringEncoder implements MessageToByteEncoder { + + public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; + + + @Override + public boolean canEncode(ResolvableType type, MediaType mediaType, Object... hints) { + return mediaType.isCompatibleWith(MediaType.TEXT_PLAIN); + } + + @Override + public Publisher encode(Publisher elementStream, ResolvableType type, MediaType mediaType, Object... hints) { + final Charset charset = HintUtils.getHintByClass(Charset.class, hints, DEFAULT_CHARSET); + return Streams.wrap(elementStream).map(s -> ByteBuffer.wrap(s.getBytes(charset))); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/support/HintUtils.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/support/HintUtils.java new file mode 100644 index 0000000000..ab63b2f659 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/support/HintUtils.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2015 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.reactive.codec.support; + +import org.springframework.reactive.codec.decoder.ByteToMessageDecoder; +import org.springframework.reactive.codec.encoder.MessageToByteEncoder; + +/** + * Utility methods for dealing with codec hints. + * + * @author Sebastien Deleuze + * @see Hints + * @see MessageToByteEncoder + * @see ByteToMessageDecoder + */ +public abstract class HintUtils { + + public static T getHintByClass(Class clazz, Object[] hints) { + return getHintByClass(clazz, hints, null); + } + + public static T getHintByClass(Class clazz, Object[] hints, T defaultValue) { + for (Object hint : hints) { + if (hint.getClass().isAssignableFrom(clazz)) { + return (T)hint; + } + } + return defaultValue; + } + + public static boolean containsHint(Object hint, Object[] hints) { + for (Object h : hints) { + if (h.equals(hint)) { + return true; + } + } + return false; + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/io/BufferOutputStream.java b/spring-web-reactive/src/main/java/org/springframework/reactive/io/BufferOutputStream.java new file mode 100644 index 0000000000..4d51ec8788 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/io/BufferOutputStream.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2015 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.reactive.io; + +import java.io.IOException; +import java.io.OutputStream; + +import reactor.io.buffer.Buffer; + +/** + * @author Sebastien Deleuze + */ +public class BufferOutputStream extends OutputStream { + + private Buffer buffer; + + public BufferOutputStream(Buffer buffer) { + this.buffer = buffer; + } + + public void write(int b) throws IOException { + buffer.append(b); + } + + public void write(byte[] bytes, int off, int len) + throws IOException { + buffer.append(bytes, off, len); + } + + public Buffer getBuffer() { + return buffer; + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteBufferInputStream.java b/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteBufferInputStream.java new file mode 100644 index 0000000000..dbd2a5e16d --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteBufferInputStream.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2015 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.reactive.io; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; + +/** + * Simple {@link InputStream} implementation that exposes currently + * available content of a {@link ByteBuffer}. + * + * From Jackson ByteBufferBackedInputStream + */ +public class ByteBufferInputStream extends InputStream { + + protected final ByteBuffer b; + + public ByteBufferInputStream(ByteBuffer buf) { b = buf; } + + @Override public int available() { return b.remaining(); } + + @Override + public int read() throws IOException { return b.hasRemaining() ? (b.get() & 0xFF) : -1; } + + @Override + public int read(byte[] bytes, int off, int len) throws IOException { + if (!b.hasRemaining()) return -1; + len = Math.min(len, b.remaining()); + b.get(bytes, off, len); + return len; + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java new file mode 100644 index 0000000000..6a7159d8fd --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java @@ -0,0 +1,134 @@ +/* + * Copyright 2002-2015 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.reactive.web.dispatch.method.annotation; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.reactivestreams.Publisher; +import reactor.rx.Stream; +import reactor.rx.Streams; +import rx.Observable; +import rx.RxReactiveStreams; + +import org.springframework.core.MethodParameter; +import org.springframework.core.ResolvableType; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.reactive.codec.decoder.ByteToMessageDecoder; +import org.springframework.reactive.web.dispatch.method.HandlerMethodArgumentResolver; +import org.springframework.reactive.web.http.ServerHttpRequest; +import org.springframework.web.bind.annotation.RequestBody; + +/** + * @author Sebastien Deleuze + */ +public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolver { + + private static final Charset UTF_8 = Charset.forName("UTF-8"); + + private final List> deserializers; + private final List> preProcessors; + + + public RequestBodyArgumentResolver(List> deserializers) { + this(deserializers, Collections.EMPTY_LIST); + } + + public RequestBodyArgumentResolver(List> deserializers, List> preProcessors) { + this.deserializers = deserializers; + this.preProcessors = preProcessors; + } + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(RequestBody.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, ServerHttpRequest request) { + + MediaType mediaType = resolveMediaType(request); + ResolvableType type = ResolvableType.forMethodParameter(parameter); + List hints = new ArrayList<>(); + hints.add(UTF_8); + // TODO: Refactor type conversion + ResolvableType readType = type; + if (Observable.class.isAssignableFrom(type.getRawClass()) || Publisher.class.isAssignableFrom(type.getRawClass())) { + readType = type.getGeneric(0); + } + + ByteToMessageDecoder deserializer = resolveDeserializers(request, type, mediaType, hints.toArray()); + if (deserializer != null) { + + Publisher inputStream = Streams.wrap(request.getBody()).map(bytes -> ByteBuffer.wrap(bytes)); + List> preProcessors = resolvePreProcessors(request, type, mediaType, hints.toArray()); + for (ByteToMessageDecoder preProcessor : preProcessors) { + inputStream = preProcessor.decode(inputStream, type, mediaType, hints.toArray()); + } + Publisher elementStream = deserializer.decode(inputStream, readType, mediaType, UTF_8); + + // TODO: Refactor type conversion + if (Stream.class.isAssignableFrom(type.getRawClass())) { + return Streams.wrap(elementStream); + } + else if (Observable.class.isAssignableFrom(type.getRawClass())) { + return RxReactiveStreams.toObservable(elementStream); + } + else if (Publisher.class.isAssignableFrom(type.getRawClass())) { + return elementStream; + } + else { + try { + return Streams.wrap(elementStream).next().await(); + } catch(InterruptedException ex) { + throw new IllegalStateException("Timeout before getter the value"); + } + } + } + throw new IllegalStateException("Argument type not supported: " + type); + } + + private MediaType resolveMediaType(ServerHttpRequest request) { + String acceptHeader = request.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE); + List mediaTypes = MediaType.parseMediaTypes(acceptHeader); + MediaType.sortBySpecificityAndQuality(mediaTypes); + return ( mediaTypes.size() > 0 ? mediaTypes.get(0) : MediaType.TEXT_PLAIN); + } + + private ByteToMessageDecoder resolveDeserializers(ServerHttpRequest request, ResolvableType type, MediaType mediaType, Object[] hints) { + for (ByteToMessageDecoder deserializer : this.deserializers) { + if (deserializer.canDecode(type, mediaType, hints)) { + return deserializer; + } + } + return null; + } + + private List> resolvePreProcessors(ServerHttpRequest request, ResolvableType type, MediaType mediaType, Object[] hints) { + List> preProcessors = new ArrayList<>(); + for (ByteToMessageDecoder preProcessor : this.preProcessors) { + if (preProcessor.canDecode(type, mediaType, hints)) { + preProcessors.add(preProcessor); + } + } + return preProcessors; + } +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java index d57e9e9413..5fcab45fd2 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java @@ -16,12 +16,16 @@ package org.springframework.reactive.web.dispatch.method.annotation; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import org.reactivestreams.Publisher; import reactor.rx.Streams; import org.springframework.beans.factory.InitializingBean; +import org.springframework.reactive.codec.decoder.JacksonJsonDecoder; +import org.springframework.reactive.codec.decoder.JsonObjectDecoder; +import org.springframework.reactive.codec.decoder.StringDecoder; import org.springframework.reactive.web.dispatch.HandlerAdapter; import org.springframework.reactive.web.dispatch.HandlerResult; import org.springframework.reactive.web.dispatch.method.HandlerMethodArgumentResolver; @@ -50,6 +54,7 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Initializin if (this.argumentResolvers == null) { this.argumentResolvers = new ArrayList<>(); this.argumentResolvers.add(new RequestParamArgumentResolver()); + this.argumentResolvers.add(new RequestBodyArgumentResolver(Arrays.asList(new StringDecoder(), new JacksonJsonDecoder()), Arrays.asList(new JsonObjectDecoder(true)))); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java index 21d8ddb0d0..69f8d483c9 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java @@ -16,16 +16,25 @@ package org.springframework.reactive.web.dispatch.method.annotation; import java.lang.reflect.Method; +import java.nio.ByteBuffer; import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import org.reactivestreams.Publisher; +import reactor.io.buffer.Buffer; import reactor.rx.Streams; +import rx.Observable; +import rx.RxReactiveStreams; import org.springframework.core.MethodParameter; import org.springframework.core.Ordered; import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; +import org.springframework.reactive.codec.encoder.MessageToByteEncoder; import org.springframework.reactive.web.dispatch.HandlerResult; import org.springframework.reactive.web.dispatch.HandlerResultHandler; import org.springframework.reactive.web.http.ServerHttpRequest; @@ -35,8 +44,7 @@ import org.springframework.web.method.HandlerMethod; /** - * For now a simple {@code String} or {@code Publisher} to - * "text/plain;charset=UTF-8" conversion. + * First version using {@link MessageToByteEncoder}s * * @author Rossen Stoyanchev */ @@ -45,9 +53,21 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered private static final Charset UTF_8 = Charset.forName("UTF-8"); + private final List> serializers; + private final List> postProcessors; + private int order = Ordered.LOWEST_PRECEDENCE; + public ResponseBodyResultHandler(List> serializers) { + this(serializers, Collections.EMPTY_LIST); + } + + public ResponseBodyResultHandler(List> serializers, List> postProcessors) { + this.serializers = serializers; + this.postProcessors = postProcessors; + } + public void setOrder(int order) { this.order = order; } @@ -80,20 +100,61 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered return Streams.empty(); } - if (value instanceof String) { - response.getHeaders().setContentType(new MediaType("text", "plain", UTF_8)); - return response.writeWith(Streams.just(((String) value).getBytes(UTF_8))); + MediaType mediaType = resolveMediaType(request); + ResolvableType type = ResolvableType.forMethodParameter(returnType); + List hints = new ArrayList<>(); + hints.add(UTF_8); + MessageToByteEncoder serializer = (MessageToByteEncoder)resolveSerializer(request, type, mediaType, hints.toArray()); + if (serializer != null) { + Publisher elementStream; + + // TODO: Refactor type conversion + if (Observable.class.isAssignableFrom(type.getRawClass())) { + elementStream = RxReactiveStreams.toPublisher((Observable) value); + } + else if (Publisher.class.isAssignableFrom(type.getRawClass())) { + elementStream = (Publisher)value; + } + else { + elementStream = Streams.just(value); + } + + Publisher outputStream = serializer.encode(elementStream, type, mediaType, hints.toArray()); + List> postProcessors = resolvePostProcessors(request, type, mediaType, hints.toArray()); + for (MessageToByteEncoder postProcessor : postProcessors) { + outputStream = postProcessor.encode(outputStream, type, mediaType, hints.toArray()); + } + response.getHeaders().setContentType(mediaType); + return response.writeWith(Streams.wrap(outputStream).map(buffer -> new Buffer(buffer).asBytes())); } - else if (value instanceof Publisher) { - Class type = ResolvableType.forMethodParameter(returnType).resolveGeneric(0); - if (String.class.equals(type)) { - @SuppressWarnings("unchecked") - Publisher content = (Publisher) value; - return response.writeWith(Streams.wrap(content).map(value1 -> value1.getBytes(UTF_8))); + return Streams.fail(new IllegalStateException( + "Return value type not supported: " + returnType)); + } + + private MediaType resolveMediaType(ServerHttpRequest request) { + String acceptHeader = request.getHeaders().getFirst(HttpHeaders.ACCEPT); + List mediaTypes = MediaType.parseMediaTypes(acceptHeader); + MediaType.sortBySpecificityAndQuality(mediaTypes); + return ( mediaTypes.size() > 0 ? mediaTypes.get(0) : MediaType.TEXT_PLAIN); + } + + private MessageToByteEncoder resolveSerializer(ServerHttpRequest request, ResolvableType type, MediaType mediaType, Object[] hints) { + for (MessageToByteEncoder codec : this.serializers) { + if (codec.canEncode(type, mediaType, hints)) { + return codec; } } + return null; + } - return Streams.fail(new IllegalStateException("Return value type not supported: " + returnType)); + private List> resolvePostProcessors(ServerHttpRequest request, ResolvableType type, MediaType mediaType, Object[] hints) { + List> postProcessors = new ArrayList<>(); + for (MessageToByteEncoder postProcessor : this.postProcessors) { + if (postProcessor.canEncode(type, mediaType, hints)) { + postProcessors.add(postProcessor); + } + } + return postProcessors; } } diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoderTests.java new file mode 100644 index 0000000000..b29dc553fe --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoderTests.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2015 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.reactive.codec.decoder; + +import java.nio.ByteBuffer; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import org.junit.Test; +import reactor.io.buffer.Buffer; +import reactor.rx.Stream; +import reactor.rx.Streams; + +import org.springframework.core.ResolvableType; +import org.springframework.http.MediaType; +import org.springframework.reactive.converter.Pojo; + +/** + * @author Sebastien Deleuze + */ +public class JacksonJsonDecoderTests { + + private final JacksonJsonDecoder decoder = new JacksonJsonDecoder(); + + @Test + public void canDecode() { + assertTrue(decoder.canDecode(null, MediaType.APPLICATION_JSON)); + assertFalse(decoder.canDecode(null, MediaType.APPLICATION_XML)); + } + + @Test + public void decode() throws InterruptedException { + Stream source = Streams.just(Buffer.wrap("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}").byteBuffer()); + List results = Streams.wrap(decoder.decode(source, ResolvableType.forClass(Pojo.class), null)) + .toList().await(); + assertEquals(1, results.size()); + assertEquals("foofoo", ((Pojo) results.get(0)).getFoo()); + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/Jaxb2DecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/Jaxb2DecoderTests.java new file mode 100644 index 0000000000..24d9d9cce1 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/Jaxb2DecoderTests.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2015 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.reactive.codec.decoder; + +import java.nio.ByteBuffer; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import org.junit.Test; +import reactor.io.buffer.Buffer; +import reactor.rx.Stream; +import reactor.rx.Streams; + +import org.springframework.core.ResolvableType; +import org.springframework.http.MediaType; +import org.springframework.reactive.converter.Pojo; + +/** + * @author Sebastien Deleuze + */ +public class Jaxb2DecoderTests { + + private final Jaxb2Decoder decoder = new Jaxb2Decoder(); + + @Test + public void canDecode() { + assertTrue(decoder.canDecode(null, MediaType.APPLICATION_XML)); + assertTrue(decoder.canDecode(null, MediaType.TEXT_XML)); + assertFalse(decoder.canDecode(null, MediaType.APPLICATION_JSON)); + } + + @Test + public void decode() throws InterruptedException { + Stream source = Streams.just(Buffer.wrap("barbarfoofoo").byteBuffer()); + List results = Streams.wrap(decoder.decode(source, ResolvableType.forClass(Pojo.class), null)) + .toList().await(); + assertEquals(1, results.size()); + assertEquals("foofoo", ((Pojo) results.get(0)).getFoo()); + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/JsonObjectDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/JsonObjectDecoderTests.java new file mode 100644 index 0000000000..d683796e6d --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/JsonObjectDecoderTests.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2015 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.reactive.codec.decoder; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import org.junit.Test; +import reactor.io.buffer.Buffer; +import reactor.rx.Stream; +import reactor.rx.Streams; + +/** + * @author Sebastien Deleuze + */ +public class JsonObjectDecoderTests { + + @Test + public void decodeSingleChunkToArray() throws InterruptedException { + JsonObjectDecoder decoder = new JsonObjectDecoder(true); + Stream source = Streams.just(Buffer.wrap("[{\"foo\": \"foofoo\", \"bar\": \"barbar\"},{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}]").byteBuffer()); + List results = Streams.wrap(decoder.decode(source, null, null)).map(chunk -> { + byte[] b = new byte[chunk.remaining()]; + chunk.get(b); + return new String(b, StandardCharsets.UTF_8); + }).toList().await(); + assertEquals(2, results.size()); + assertEquals("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}", results.get(0)); + assertEquals("{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}", results.get(1)); + } + + @Test + public void decodeMultipleChunksToArray() throws InterruptedException { + JsonObjectDecoder decoder = new JsonObjectDecoder(true); + Stream source = Streams.just(Buffer.wrap("[{\"foo\": \"foofoo\", \"bar\"").byteBuffer(), Buffer.wrap(": \"barbar\"},{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}]").byteBuffer()); + List results = Streams.wrap(decoder.decode(source, null, null)).map(chunk -> { + byte[] b = new byte[chunk.remaining()]; + chunk.get(b); + return new String(b, StandardCharsets.UTF_8); + }).toList().await(); + assertEquals(2, results.size()); + assertEquals("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}", results.get(0)); + assertEquals("{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}", results.get(1)); + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/StringDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/StringDecoderTests.java new file mode 100644 index 0000000000..9ad83fc7b9 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/StringDecoderTests.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2015 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.reactive.codec.decoder; + +import java.nio.ByteBuffer; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import org.junit.Test; +import reactor.io.buffer.Buffer; +import reactor.rx.Stream; +import reactor.rx.Streams; + +import org.springframework.core.ResolvableType; +import org.springframework.http.MediaType; +import org.springframework.reactive.converter.Pojo; + +/** + * @author Sebastien Deleuze + */ +public class StringDecoderTests { + + private final StringDecoder decoder = new StringDecoder(); + + @Test + public void canDecode() { + assertTrue(decoder.canDecode(null, MediaType.TEXT_PLAIN)); + assertFalse(decoder.canDecode(null, MediaType.APPLICATION_JSON)); + } + + @Test + public void decode() throws InterruptedException { + Stream source = Streams.just(Buffer.wrap("foo").byteBuffer(), Buffer.wrap("bar").byteBuffer()); + List results = Streams.wrap(decoder.decode(source, ResolvableType.forClass(Pojo.class), null)) + .toList().await(); + assertEquals(2, results.size()); + assertEquals("foo", results.get(0)); + assertEquals("bar", results.get(1)); + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoderTests.java new file mode 100644 index 0000000000..4207c5997e --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoderTests.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2015 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.reactive.codec.encoder; + +import java.nio.charset.StandardCharsets; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import org.junit.Test; +import reactor.rx.Stream; +import reactor.rx.Streams; + +import org.springframework.http.MediaType; +import org.springframework.reactive.converter.Pojo; + +/** + * @author Sebastien Deleuze + */ +public class JacksonJsonEncoderTests { + + private final JacksonJsonEncoder encoder = new JacksonJsonEncoder(); + + @Test + public void canWrite() { + assertTrue(encoder.canEncode(null, MediaType.APPLICATION_JSON)); + assertFalse(encoder.canEncode(null, MediaType.APPLICATION_XML)); + } + + @Test + public void write() throws InterruptedException { + Stream source = Streams.just(new Pojo("foofoo", "barbar"), new Pojo("foofoofoo", "barbarbar")); + List results = Streams.wrap(encoder.encode(source, null, null)).map(chunk -> { + byte[] b = new byte[chunk.remaining()]; + chunk.get(b); + return new String(b, StandardCharsets.UTF_8); + }).toList().await(); + assertEquals(2, results.size()); + assertEquals("{\"foo\":\"foofoo\",\"bar\":\"barbar\"}", results.get(0)); + assertEquals("{\"foo\":\"foofoofoo\",\"bar\":\"barbarbar\"}", results.get(1)); + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/Jaxb2EncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/Jaxb2EncoderTests.java new file mode 100644 index 0000000000..1710b6708b --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/Jaxb2EncoderTests.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2015 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.reactive.codec.encoder; + +import java.nio.charset.StandardCharsets; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import org.junit.Test; +import reactor.rx.Stream; +import reactor.rx.Streams; + +import org.springframework.http.MediaType; +import org.springframework.reactive.codec.decoder.Jaxb2Decoder; +import org.springframework.reactive.converter.Pojo; + +/** + * @author Sebastien Deleuze + */ +public class Jaxb2EncoderTests { + + private final Jaxb2Encoder encoder = new Jaxb2Encoder(); + + @Test + public void canEncode() { + assertTrue(encoder.canEncode(null, MediaType.APPLICATION_XML)); + assertTrue(encoder.canEncode(null, MediaType.TEXT_XML)); + assertFalse(encoder.canEncode(null, MediaType.APPLICATION_JSON)); + } + + @Test + public void encode() throws InterruptedException { + Stream source = Streams.just(new Pojo("foofoo", "barbar"), new Pojo("foofoofoo", "barbarbar")); + List results = Streams.wrap(encoder.encode(source, null, null)).map(chunk -> { + byte[] b = new byte[chunk.remaining()]; + chunk.get(b); + return new String(b, StandardCharsets.UTF_8); + }).toList().await(); + assertEquals(2, results.size()); + assertEquals("barbarfoofoo", results.get(0)); + assertEquals("barbarbarfoofoofoo", results.get(1)); + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/JsonObjectEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/JsonObjectEncoderTests.java new file mode 100644 index 0000000000..47726014f0 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/JsonObjectEncoderTests.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2015 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.reactive.codec.encoder; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import org.junit.Test; +import reactor.io.buffer.Buffer; +import reactor.rx.Stream; +import reactor.rx.Streams; + +/** + * @author Sebastien Deleuze + */ +public class JsonObjectEncoderTests { + + @Test + public void encodeToArray() throws InterruptedException { + JsonObjectEncoder encoder = new JsonObjectEncoder(); + Stream source = Streams.just(Buffer.wrap("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}").byteBuffer(), Buffer.wrap("{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}").byteBuffer()); + List results = Streams.wrap(encoder.encode(source, null, null)).map(chunk -> { + byte[] b = new byte[chunk.remaining()]; + chunk.get(b); + return new String(b, StandardCharsets.UTF_8); + }).toList().await(); + String result = String.join("", results); + assertEquals("[{\"foo\": \"foofoo\", \"bar\": \"barbar\"},{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}]", result); + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/StringEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/StringEncoderTests.java new file mode 100644 index 0000000000..ce67e5e29a --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/StringEncoderTests.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2015 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.reactive.codec.encoder; + +import java.nio.charset.StandardCharsets; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import org.junit.Test; +import reactor.rx.Streams; + +import org.springframework.http.MediaType; + +/** + * @author Sebastien Deleuze + */ +public class StringEncoderTests { + + private final StringEncoder encoder = new StringEncoder(); + + @Test + public void canWrite() { + assertTrue(encoder.canEncode(null, MediaType.TEXT_PLAIN)); + assertFalse(encoder.canEncode(null, MediaType.APPLICATION_JSON)); + } + + @Test + public void write() throws InterruptedException { + List results = Streams.wrap(encoder.encode(Streams.just("foo"), null, null)) + .map(chunk -> { + byte[] b = new byte[chunk.remaining()]; + chunk.get(b); + return new String(b, StandardCharsets.UTF_8); + }).toList().await(); + assertEquals(1, results.size()); + assertEquals("foo", results.get(0)); + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/converter/Pojo.java b/spring-web-reactive/src/test/java/org/springframework/reactive/converter/Pojo.java new file mode 100644 index 0000000000..913acc4d12 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/converter/Pojo.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2015 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.reactive.converter; + +import javax.xml.bind.annotation.XmlRootElement; + +/** + * @author Sebastien Deleuze + */ +@XmlRootElement +public class Pojo { + + private String foo; + + private String bar; + + public Pojo() { + } + + public Pojo(String foo, String bar) { + this.foo = foo; + this.bar = bar; + } + + public String getFoo() { + return this.foo; + } + + public void setFoo(String foo) { + this.foo = foo; + } + + public String getBar() { + return this.bar; + } + + public void setBar(String bar) { + this.bar = bar; + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingIntegrationTests.java index 52f2dacc4f..053fcc716b 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingIntegrationTests.java @@ -17,41 +17,48 @@ package org.springframework.reactive.web.dispatch.method.annotation; import java.net.URI; -import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.List; +import static org.junit.Assert.assertEquals; import org.junit.Test; import org.reactivestreams.Publisher; +import reactor.rx.Stream; import reactor.rx.Streams; +import rx.Observable; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.MediaType; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; +import org.springframework.reactive.codec.encoder.JacksonJsonEncoder; +import org.springframework.reactive.codec.encoder.JsonObjectEncoder; +import org.springframework.reactive.codec.encoder.StringEncoder; import org.springframework.reactive.web.dispatch.DispatcherHandler; import org.springframework.reactive.web.http.AbstractHttpHandlerIntegrationTests; import org.springframework.reactive.web.http.HttpHandler; import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.client.RestTemplate; import org.springframework.web.context.support.StaticWebApplicationContext; -import static org.junit.Assert.assertArrayEquals; - /** * @author Rossen Stoyanchev + * @author Sebastien Deleuze */ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrationTests { - private static final Charset UTF_8 = Charset.forName("UTF-8"); - - @Override protected HttpHandler createHttpHandler() { StaticWebApplicationContext wac = new StaticWebApplicationContext(); wac.registerSingleton("handlerMapping", RequestMappingHandlerMapping.class); wac.registerSingleton("handlerAdapter", RequestMappingHandlerAdapter.class); - wac.registerSingleton("responseBodyResultHandler", ResponseBodyResultHandler.class); + wac.getDefaultListableBeanFactory().registerSingleton("responseBodyResultHandler", + new ResponseBodyResultHandler(Arrays.asList(new StringEncoder(), new JacksonJsonEncoder()), Arrays.asList(new JsonObjectEncoder()))); wac.registerSingleton("controller", TestController.class); wac.refresh(); @@ -67,9 +74,97 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati URI url = new URI("http://localhost:" + port + "/param?name=George"); RequestEntity request = RequestEntity.get(url).build(); - ResponseEntity response = restTemplate.exchange(request, byte[].class); + ResponseEntity response = restTemplate.exchange(request, String.class); - assertArrayEquals("Hello George!".getBytes(UTF_8), response.getBody()); + assertEquals("Hello George!", response.getBody()); + } + + @Test + public void serializeAsPojo() throws Exception { + + RestTemplate restTemplate = new RestTemplate(); + + URI url = new URI("http://localhost:" + port + "/person"); + RequestEntity request = RequestEntity.get(url).accept(MediaType.APPLICATION_JSON).build(); + ResponseEntity response = restTemplate.exchange(request, Person.class); + + assertEquals(new Person("Robert"), response.getBody()); + } + + @Test + public void serializeAsList() throws Exception { + + RestTemplate restTemplate = new RestTemplate(); + + URI url = new URI("http://localhost:" + port + "/list"); + RequestEntity request = RequestEntity.get(url).accept(MediaType.APPLICATION_JSON).build(); + List results = restTemplate.exchange(request, new ParameterizedTypeReference>(){}).getBody(); + + assertEquals(2, results.size()); + assertEquals(new Person("Robert"), results.get(0)); + assertEquals(new Person("Marie"), results.get(1)); + } + + @Test + public void serializeAsPublisher() throws Exception { + + RestTemplate restTemplate = new RestTemplate(); + + URI url = new URI("http://localhost:" + port + "/publisher"); + RequestEntity request = RequestEntity.get(url).accept(MediaType.APPLICATION_JSON).build(); + List results = restTemplate.exchange(request, new ParameterizedTypeReference>(){}).getBody(); + + assertEquals(2, results.size()); + assertEquals(new Person("Robert"), results.get(0)); + assertEquals(new Person("Marie"), results.get(1)); + } + + @Test + public void serializeAsObservable() throws Exception { + + RestTemplate restTemplate = new RestTemplate(); + + URI url = new URI("http://localhost:" + port + "/observable"); + RequestEntity request = RequestEntity.get(url).accept(MediaType.APPLICATION_JSON).build(); + List results = restTemplate.exchange(request, new ParameterizedTypeReference>() { + }).getBody(); + + assertEquals(2, results.size()); + assertEquals(new Person("Robert"), results.get(0)); + assertEquals(new Person("Marie"), results.get(1)); + } + + @Test + public void serializeAsReactorStream() throws Exception { + + RestTemplate restTemplate = new RestTemplate(); + + URI url = new URI("http://localhost:" + port + "/stream"); + RequestEntity request = RequestEntity.get(url).accept(MediaType.APPLICATION_JSON).build(); + List results = restTemplate.exchange(request, new ParameterizedTypeReference>() { + }).getBody(); + + assertEquals(2, results.size()); + assertEquals(new Person("Robert"), results.get(0)); + assertEquals(new Person("Marie"), results.get(1)); + } + + @Test + public void echo() throws Exception { + RestTemplate restTemplate = new RestTemplate(); + + URI url = new URI("http://localhost:" + port + "/echo"); + List persons = Arrays.asList(new Person("Robert"), new Person("Marie")); + RequestEntity> request = RequestEntity + .post(url) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .body(persons); + List results = restTemplate.exchange(request, new ParameterizedTypeReference>(){}).getBody(); + + assertEquals(2, results.size()); + assertEquals("Robert", results.get(0).getName()); + assertEquals("Marie", results.get(1).getName()); } @@ -82,6 +177,73 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati public Publisher handleWithParam(@RequestParam String name) { return Streams.just("Hello ", name, "!"); } + + @RequestMapping("/person") + @ResponseBody + public Person personResponseBody() { + return new Person("Robert"); + } + + @RequestMapping("/list") + @ResponseBody + public List listResponseBody() { + return Arrays.asList(new Person("Robert"), new Person("Marie")); + } + + @RequestMapping("/publisher") + @ResponseBody + public Publisher publisherResponseBody() { + return Streams.just(new Person("Robert"), new Person("Marie")); + } + + @RequestMapping("/observable") + @ResponseBody + public Observable observableResponseBody() { + return Observable.just(new Person("Robert"), new Person("Marie")); + } + + @RequestMapping("/stream") + @ResponseBody + public Stream reactorStreamResponseBody() { + return Streams.just(new Person("Robert"), new Person("Marie")); + } + + @RequestMapping("/echo") + @ResponseBody + public Publisher echo(@RequestBody Publisher persons) { + return persons; + } + + } + + private static class Person { + + private String name; + + public Person() { + } + + public Person(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public boolean equals(Object o) { + return name.equals(((Person)o).name); + } + + @Override + public int hashCode() { + return name.hashCode(); + } } } From 3fd0f60618e99599c3b278233668e86ae137b966 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Thu, 10 Sep 2015 17:44:06 +0200 Subject: [PATCH 032/498] Polish integration tests --- .../RequestMappingIntegrationTests.java | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingIntegrationTests.java index 053fcc716b..00f5c8a698 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingIntegrationTests.java @@ -150,10 +150,10 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati } @Test - public void echo() throws Exception { + public void capitalize() throws Exception { RestTemplate restTemplate = new RestTemplate(); - URI url = new URI("http://localhost:" + port + "/echo"); + URI url = new URI("http://localhost:" + port + "/capitalize"); List persons = Arrays.asList(new Person("Robert"), new Person("Marie")); RequestEntity> request = RequestEntity .post(url) @@ -163,8 +163,8 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati List results = restTemplate.exchange(request, new ParameterizedTypeReference>(){}).getBody(); assertEquals(2, results.size()); - assertEquals("Robert", results.get(0).getName()); - assertEquals("Marie", results.get(1).getName()); + assertEquals("ROBERT", results.get(0).getName()); + assertEquals("MARIE", results.get(1).getName()); } @@ -208,10 +208,13 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati return Streams.just(new Person("Robert"), new Person("Marie")); } - @RequestMapping("/echo") + @RequestMapping("/capitalize") @ResponseBody - public Publisher echo(@RequestBody Publisher persons) { - return persons; + public Observable capitalize(@RequestBody Observable persons) { + return persons.map(person -> { + person.setName(person.getName().toUpperCase()); + return person; + }); } } From 3f6301648ee78c3eadba501b079f01d1ddd9c14c Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Thu, 10 Sep 2015 17:48:29 +0200 Subject: [PATCH 033/498] Update version to 0.1.0.BUILD-SNAPSHOT --- spring-web-reactive/gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-web-reactive/gradle.properties b/spring-web-reactive/gradle.properties index bebfcbcfd2..99f6735037 100644 --- a/spring-web-reactive/gradle.properties +++ b/spring-web-reactive/gradle.properties @@ -1 +1 @@ -version=1.0.0.BUILD-SNAPSHOT +version=0.1.0.BUILD-SNAPSHOT From ca1ec24aa9a8236f52781122648bb9aaa095ae55 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Thu, 10 Sep 2015 18:06:38 +0200 Subject: [PATCH 034/498] Define the group to use for the dependency --- spring-web-reactive/gradle.properties | 1 + 1 file changed, 1 insertion(+) diff --git a/spring-web-reactive/gradle.properties b/spring-web-reactive/gradle.properties index 99f6735037..ae8f3ee0fd 100644 --- a/spring-web-reactive/gradle.properties +++ b/spring-web-reactive/gradle.properties @@ -1 +1,2 @@ version=0.1.0.BUILD-SNAPSHOT +group=org.springframework.reactive From 870c1ae43bd143c81e476dd7312a7c19fcc6996c Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Thu, 10 Sep 2015 18:39:17 +0200 Subject: [PATCH 035/498] Polishing --- .../springframework/reactive/{converter => codec}/Pojo.java | 2 +- .../reactive/codec/decoder/JacksonJsonDecoderTests.java | 2 +- .../reactive/codec/decoder/Jaxb2DecoderTests.java | 2 +- .../reactive/codec/decoder/StringDecoderTests.java | 2 +- .../reactive/codec/encoder/JacksonJsonEncoderTests.java | 2 +- .../reactive/codec/encoder/Jaxb2EncoderTests.java | 3 +-- 6 files changed, 6 insertions(+), 7 deletions(-) rename spring-web-reactive/src/test/java/org/springframework/reactive/{converter => codec}/Pojo.java (95%) diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/converter/Pojo.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/Pojo.java similarity index 95% rename from spring-web-reactive/src/test/java/org/springframework/reactive/converter/Pojo.java rename to spring-web-reactive/src/test/java/org/springframework/reactive/codec/Pojo.java index 913acc4d12..ee55b9ec50 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/converter/Pojo.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/Pojo.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.converter; +package org.springframework.reactive.codec; import javax.xml.bind.annotation.XmlRootElement; diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoderTests.java index b29dc553fe..82a185efcb 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoderTests.java @@ -29,7 +29,7 @@ import reactor.rx.Streams; import org.springframework.core.ResolvableType; import org.springframework.http.MediaType; -import org.springframework.reactive.converter.Pojo; +import org.springframework.reactive.codec.Pojo; /** * @author Sebastien Deleuze diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/Jaxb2DecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/Jaxb2DecoderTests.java index 24d9d9cce1..155dbbb3ff 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/Jaxb2DecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/Jaxb2DecoderTests.java @@ -29,7 +29,7 @@ import reactor.rx.Streams; import org.springframework.core.ResolvableType; import org.springframework.http.MediaType; -import org.springframework.reactive.converter.Pojo; +import org.springframework.reactive.codec.Pojo; /** * @author Sebastien Deleuze diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/StringDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/StringDecoderTests.java index 9ad83fc7b9..ce707c40c9 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/StringDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/StringDecoderTests.java @@ -29,7 +29,7 @@ import reactor.rx.Streams; import org.springframework.core.ResolvableType; import org.springframework.http.MediaType; -import org.springframework.reactive.converter.Pojo; +import org.springframework.reactive.codec.Pojo; /** * @author Sebastien Deleuze diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoderTests.java index 4207c5997e..22af3b1683 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoderTests.java @@ -27,7 +27,7 @@ import reactor.rx.Stream; import reactor.rx.Streams; import org.springframework.http.MediaType; -import org.springframework.reactive.converter.Pojo; +import org.springframework.reactive.codec.Pojo; /** * @author Sebastien Deleuze diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/Jaxb2EncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/Jaxb2EncoderTests.java index 1710b6708b..d0cc833b64 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/Jaxb2EncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/Jaxb2EncoderTests.java @@ -27,8 +27,7 @@ import reactor.rx.Stream; import reactor.rx.Streams; import org.springframework.http.MediaType; -import org.springframework.reactive.codec.decoder.Jaxb2Decoder; -import org.springframework.reactive.converter.Pojo; +import org.springframework.reactive.codec.Pojo; /** * @author Sebastien Deleuze From 5e39858780ee68bb65986109d0841216d705e69b Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Fri, 11 Sep 2015 16:09:49 -0400 Subject: [PATCH 036/498] ignore for now --- .../reactive/web/http/EchoHandlerIntegrationTests.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/EchoHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/EchoHandlerIntegrationTests.java index 270aa1a9da..2011b613f5 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/EchoHandlerIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/EchoHandlerIntegrationTests.java @@ -19,6 +19,7 @@ package org.springframework.reactive.web.http; import java.net.URI; import java.util.Random; +import org.junit.Ignore; import org.junit.Test; import org.springframework.http.RequestEntity; @@ -43,6 +44,7 @@ public class EchoHandlerIntegrationTests extends AbstractHttpHandlerIntegrationT @Test + @Ignore public void echoBytes() throws Exception { RestTemplate restTemplate = new RestTemplate(); From 4d29110cd7cc722e289847dc660c06b52ec905ec Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Fri, 11 Sep 2015 22:46:44 +0200 Subject: [PATCH 037/498] Polish Gradle configuration for deployment --- spring-web-reactive/build.gradle | 5 +++++ spring-web-reactive/gradle.properties | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index 2e6c3722eb..5619827d0b 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -12,6 +12,11 @@ apply plugin: 'propdeps' apply plugin: 'propdeps-idea' apply plugin: 'propdeps-maven' +jar { + baseName = 'spring-reactive' +} + +group = 'org.springframework.reactive' repositories { mavenCentral() diff --git a/spring-web-reactive/gradle.properties b/spring-web-reactive/gradle.properties index ae8f3ee0fd..09f2a8e531 100644 --- a/spring-web-reactive/gradle.properties +++ b/spring-web-reactive/gradle.properties @@ -1,2 +1,2 @@ version=0.1.0.BUILD-SNAPSHOT -group=org.springframework.reactive + From 77c5b3fd657e731a560351d764f12e2ace9b595a Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Thu, 17 Sep 2015 14:25:57 -0400 Subject: [PATCH 038/498] Fix RxNetty tests by finding a new port for each test --- .../RequestMappingIntegrationTests.java | 41 +++++++++++++++++-- .../AbstractHttpHandlerIntegrationTests.java | 5 ++- .../web/http/EchoHandlerIntegrationTests.java | 1 - 3 files changed, 40 insertions(+), 7 deletions(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingIntegrationTests.java index 00f5c8a698..98c8fcd39a 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingIntegrationTests.java @@ -150,10 +150,25 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati } @Test - public void capitalize() throws Exception { + public void publisherCapitalize() throws Exception { + capitalize("http://localhost:" + port + "/publisher-capitalize"); + } + + @Test + public void observableCapitalize() throws Exception { + capitalize("http://localhost:" + port + "/observable-capitalize"); + } + + @Test + public void streamCapitalize() throws Exception { + capitalize("http://localhost:" + port + "/stream-capitalize"); + } + + + public void capitalize(String requestUrl) throws Exception { RestTemplate restTemplate = new RestTemplate(); - URI url = new URI("http://localhost:" + port + "/capitalize"); + URI url = new URI(requestUrl); List persons = Arrays.asList(new Person("Robert"), new Person("Marie")); RequestEntity> request = RequestEntity .post(url) @@ -208,9 +223,27 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati return Streams.just(new Person("Robert"), new Person("Marie")); } - @RequestMapping("/capitalize") + @RequestMapping("/publisher-capitalize") @ResponseBody - public Observable capitalize(@RequestBody Observable persons) { + public Publisher publisherCapitalize(@RequestBody Publisher persons) { + return Streams.wrap(persons).map(person -> { + person.setName(person.getName().toUpperCase()); + return person; + }); + } + + @RequestMapping("/observable-capitalize") + @ResponseBody + public Observable observableCapitalize(@RequestBody Observable persons) { + return persons.map(person -> { + person.setName(person.getName().toUpperCase()); + return person; + }); + } + + @RequestMapping("/stream-capitalize") + @ResponseBody + public Stream streamCapitalize(@RequestBody Stream persons) { return persons.map(person -> { person.setName(person.getName().toUpperCase()); return person; diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/AbstractHttpHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/AbstractHttpHandlerIntegrationTests.java index 76512ed0c6..240c0ff82b 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/AbstractHttpHandlerIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/AbstractHttpHandlerIntegrationTests.java @@ -27,7 +27,7 @@ import org.springframework.util.SocketUtils; @RunWith(Parameterized.class) public abstract class AbstractHttpHandlerIntegrationTests { - protected static int port = SocketUtils.findAvailableTcpPort(); + protected int port; @Parameterized.Parameter(0) public HttpServer server; @@ -45,7 +45,8 @@ public abstract class AbstractHttpHandlerIntegrationTests { @Before public void setup() throws Exception { - this.server.setPort(port); + this.port = SocketUtils.findAvailableTcpPort(); + this.server.setPort(this.port); this.server.setHandler(createHttpHandler()); this.server.afterPropertiesSet(); this.server.start(); diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/EchoHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/EchoHandlerIntegrationTests.java index 2011b613f5..e834ae3166 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/EchoHandlerIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/EchoHandlerIntegrationTests.java @@ -44,7 +44,6 @@ public class EchoHandlerIntegrationTests extends AbstractHttpHandlerIntegrationT @Test - @Ignore public void echoBytes() throws Exception { RestTemplate restTemplate = new RestTemplate(); From 9cbe98459868efc1198341cc40a65fc95aa905dd Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Mon, 21 Sep 2015 09:46:36 +0200 Subject: [PATCH 039/498] Create README.md --- spring-web-reactive/README.md | 43 +++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 spring-web-reactive/README.md diff --git a/spring-web-reactive/README.md b/spring-web-reactive/README.md new file mode 100644 index 0000000000..64c929c5e6 --- /dev/null +++ b/spring-web-reactive/README.md @@ -0,0 +1,43 @@ +Spring Reactive is a sandbox for experimenting on the reactive support intended to be part +of Spring Framework 5. For more information about this topic, you can have a look to +[Intro to Reactive programming][] and [Reactive Web Applications][] talks. + +## Downloading Artifacts +Artifacts are not yet available, so you have to build the project from source for now. + +## Building from Source +Spring Reactive uses a [Gradle][]-based build system. In the instructions +below, `./gradlew` is invoked from the root of the source tree and serves as +a cross-platform, self-contained bootstrap mechanism for the build. + +You can check the current build status on this [Bamboo Spring Reactive build][]. + +### Prerequisites + +[Git][] and [JDK 8 update 20 or later][JDK8 build] + +Be sure that your `JAVA_HOME` environment variable points to the `jdk1.8.0` folder +extracted from the JDK download. + +### Install all spring-\* jars into your local Maven cache +`./gradlew install` + +### Compile and test; build all jars, distribution zips, and docs +`./gradlew build` + +## Contributing +Feel free to send us your feedback on the [issue tracker][]; [Pull requests][] are welcome. + +## License +The Spring Reactive is released under version 2.0 of the [Apache License][]. + + +[Gradle]: http://gradle.org +[Bamboo Spring Reactive build]: https://build.spring.io/browse/SR-PUB +[Git]: http://help.github.com/set-up-git-redirect +[JDK8 build]: http://www.oracle.com/technetwork/java/javase/downloads +[Intro to Reactive programming]: http://fr.slideshare.net/StphaneMaldini/intro-to-reactive-programming-52821416 +[Reactive Web Applications]: http://fr.slideshare.net/rstoya05/reactive-web-applications +[issue tracker]: https://github.com/spring-projects/spring-reactive/issues +[Pull requests]: http://help.github.com/send-pull-requests +[Apache License]: http://www.apache.org/licenses/LICENSE-2.0 From 110d9d7cd906c524af5053329a65ef4e1bd9f7eb Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Fri, 18 Sep 2015 16:39:23 -0400 Subject: [PATCH 040/498] Use ByteBuffer instead of byte[] --- .../reactive/codec/decoder/Jaxb2Decoder.java | 5 +- .../io/ByteArrayPublisherOutputStream.java | 57 --------- ...va => ByteBufferPublisherInputStream.java} | 23 ++-- .../RequestBodyArgumentResolver.java | 2 +- .../annotation/ResponseBodyResultHandler.java | 3 +- .../reactive/web/http/ServerHttpRequest.java | 4 +- .../reactive/web/http/ServerHttpResponse.java | 4 +- .../rxnetty/RxNettyServerHttpRequest.java | 9 +- .../rxnetty/RxNettyServerHttpResponse.java | 7 +- .../http/servlet/RequestBodyPublisher.java | 9 +- .../http/servlet/ResponseBodySubscriber.java | 11 +- .../servlet/ServletServerHttpRequest.java | 7 +- .../servlet/ServletServerHttpResponse.java | 3 +- .../ByteArrayPublisherInputStreamTests.java | 115 ------------------ ...mpleUrlHandlerMappingIntegrationTests.java | 5 +- .../reactive/web/http/RandomHandler.java | 10 +- .../reactive/web/http/XmlHandler.java | 15 ++- 17 files changed, 65 insertions(+), 224 deletions(-) delete mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteArrayPublisherOutputStream.java rename spring-web-reactive/src/main/java/org/springframework/reactive/io/{ByteArrayPublisherInputStream.java => ByteBufferPublisherInputStream.java} (86%) delete mode 100644 spring-web-reactive/src/test/java/org/springframework/reactive/io/ByteArrayPublisherInputStreamTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/Jaxb2Decoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/Jaxb2Decoder.java index 462baab1c9..d0d930c361 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/Jaxb2Decoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/Jaxb2Decoder.java @@ -42,7 +42,7 @@ import org.springframework.core.ResolvableType; import org.springframework.http.MediaType; import org.springframework.reactive.codec.CodecException; import org.springframework.reactive.codec.encoder.Jaxb2Encoder; -import org.springframework.reactive.io.ByteArrayPublisherInputStream; +import org.springframework.reactive.io.ByteBufferPublisherInputStream; import org.springframework.util.Assert; /** @@ -63,10 +63,9 @@ public class Jaxb2Decoder implements ByteToMessageDecoder { @Override public Publisher decode(Publisher inputStream, ResolvableType type, MediaType mediaType, Object... hints) { - Stream stream = Streams.wrap(inputStream).map(chunk -> new Buffer(chunk).asBytes()); Class outputClass = type.getRawClass(); try { - Source source = processSource(new StreamSource(new ByteArrayPublisherInputStream(stream))); + Source source = processSource(new StreamSource(new ByteBufferPublisherInputStream(inputStream))); Unmarshaller unmarshaller = createUnmarshaller(outputClass); if (outputClass.isAnnotationPresent(XmlRootElement.class)) { return Streams.just(unmarshaller.unmarshal(source)); diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteArrayPublisherOutputStream.java b/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteArrayPublisherOutputStream.java deleted file mode 100644 index 806a13c238..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteArrayPublisherOutputStream.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2002-2015 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.reactive.io; - -import java.io.IOException; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.List; - -import org.reactivestreams.Publisher; -import reactor.rx.Streams; - -/** - * {@code OutputStream} implementation that stores all written bytes, to be retrieved - * using {@link #toByteArrayPublisher()}. - * @author Arjen Poutsma - */ -public class ByteArrayPublisherOutputStream extends OutputStream { - - private final List buffers = new ArrayList<>(); - - - /** - * Returns the written data as a {@code Publisher}. - * @return a publisher for the written bytes - */ - public Publisher toByteArrayPublisher() { - return Streams.from(buffers); - } - - @Override - public void write(int b) throws IOException { - write(new byte[]{(byte) b}); - } - - @Override - public void write(byte[] b, int off, int len) throws IOException { - byte[] copy = new byte[len - off]; - System.arraycopy(b, off, copy, 0, len); - buffers.add(copy); - } - -} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteArrayPublisherInputStream.java b/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteBufferPublisherInputStream.java similarity index 86% rename from spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteArrayPublisherInputStream.java rename to spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteBufferPublisherInputStream.java index 2156f654bf..e5ccea71e4 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteArrayPublisherInputStream.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteBufferPublisherInputStream.java @@ -16,9 +16,9 @@ package org.springframework.reactive.io; -import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import java.nio.ByteBuffer; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; @@ -32,13 +32,14 @@ import org.springframework.util.Assert; /** * {@code InputStream} implementation based on a byte array {@link Publisher}. * @author Arjen Poutsma + * @author Sebastien Deleuze */ -public class ByteArrayPublisherInputStream extends InputStream { +public class ByteBufferPublisherInputStream extends InputStream { - private final BlockingQueue> queue = + private final BlockingQueue> queue = new LinkedBlockingQueue<>(); - private ByteArrayInputStream currentStream; + private ByteBufferInputStream currentStream; private boolean completed; @@ -47,7 +48,7 @@ public class ByteArrayPublisherInputStream extends InputStream { * Creates a new {@code ByteArrayPublisherInputStream} based on the given publisher. * @param publisher the publisher to use */ - public ByteArrayPublisherInputStream(Publisher publisher) { + public ByteBufferPublisherInputStream(Publisher publisher) { this(publisher, 1); } @@ -57,7 +58,7 @@ public class ByteArrayPublisherInputStream extends InputStream { * @param requestSize the {@linkplain Subscription#request(long) request size} to use * on the publisher */ - public ByteArrayPublisherInputStream(Publisher publisher, long requestSize) { + public ByteBufferPublisherInputStream(Publisher publisher, long requestSize) { Assert.notNull(publisher, "'publisher' must not be null"); publisher.subscribe(new BlockingQueueSubscriber(requestSize)); @@ -130,11 +131,11 @@ public class ByteArrayPublisherInputStream extends InputStream { } else { // take() blocks, but that's OK since this is a *blocking* InputStream - PublisherSignal signal = this.queue.take(); + PublisherSignal signal = this.queue.take(); if (signal.isData()) { - byte[] data = signal.data(); - this.currentStream = new ByteArrayInputStream(data); + ByteBuffer data = signal.data(); + this.currentStream = new ByteBufferInputStream(data); return this.currentStream; } else if (signal.isComplete()) { @@ -159,7 +160,7 @@ public class ByteArrayPublisherInputStream extends InputStream { throw new IOException(); } - private class BlockingQueueSubscriber implements Subscriber { + private class BlockingQueueSubscriber implements Subscriber { private final long requestSize; @@ -177,7 +178,7 @@ public class ByteArrayPublisherInputStream extends InputStream { } @Override - public void onNext(byte[] bytes) { + public void onNext(ByteBuffer bytes) { try { queue.put(PublisherSignal.data(bytes)); this.subscription.request(requestSize); diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java index 6a7159d8fd..ce8d83983e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java @@ -78,7 +78,7 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve ByteToMessageDecoder deserializer = resolveDeserializers(request, type, mediaType, hints.toArray()); if (deserializer != null) { - Publisher inputStream = Streams.wrap(request.getBody()).map(bytes -> ByteBuffer.wrap(bytes)); + Publisher inputStream = request.getBody(); List> preProcessors = resolvePreProcessors(request, type, mediaType, hints.toArray()); for (ByteToMessageDecoder preProcessor : preProcessors) { inputStream = preProcessor.decode(inputStream, type, mediaType, hints.toArray()); diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java index 69f8d483c9..387fa657e6 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java @@ -23,7 +23,6 @@ import java.util.Collections; import java.util.List; import org.reactivestreams.Publisher; -import reactor.io.buffer.Buffer; import reactor.rx.Streams; import rx.Observable; import rx.RxReactiveStreams; @@ -125,7 +124,7 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered outputStream = postProcessor.encode(outputStream, type, mediaType, hints.toArray()); } response.getHeaders().setContentType(mediaType); - return response.writeWith(Streams.wrap(outputStream).map(buffer -> new Buffer(buffer).asBytes())); + return response.writeWith(Streams.wrap(outputStream)); } return Streams.fail(new IllegalStateException( "Return value type not supported: " + returnType)); diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/ServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/ServerHttpRequest.java index c2e24d2f9c..bfbf9771ef 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/ServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/ServerHttpRequest.java @@ -15,6 +15,8 @@ */ package org.springframework.reactive.web.http; +import java.nio.ByteBuffer; + import org.reactivestreams.Publisher; /** @@ -23,6 +25,6 @@ import org.reactivestreams.Publisher; */ public interface ServerHttpRequest extends HttpRequest { - Publisher getBody(); + Publisher getBody(); } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/ServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/ServerHttpResponse.java index a485b898eb..3063aa2b2f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/ServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/ServerHttpResponse.java @@ -15,6 +15,8 @@ */ package org.springframework.reactive.web.http; +import java.nio.ByteBuffer; + import org.reactivestreams.Publisher; import org.springframework.http.HttpStatus; @@ -26,6 +28,6 @@ public interface ServerHttpResponse extends HttpMessage { void setStatusCode(HttpStatus status); - Publisher writeWith(Publisher contentPublisher); + Publisher writeWith(Publisher contentPublisher); } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpRequest.java index 4c80e78c50..e422bb7502 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpRequest.java @@ -17,6 +17,7 @@ package org.springframework.reactive.web.http.rxnetty; import java.net.URI; import java.net.URISyntaxException; +import java.nio.ByteBuffer; import io.netty.buffer.ByteBuf; import io.reactivex.netty.protocol.http.server.HttpServerRequest; @@ -74,12 +75,8 @@ public class RxNettyServerHttpRequest implements ServerHttpRequest { } @Override - public Publisher getBody() { - Observable bytesContent = this.request.getContent().map(byteBuf -> { - byte[] copy = new byte[byteBuf.readableBytes()]; - byteBuf.readBytes(copy); - return copy; - }); + public Publisher getBody() { + Observable bytesContent = this.request.getContent().map(byteBuf -> byteBuf.nioBuffer()); return rx.RxReactiveStreams.toPublisher(bytesContent); } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpResponse.java index ca41e01e55..f8e941d639 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpResponse.java @@ -15,9 +15,12 @@ */ package org.springframework.reactive.web.http.rxnetty; +import java.nio.ByteBuffer; + import io.netty.handler.codec.http.HttpResponseStatus; import io.reactivex.netty.protocol.http.server.HttpServerResponse; import org.reactivestreams.Publisher; +import reactor.io.buffer.Buffer; import rx.Observable; import rx.RxReactiveStreams; @@ -56,9 +59,9 @@ public class RxNettyServerHttpResponse implements ServerHttpResponse { } @Override - public Publisher writeWith(Publisher contentPublisher) { + public Publisher writeWith(Publisher contentPublisher) { writeHeaders(); - Observable contentObservable = RxReactiveStreams.toObservable(contentPublisher); + Observable contentObservable = RxReactiveStreams.toObservable(contentPublisher).map(content -> new Buffer(content).asBytes()); return RxReactiveStreams.toPublisher(this.response.writeBytes(contentObservable)); } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/RequestBodyPublisher.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/RequestBodyPublisher.java index 35cc27fdcb..8ce7c8c827 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/RequestBodyPublisher.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/RequestBodyPublisher.java @@ -17,6 +17,7 @@ package org.springframework.reactive.web.http.servlet; import java.io.IOException; +import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.util.Arrays; import javax.servlet.ReadListener; @@ -33,7 +34,7 @@ import org.springframework.reactive.util.DemandCounter; /** * @author Arjen Poutsma */ -public class RequestBodyPublisher implements ReadListener, Publisher { +public class RequestBodyPublisher implements ReadListener, Publisher { private final Charset UTF_8 = Charset.forName("UTF-8"); @@ -45,7 +46,7 @@ public class RequestBodyPublisher implements ReadListener, Publisher { private final DemandCounter demand = new DemandCounter(); - private Subscriber subscriber; + private Subscriber subscriber; private boolean stalled; @@ -57,7 +58,7 @@ public class RequestBodyPublisher implements ReadListener, Publisher { } @Override - public void subscribe(Subscriber subscriber) { + public void subscribe(Subscriber subscriber) { if (subscriber == null) { throw new NullPointerException(); } @@ -103,7 +104,7 @@ public class RequestBodyPublisher implements ReadListener, Publisher { // logger.debug("Next: " + new String(copy, UTF_8)); - this.subscriber.onNext(copy); + this.subscriber.onNext(ByteBuffer.wrap(copy)); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ResponseBodySubscriber.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ResponseBodySubscriber.java index 39aaf86974..4d7ca62011 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ResponseBodySubscriber.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ResponseBodySubscriber.java @@ -17,6 +17,7 @@ package org.springframework.reactive.web.http.servlet; import java.io.IOException; +import java.nio.ByteBuffer; import javax.servlet.ServletOutputStream; import javax.servlet.WriteListener; @@ -24,13 +25,14 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; +import reactor.io.buffer.Buffer; import org.springframework.util.Assert; /** * @author Arjen Poutsma */ -public class ResponseBodySubscriber implements WriteListener, Subscriber { +public class ResponseBodySubscriber implements WriteListener, Subscriber { private static final Log logger = LogFactory.getLog(ResponseBodySubscriber.class); @@ -38,7 +40,7 @@ public class ResponseBodySubscriber implements WriteListener, Subscriber private Subscription subscription; - private byte[] buffer; + private ByteBuffer buffer; private volatile boolean subscriberComplete = false; @@ -53,8 +55,7 @@ public class ResponseBodySubscriber implements WriteListener, Subscriber } @Override - public void onNext(byte[] bytes) { - logger.debug("Next: " + bytes.length + " bytes"); + public void onNext(ByteBuffer bytes) { Assert.isNull(buffer); @@ -87,7 +88,7 @@ public class ResponseBodySubscriber implements WriteListener, Subscriber if (ready) { if (this.buffer != null) { - output.write(this.buffer); + output.write(new Buffer(this.buffer).asBytes()); this.buffer = null; if (!subscriberComplete) { diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpRequest.java index 583449e235..ebeb95d510 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpRequest.java @@ -17,6 +17,7 @@ package org.springframework.reactive.web.http.servlet; import java.net.URI; import java.net.URISyntaxException; +import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.util.Enumeration; import java.util.Map; @@ -39,12 +40,12 @@ public class ServletServerHttpRequest implements ServerHttpRequest { private final HttpServletRequest servletRequest; - private final Publisher requestBodyPublisher; + private final Publisher requestBodyPublisher; private HttpHeaders headers; - public ServletServerHttpRequest(HttpServletRequest servletRequest, Publisher requestBodyPublisher) { + public ServletServerHttpRequest(HttpServletRequest servletRequest, Publisher requestBodyPublisher) { Assert.notNull(servletRequest, "HttpServletRequest must not be null"); this.servletRequest = servletRequest; this.requestBodyPublisher = requestBodyPublisher; @@ -111,7 +112,7 @@ public class ServletServerHttpRequest implements ServerHttpRequest { } @Override - public Publisher getBody() { + public Publisher getBody() { return this.requestBodyPublisher; } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpResponse.java index f4c43fada8..0895d62b69 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpResponse.java @@ -15,6 +15,7 @@ */ package org.springframework.reactive.web.http.servlet; +import java.nio.ByteBuffer; import java.util.List; import java.util.Map; import javax.servlet.http.HttpServletResponse; @@ -60,7 +61,7 @@ public class ServletServerHttpResponse implements ServerHttpResponse { } @Override - public Publisher writeWith(final Publisher contentPublisher) { + public Publisher writeWith(final Publisher contentPublisher) { writeHeaders(); return (s -> contentPublisher.subscribe(responseSubscriber)); } diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/io/ByteArrayPublisherInputStreamTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/io/ByteArrayPublisherInputStreamTests.java deleted file mode 100644 index 362355b089..0000000000 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/io/ByteArrayPublisherInputStreamTests.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright 2002-2015 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.reactive.io; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; - -import org.junit.Before; -import org.junit.Test; -import org.reactivestreams.Publisher; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; -import reactor.rx.Stream; -import reactor.rx.Streams; - -import org.springframework.util.FileCopyUtils; - -import static org.junit.Assert.*; - -/** - * @author Arjen Poutsma - */ -public class ByteArrayPublisherInputStreamTests { - - - private ByteArrayPublisherInputStream is; - - @Before - public void createStream() { - Stream stream = - Streams.just(new byte[]{'a', 'b', 'c'}, new byte[]{'d', 'e'}); - - is = new ByteArrayPublisherInputStream(stream); - } - - @Test - public void reactor() throws Exception { - assertEquals(3, is.available()); - - int ch = is.read(); - assertEquals('a', ch); - ch = is.read(); - assertEquals('b', ch); - ch = is.read(); - assertEquals('c', ch); - - assertEquals(2, is.available()); - ch = is.read(); - assertEquals('d', ch); - ch = is.read(); - assertEquals('e', ch); - - ch = is.read(); - assertEquals(-1, ch); - - assertEquals(0, is.available()); - } - - @Test - public void copy() throws Exception { - ByteArrayPublisherOutputStream os = new ByteArrayPublisherOutputStream(); - - FileCopyUtils.copy(is, os); - - Publisher publisher = os.toByteArrayPublisher(); - List result = new ArrayList<>(); - AtomicBoolean complete = new AtomicBoolean(); - - publisher.subscribe(new Subscriber() { - - @Override - public void onSubscribe(Subscription s) { - s.request(Long.MAX_VALUE); - } - - @Override - public void onNext(byte[] bytes) { - result.add(bytes); - } - - @Override - public void onError(Throwable t) { - fail(t.getMessage()); - } - - @Override - public void onComplete() { - complete.set(true); - } - }); - - while (!complete.get()) { - - } - assertArrayEquals(result.get(0), new byte[]{'a', 'b', 'c'}); - assertArrayEquals(result.get(1), new byte[]{'d', 'e'}); - - } - -} \ No newline at end of file diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/handler/SimpleUrlHandlerMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/handler/SimpleUrlHandlerMappingIntegrationTests.java index 8bbe9841fb..b9700a02f1 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/handler/SimpleUrlHandlerMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/handler/SimpleUrlHandlerMappingIntegrationTests.java @@ -22,6 +22,7 @@ import java.util.Map; import org.junit.Test; import org.reactivestreams.Publisher; +import reactor.io.buffer.Buffer; import reactor.rx.Streams; import org.springframework.http.RequestEntity; @@ -97,7 +98,7 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler @Override public Publisher handle(ServerHttpRequest request, ServerHttpResponse response) { - return response.writeWith(Streams.just("foo".getBytes(UTF_8))); + return response.writeWith(Streams.just(Buffer.wrap("foo").byteBuffer())); } } @@ -105,7 +106,7 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler @Override public Publisher handle(ServerHttpRequest request, ServerHttpResponse response) { - return response.writeWith(Streams.just("bar".getBytes(UTF_8))); + return response.writeWith(Streams.just(Buffer.wrap("bar").byteBuffer())); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/RandomHandler.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/RandomHandler.java index 111b00b70b..69244b098c 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/RandomHandler.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/RandomHandler.java @@ -16,6 +16,7 @@ package org.springframework.reactive.web.http; +import java.nio.ByteBuffer; import java.util.Random; import org.apache.commons.logging.Log; @@ -23,6 +24,7 @@ import org.apache.commons.logging.LogFactory; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; +import reactor.io.buffer.Buffer; import reactor.rx.Streams; import static org.junit.Assert.assertEquals; @@ -41,7 +43,7 @@ public class RandomHandler implements HttpHandler { @Override public Publisher handle(ServerHttpRequest request, ServerHttpResponse response) { - request.getBody().subscribe(new Subscriber() { + request.getBody().subscribe(new Subscriber() { private Subscription s; private int requestSize = 0; @@ -53,8 +55,8 @@ public class RandomHandler implements HttpHandler { } @Override - public void onNext(byte[] bytes) { - requestSize += bytes.length; + public void onNext(ByteBuffer bytes) { + requestSize += new Buffer(bytes).limit(); } @Override @@ -71,7 +73,7 @@ public class RandomHandler implements HttpHandler { }); response.getHeaders().setContentLength(RESPONSE_SIZE); - return response.writeWith(Streams.just(randomBytes())); + return response.writeWith(Streams.just(ByteBuffer.wrap(randomBytes()))); } private byte[] randomBytes() { diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/XmlHandler.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/XmlHandler.java index 391f6acbff..dcf95a8dc2 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/XmlHandler.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/XmlHandler.java @@ -23,13 +23,14 @@ import javax.xml.bind.Unmarshaller; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.reactivestreams.Publisher; -import reactor.rx.Streams; import org.springframework.http.MediaType; -import org.springframework.reactive.io.ByteArrayPublisherInputStream; -import org.springframework.reactive.io.ByteArrayPublisherOutputStream; +import org.springframework.reactive.io.BufferOutputStream; +import org.springframework.reactive.io.ByteBufferPublisherInputStream; import static org.junit.Assert.fail; +import reactor.io.buffer.Buffer; +import reactor.rx.Streams; /** * @author Arjen Poutsma @@ -48,7 +49,7 @@ public class XmlHandler implements HttpHandler { Runnable r = () -> { try { - ByteArrayPublisherInputStream bis = new ByteArrayPublisherInputStream(request.getBody()); + ByteBufferPublisherInputStream bis = new ByteBufferPublisherInputStream(request.getBody()); XmlHandlerIntegrationTests.Person johnDoe = (XmlHandlerIntegrationTests.Person) unmarshaller.unmarshal(bis); @@ -66,11 +67,13 @@ public class XmlHandler implements HttpHandler { response.getHeaders().setContentType(MediaType.APPLICATION_XML); XmlHandlerIntegrationTests.Person janeDoe = new XmlHandlerIntegrationTests.Person("Jane Doe"); - ByteArrayPublisherOutputStream bos = new ByteArrayPublisherOutputStream(); + Buffer buffer = new Buffer(); + BufferOutputStream bos = new BufferOutputStream(buffer); marshaller.marshal(janeDoe, bos); bos.close(); + buffer.flip(); - return response.writeWith(bos.toByteArrayPublisher()); + return response.writeWith(Streams.just(buffer.byteBuffer())); } catch (Exception ex) { logger.error(ex, ex); From d3e5d9dd0328cbc9bd45e6a61ed6eb44a3558ddb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Evangelista?= Date: Tue, 22 Sep 2015 08:13:04 +0200 Subject: [PATCH 041/498] Polishing --- .../reactive/codec/decoder/Jaxb2Decoder.java | 5 +--- .../codec/encoder/JacksonJsonEncoder.java | 4 +-- .../reactive/codec/encoder/Jaxb2Encoder.java | 3 ++- .../codec/encoder/JsonObjectEncoder.java | 3 ++- .../reactive/codec/support/HintUtils.java | 3 +-- .../reactive/io/BufferOutputStream.java | 25 +++++++++++-------- .../reactive/io/ByteBufferInputStream.java | 17 +++++++++---- .../reactive/util/PublisherSignal.java | 4 ++- .../servlet/AsyncContextSynchronizer.java | 1 + .../web/http/servlet/HttpHandlerServlet.java | 3 ++- .../http/servlet/RequestBodyPublisher.java | 3 +-- .../servlet/ServletServerHttpRequest.java | 5 ++-- .../servlet/ServletServerHttpResponse.java | 1 + .../web/http/EchoHandlerIntegrationTests.java | 1 - .../web/http/XmlHandlerIntegrationTests.java | 2 -- 15 files changed, 45 insertions(+), 35 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/Jaxb2Decoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/Jaxb2Decoder.java index d0d930c361..77ae96a315 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/Jaxb2Decoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/Jaxb2Decoder.java @@ -19,6 +19,7 @@ package org.springframework.reactive.codec.decoder; import java.nio.ByteBuffer; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; + import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBElement; import javax.xml.bind.JAXBException; @@ -34,8 +35,6 @@ import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.XMLReader; import org.xml.sax.helpers.XMLReaderFactory; -import reactor.io.buffer.Buffer; -import reactor.rx.Stream; import reactor.rx.Streams; import org.springframework.core.ResolvableType; @@ -55,7 +54,6 @@ public class Jaxb2Decoder implements ByteToMessageDecoder { private final ConcurrentMap, JAXBContext> jaxbContexts = new ConcurrentHashMap, JAXBContext>(64); - @Override public boolean canDecode(ResolvableType type, MediaType mediaType, Object... hints) { return mediaType.isCompatibleWith(MediaType.APPLICATION_XML) || mediaType.isCompatibleWith(MediaType.TEXT_XML); @@ -127,5 +125,4 @@ public class Jaxb2Decoder implements ByteToMessageDecoder { } return jaxbContext; } - } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoder.java index 931b853f56..3fb9200140 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoder.java @@ -41,7 +41,6 @@ public class JacksonJsonEncoder implements MessageToByteEncoder { private final ObjectMapper mapper; - public JacksonJsonEncoder() { this(new ObjectMapper()); } @@ -50,7 +49,6 @@ public class JacksonJsonEncoder implements MessageToByteEncoder { this.mapper = mapper; } - @Override public boolean canEncode(ResolvableType type, MediaType mediaType, Object... hints) { return mediaType.isCompatibleWith(MediaType.APPLICATION_JSON); @@ -58,7 +56,7 @@ public class JacksonJsonEncoder implements MessageToByteEncoder { @Override public Publisher encode(Publisher messageStream, ResolvableType type, MediaType mediaType, Object... hints) { - Stream stream = Streams.wrap(messageStream).map(value -> { + Stream stream = Streams.wrap(messageStream).map(value -> { Buffer buffer = new Buffer(); BufferOutputStream outputStream = new BufferOutputStream(buffer); try { diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/Jaxb2Encoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/Jaxb2Encoder.java index b0adac193f..a1fffb1b9e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/Jaxb2Encoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/Jaxb2Encoder.java @@ -20,6 +20,7 @@ import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; + import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.MarshalException; @@ -47,7 +48,6 @@ public class Jaxb2Encoder implements MessageToByteEncoder { private final ConcurrentMap, JAXBContext> jaxbContexts = new ConcurrentHashMap, JAXBContext>(64); - @Override public boolean canEncode(ResolvableType type, MediaType mediaType, Object... hints) { return mediaType.isCompatibleWith(MediaType.APPLICATION_XML) || mediaType.isCompatibleWith(MediaType.TEXT_XML); @@ -106,3 +106,4 @@ public class Jaxb2Encoder implements MessageToByteEncoder { } } + diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JsonObjectEncoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JsonObjectEncoder.java index 974f00b943..827ef31ed4 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JsonObjectEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JsonObjectEncoder.java @@ -19,7 +19,6 @@ package org.springframework.reactive.codec.encoder; import java.nio.ByteBuffer; import org.reactivestreams.Publisher; - import rx.Observable; import rx.RxReactiveStreams; @@ -37,7 +36,9 @@ import org.springframework.reactive.codec.decoder.JsonObjectDecoder; public class JsonObjectEncoder implements MessageToByteEncoder { private final ByteBuffer START_ARRAY = ByteBuffer.wrap("[".getBytes()); + private final ByteBuffer END_ARRAY = ByteBuffer.wrap("]".getBytes()); + private final ByteBuffer COMMA = ByteBuffer.wrap(",".getBytes()); diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/support/HintUtils.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/support/HintUtils.java index ab63b2f659..a15c57b77f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/support/HintUtils.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/support/HintUtils.java @@ -23,7 +23,6 @@ import org.springframework.reactive.codec.encoder.MessageToByteEncoder; * Utility methods for dealing with codec hints. * * @author Sebastien Deleuze - * @see Hints * @see MessageToByteEncoder * @see ByteToMessageDecoder */ @@ -36,7 +35,7 @@ public abstract class HintUtils { public static T getHintByClass(Class clazz, Object[] hints, T defaultValue) { for (Object hint : hints) { if (hint.getClass().isAssignableFrom(clazz)) { - return (T)hint; + return (T) hint; } } return defaultValue; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/io/BufferOutputStream.java b/spring-web-reactive/src/main/java/org/springframework/reactive/io/BufferOutputStream.java index 4d51ec8788..6c97f7b612 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/io/BufferOutputStream.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/io/BufferOutputStream.java @@ -22,24 +22,29 @@ import java.io.OutputStream; import reactor.io.buffer.Buffer; /** + * Simple extension of {@link OutputStream} that uses {@link Buffer} to stream + * the content + * * @author Sebastien Deleuze */ public class BufferOutputStream extends OutputStream { - private Buffer buffer; + private Buffer buffer; public BufferOutputStream(Buffer buffer) { - this.buffer = buffer; - } + this.buffer = buffer; + } - public void write(int b) throws IOException { - buffer.append(b); - } + @Override + public void write(int b) throws IOException { + buffer.append(b); + } - public void write(byte[] bytes, int off, int len) - throws IOException { - buffer.append(bytes, off, len); - } + @Override + public void write(byte[] bytes, int off, int len) + throws IOException { + buffer.append(bytes, off, len); + } public Buffer getBuffer() { return buffer; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteBufferInputStream.java b/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteBufferInputStream.java index dbd2a5e16d..a006596b2a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteBufferInputStream.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteBufferInputStream.java @@ -26,16 +26,23 @@ import java.nio.ByteBuffer; * * From Jackson ByteBufferBackedInputStream */ -public class ByteBufferInputStream extends InputStream { +public class ByteBufferInputStream extends InputStream { protected final ByteBuffer b; - public ByteBufferInputStream(ByteBuffer buf) { b = buf; } - - @Override public int available() { return b.remaining(); } + public ByteBufferInputStream(ByteBuffer buf) { + b = buf; + } @Override - public int read() throws IOException { return b.hasRemaining() ? (b.get() & 0xFF) : -1; } + public int available() { + return b.remaining(); + } + + @Override + public int read() throws IOException { + return b.hasRemaining() ? (b.get() & 0xFF) : -1; + } @Override public int read(byte[] bytes, int off, int len) throws IOException { diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/util/PublisherSignal.java b/spring-web-reactive/src/main/java/org/springframework/reactive/util/PublisherSignal.java index 9fdd51c963..46ab3572eb 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/util/PublisherSignal.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/util/PublisherSignal.java @@ -16,11 +16,13 @@ package org.springframework.reactive.util; +import org.reactivestreams.Publisher; + import org.springframework.util.Assert; /** * Represents a signal value object, useful for wrapping signals as published by a {@link - * #Publisher()}. Mostly used to store signals in buffers. + * Publisher}. Mostly used to store signals in buffers. * @author Arjen Poutsma */ public abstract class PublisherSignal { diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/AsyncContextSynchronizer.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/AsyncContextSynchronizer.java index 65240b963c..98811b67a2 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/AsyncContextSynchronizer.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/AsyncContextSynchronizer.java @@ -18,6 +18,7 @@ package org.springframework.reactive.web.http.servlet; import java.io.IOException; import java.util.concurrent.atomic.AtomicInteger; + import javax.servlet.AsyncContext; import javax.servlet.ServletInputStream; import javax.servlet.ServletOutputStream; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/HttpHandlerServlet.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/HttpHandlerServlet.java index 98fcb61abe..9bcddbfd6f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/HttpHandlerServlet.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/HttpHandlerServlet.java @@ -17,6 +17,7 @@ package org.springframework.reactive.web.http.servlet; import java.io.IOException; + import javax.servlet.AsyncContext; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; @@ -36,7 +37,7 @@ import org.springframework.reactive.web.http.HttpHandler; * @author Arjen Poutsma * @author Rossen Stoyanchev */ -@WebServlet(asyncSupported = true ) +@WebServlet(asyncSupported = true) public class HttpHandlerServlet extends HttpServlet { private static final int BUFFER_SIZE = 8192; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/RequestBodyPublisher.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/RequestBodyPublisher.java index 8ce7c8c827..c685882a09 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/RequestBodyPublisher.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/RequestBodyPublisher.java @@ -20,6 +20,7 @@ import java.io.IOException; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.util.Arrays; + import javax.servlet.ReadListener; import javax.servlet.ServletInputStream; @@ -36,8 +37,6 @@ import org.springframework.reactive.util.DemandCounter; */ public class RequestBodyPublisher implements ReadListener, Publisher { - private final Charset UTF_8 = Charset.forName("UTF-8"); - private static final Log logger = LogFactory.getLog(RequestBodyPublisher.class); private final AsyncContextSynchronizer synchronizer; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpRequest.java index ebeb95d510..b6aaea8fb7 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpRequest.java @@ -21,6 +21,7 @@ import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.util.Enumeration; import java.util.Map; + import javax.servlet.http.HttpServletRequest; import org.reactivestreams.Publisher; @@ -73,10 +74,10 @@ public class ServletServerHttpRequest implements ServerHttpRequest { public HttpHeaders getHeaders() { if (this.headers == null) { this.headers = new HttpHeaders(); - for (Enumeration headerNames = this.servletRequest.getHeaderNames(); headerNames.hasMoreElements();) { + for (Enumeration headerNames = this.servletRequest.getHeaderNames(); headerNames.hasMoreElements(); ) { String headerName = (String) headerNames.nextElement(); for (Enumeration headerValues = this.servletRequest.getHeaders(headerName); - headerValues.hasMoreElements();) { + headerValues.hasMoreElements(); ) { String headerValue = (String) headerValues.nextElement(); this.headers.add(headerName, headerValue); } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpResponse.java index 0895d62b69..37987c36ee 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpResponse.java @@ -18,6 +18,7 @@ package org.springframework.reactive.web.http.servlet; import java.nio.ByteBuffer; import java.util.List; import java.util.Map; + import javax.servlet.http.HttpServletResponse; import org.reactivestreams.Publisher; diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/EchoHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/EchoHandlerIntegrationTests.java index e834ae3166..270aa1a9da 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/EchoHandlerIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/EchoHandlerIntegrationTests.java @@ -19,7 +19,6 @@ package org.springframework.reactive.web.http; import java.net.URI; import java.util.Random; -import org.junit.Ignore; import org.junit.Test; import org.springframework.http.RequestEntity; diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/XmlHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/XmlHandlerIntegrationTests.java index d703582ea0..004cf6fc4a 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/XmlHandlerIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/XmlHandlerIntegrationTests.java @@ -45,8 +45,6 @@ public class XmlHandlerIntegrationTests extends AbstractHttpHandlerIntegrationTe johnDoe); ResponseEntity response = restTemplate.exchange(request, Person.class); System.out.println(response.getBody()); - - } @XmlRootElement From ba66de0dc3ad332603cd8c4732bcaed4e7759185 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Tue, 22 Sep 2015 12:11:59 +0200 Subject: [PATCH 042/498] Set Gradle project name for CI --- spring-web-reactive/settings.gradle | 1 + 1 file changed, 1 insertion(+) create mode 100644 spring-web-reactive/settings.gradle diff --git a/spring-web-reactive/settings.gradle b/spring-web-reactive/settings.gradle new file mode 100644 index 0000000000..4fd62c48f4 --- /dev/null +++ b/spring-web-reactive/settings.gradle @@ -0,0 +1 @@ +rootProject.name = "spring-reactive" From 6716f969d6a21670cee060dbc5e270f2cc44ad6a Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Tue, 22 Sep 2015 12:27:34 +0200 Subject: [PATCH 043/498] Update README.md with artifact infos --- spring-web-reactive/README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/spring-web-reactive/README.md b/spring-web-reactive/README.md index 64c929c5e6..8b017c24b0 100644 --- a/spring-web-reactive/README.md +++ b/spring-web-reactive/README.md @@ -3,7 +3,11 @@ of Spring Framework 5. For more information about this topic, you can have a loo [Intro to Reactive programming][] and [Reactive Web Applications][] talks. ## Downloading Artifacts -Artifacts are not yet available, so you have to build the project from source for now. +Spring Reactive JAR dependency is available from Spring snapshot repository: + - Repository URL: `https://repo.spring.io/snapshot/` + - GroupId: `org.springframework.reactive` + - ArtifactId: `spring-reactive` + - Version: `0.1.0.BUILD-SNAPSHOT` ## Building from Source Spring Reactive uses a [Gradle][]-based build system. In the instructions From f816cc6a516c324875fa409a33d288447c6996f7 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Thu, 24 Sep 2015 23:02:49 +0200 Subject: [PATCH 044/498] Add CompletableFuture/Single/Promise support --- .../codec/decoder/JsonObjectDecoder.java | 3 +- .../codec/encoder/JsonObjectEncoder.java | 3 +- .../reactive/util/CompletableFutureUtils.java | 141 +++++++++++++ .../RequestBodyArgumentResolver.java | 24 ++- .../annotation/ResponseBodyResultHandler.java | 16 +- .../util/CompletableFutureUtilsTests.java | 101 ++++++++++ .../RequestMappingIntegrationTests.java | 190 ++++++++++++------ 7 files changed, 413 insertions(+), 65 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/util/CompletableFutureUtils.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/reactive/util/CompletableFutureUtilsTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JsonObjectDecoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JsonObjectDecoder.java index 6a1f5db21d..4aa1edca46 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JsonObjectDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JsonObjectDecoder.java @@ -25,6 +25,7 @@ import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; import org.reactivestreams.Publisher; import reactor.fn.Function; +import reactor.rx.Promise; import reactor.rx.Streams; import rx.Observable; @@ -90,7 +91,7 @@ public class JsonObjectDecoder implements ByteToMessageDecoder { @Override public boolean canDecode(ResolvableType type, MediaType mediaType, Object... hints) { - return mediaType.isCompatibleWith(MediaType.APPLICATION_JSON) && + return mediaType.isCompatibleWith(MediaType.APPLICATION_JSON) && !Promise.class.isAssignableFrom(type.getRawClass()) && (Observable.class.isAssignableFrom(type.getRawClass()) || Publisher.class.isAssignableFrom(type.getRawClass())); } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JsonObjectEncoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JsonObjectEncoder.java index 827ef31ed4..e162dd9ed3 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JsonObjectEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JsonObjectEncoder.java @@ -19,6 +19,7 @@ package org.springframework.reactive.codec.encoder; import java.nio.ByteBuffer; import org.reactivestreams.Publisher; +import reactor.rx.Promise; import rx.Observable; import rx.RxReactiveStreams; @@ -44,7 +45,7 @@ public class JsonObjectEncoder implements MessageToByteEncoder { @Override public boolean canEncode(ResolvableType type, MediaType mediaType, Object... hints) { - return mediaType.isCompatibleWith(MediaType.APPLICATION_JSON) && + return mediaType.isCompatibleWith(MediaType.APPLICATION_JSON) && !Promise.class.isAssignableFrom(type.getRawClass()) && (Observable.class.isAssignableFrom(type.getRawClass()) || Publisher.class.isAssignableFrom(type.getRawClass())); } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/util/CompletableFutureUtils.java b/spring-web-reactive/src/main/java/org/springframework/reactive/util/CompletableFutureUtils.java new file mode 100644 index 0000000000..e26afa24e5 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/util/CompletableFutureUtils.java @@ -0,0 +1,141 @@ +/* + * Copyright 2002-2015 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.reactive.util; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import reactor.core.support.Exceptions; +import reactor.rx.Stream; +import reactor.rx.action.Action; +import reactor.rx.subscription.ReactiveSubscription; + +import org.springframework.util.Assert; + +/** + * @author Sebastien Deleuze + */ +public class CompletableFutureUtils { + + public static Publisher toPublisher(CompletableFuture future) { + return new CompletableFutureStream(future); + } + + public static CompletableFuture> fromPublisher(Publisher publisher) { + final CompletableFuture> future = new CompletableFuture<>(); + publisher.subscribe(new Subscriber() { + private final List values = new ArrayList<>(); + + @Override + public void onSubscribe(Subscription s) { + s.request(Long.MAX_VALUE); + } + + @Override + public void onNext(T t) { + values.add(t); + } + + @Override + public void onError(Throwable t) { + future.completeExceptionally(t); + } + + @Override + public void onComplete() { + future.complete(values); + } + }); + return future; + } + + public static CompletableFuture fromSinglePublisher(Publisher publisher) { + final CompletableFuture future = new CompletableFuture<>(); + publisher.subscribe(new Subscriber() { + private T value; + + @Override + public void onSubscribe(Subscription s) { + s.request(Long.MAX_VALUE); + } + + @Override + public void onNext(T t) { + Assert.state(value == null, "This publisher should not publish multiple values"); + value = t; + } + + @Override + public void onError(Throwable t) { + future.completeExceptionally(t); + } + + @Override + public void onComplete() { + future.complete(value); + } + }); + return future; + } + + private static class CompletableFutureStream extends Stream { + + private final CompletableFuture future; + + public CompletableFutureStream(CompletableFuture future) { + this.future = future; + } + + @Override + public void subscribe(final Subscriber subscriber) { + try { + subscriber.onSubscribe(new ReactiveSubscription(this, subscriber) { + + @Override + public void request(long elements) { + Action.checkRequest(elements); + if (isComplete()) return; + + try { + future.whenComplete((result, error) -> { + if (error != null) { + onError(error); + } + else { + subscriber.onNext(result); + onComplete(); + } + }); + + } catch (Throwable e) { + onError(e); + } + } + }); + } catch (Throwable throwable) { + Exceptions.throwIfFatal(throwable); + subscriber.onError(throwable); + } + } + + } + +} \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java index ce8d83983e..da6d1e1a93 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java @@ -21,18 +21,22 @@ import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.concurrent.CompletableFuture; import org.reactivestreams.Publisher; +import reactor.rx.Promise; import reactor.rx.Stream; import reactor.rx.Streams; import rx.Observable; import rx.RxReactiveStreams; +import rx.Single; import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.reactive.codec.decoder.ByteToMessageDecoder; +import org.springframework.reactive.util.CompletableFutureUtils; import org.springframework.reactive.web.dispatch.method.HandlerMethodArgumentResolver; import org.springframework.reactive.web.http.ServerHttpRequest; import org.springframework.web.bind.annotation.RequestBody; @@ -69,9 +73,14 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve ResolvableType type = ResolvableType.forMethodParameter(parameter); List hints = new ArrayList<>(); hints.add(UTF_8); + // TODO: Refactor type conversion ResolvableType readType = type; - if (Observable.class.isAssignableFrom(type.getRawClass()) || Publisher.class.isAssignableFrom(type.getRawClass())) { + if (Observable.class.isAssignableFrom(type.getRawClass()) || + Single.class.isAssignableFrom(type.getRawClass()) || + Promise.class.isAssignableFrom(type.getRawClass()) || + Publisher.class.isAssignableFrom(type.getRawClass()) || + CompletableFuture.class.isAssignableFrom(type.getRawClass())) { readType = type.getGeneric(0); } @@ -89,9 +98,18 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve if (Stream.class.isAssignableFrom(type.getRawClass())) { return Streams.wrap(elementStream); } + else if (Promise.class.isAssignableFrom(type.getRawClass())) { + return Streams.wrap(elementStream).take(1).next(); + } else if (Observable.class.isAssignableFrom(type.getRawClass())) { return RxReactiveStreams.toObservable(elementStream); } + else if (Single.class.isAssignableFrom(type.getRawClass())) { + return RxReactiveStreams.toObservable(elementStream).toSingle(); + } + else if (CompletableFuture.class.isAssignableFrom(type.getRawClass())) { + return CompletableFutureUtils.fromSinglePublisher(elementStream); + } else if (Publisher.class.isAssignableFrom(type.getRawClass())) { return elementStream; } @@ -99,11 +117,11 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve try { return Streams.wrap(elementStream).next().await(); } catch(InterruptedException ex) { - throw new IllegalStateException("Timeout before getter the value"); + return Streams.fail(new IllegalStateException("Timeout before getter the value")); } } } - throw new IllegalStateException("Argument type not supported: " + type); + return Streams.fail(new IllegalStateException("Argument type not supported: " + type)); } private MediaType resolveMediaType(ServerHttpRequest request) { diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java index 387fa657e6..8109c3f24f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java @@ -21,11 +21,15 @@ import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.concurrent.CompletableFuture; import org.reactivestreams.Publisher; +import reactor.rx.Promise; +import reactor.rx.Stream; import reactor.rx.Streams; import rx.Observable; import rx.RxReactiveStreams; +import rx.Single; import org.springframework.core.MethodParameter; import org.springframework.core.Ordered; @@ -34,6 +38,7 @@ import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.reactive.codec.encoder.MessageToByteEncoder; +import org.springframework.reactive.util.CompletableFutureUtils; import org.springframework.reactive.web.dispatch.HandlerResult; import org.springframework.reactive.web.dispatch.HandlerResultHandler; import org.springframework.reactive.web.http.ServerHttpRequest; @@ -108,9 +113,18 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered Publisher elementStream; // TODO: Refactor type conversion - if (Observable.class.isAssignableFrom(type.getRawClass())) { + if (Promise.class.isAssignableFrom(type.getRawClass())) { + elementStream = ((Promise)value).stream(); + } + else if (Observable.class.isAssignableFrom(type.getRawClass())) { elementStream = RxReactiveStreams.toPublisher((Observable) value); } + else if (Single.class.isAssignableFrom(type.getRawClass())) { + elementStream = RxReactiveStreams.toPublisher(((Single)value).toObservable()); + } + else if (CompletableFuture.class.isAssignableFrom(type.getRawClass())) { + elementStream = CompletableFutureUtils.toPublisher((CompletableFuture) value); + } else if (Publisher.class.isAssignableFrom(type.getRawClass())) { elementStream = (Publisher)value; } diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/util/CompletableFutureUtilsTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/util/CompletableFutureUtilsTests.java new file mode 100644 index 0000000000..2ea5a305e4 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/util/CompletableFutureUtilsTests.java @@ -0,0 +1,101 @@ +/* + * Copyright 2002-2015 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.reactive.util; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import org.junit.Test; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import reactor.rx.Streams; + +/** + * @author Sebastien Deleuze + */ +public class CompletableFutureUtilsTests { + + private CountDownLatch lock = new CountDownLatch(1); + private final List results = new ArrayList<>(); + private final List errors = new ArrayList<>(); + + @Test + public void fromPublisher() throws InterruptedException { + Publisher publisher = Streams.just(true, false); + CompletableFuture> future = CompletableFutureUtils.fromPublisher(publisher); + future.whenComplete((result, error) -> { + if (error != null) { + errors.add(error); + } + else { + results.addAll(result); + } + lock.countDown(); + }); + lock.await(2000, TimeUnit.MILLISECONDS); + assertEquals("onError not expected: " + errors.toString(), 0, errors.size()); + assertEquals(2, results.size()); + assertTrue(results.get(0)); + assertFalse(results.get(1)); + } + + @Test + public void fromSinglePublisher() throws InterruptedException { + Publisher publisher = Streams.just(true); + CompletableFuture future = CompletableFutureUtils.fromSinglePublisher(publisher); + future.whenComplete((result, error) -> { + if (error != null) { + errors.add(error); + } + else { + results.add(result); + } + lock.countDown(); + }); + lock.await(2000, TimeUnit.MILLISECONDS); + assertEquals("onError not expected: " + errors.toString(), 0, errors.size()); + assertEquals(1, results.size()); + assertTrue(results.get(0)); + } + + @Test + public void fromSinglePublisherWithMultipleValues() throws InterruptedException { + Publisher publisher = Streams.just(true, false); + CompletableFuture future = CompletableFutureUtils.fromSinglePublisher(publisher); + future.whenComplete((result, error) -> { + if (error != null) { + errors.add(error); + } + else { + results.add(result); + } + lock.countDown(); + }); + lock.await(2000, TimeUnit.MILLISECONDS); + assertEquals(1, errors.size()); + assertEquals(IllegalStateException.class, errors.get(0).getClass()); + assertEquals(0, results.size()); + } + +} \ No newline at end of file diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingIntegrationTests.java index 98c8fcd39a..8bf6234fbf 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingIntegrationTests.java @@ -19,13 +19,17 @@ package org.springframework.reactive.web.dispatch.method.annotation; import java.net.URI; import java.util.Arrays; import java.util.List; +import java.util.concurrent.CompletableFuture; import static org.junit.Assert.assertEquals; import org.junit.Test; import org.reactivestreams.Publisher; +import reactor.rx.Promise; +import reactor.rx.Promises; import reactor.rx.Stream; import reactor.rx.Streams; import rx.Observable; +import rx.Single; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.MediaType; @@ -81,22 +85,89 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @Test public void serializeAsPojo() throws Exception { + serializeAsPojo("http://localhost:" + port + "/person"); + } + @Test + public void serializeAsCompletableFuture() throws Exception { + serializeAsPojo("http://localhost:" + port + "/completable-future"); + } + + @Test + public void serializeAsSingle() throws Exception { + serializeAsPojo("http://localhost:" + port + "/single"); + } + + @Test + public void serializeAsPromise() throws Exception { + serializeAsPojo("http://localhost:" + port + "/promise"); + } + + @Test + public void serializeAsList() throws Exception { + serializeAsCollection("http://localhost:" + port + "/list"); + } + + @Test + public void serializeAsPublisher() throws Exception { + serializeAsCollection("http://localhost:" + port + "/publisher"); + } + + @Test + public void serializeAsObservable() throws Exception { + serializeAsCollection("http://localhost:" + port + "/observable"); + } + + @Test + public void serializeAsReactorStream() throws Exception { + serializeAsCollection("http://localhost:" + port + "/stream"); + } + + @Test + public void publisherCapitalize() throws Exception { + capitalizeCollection("http://localhost:" + port + "/publisher-capitalize"); + } + + @Test + public void observableCapitalize() throws Exception { + capitalizeCollection("http://localhost:" + port + "/observable-capitalize"); + } + + @Test + public void streamCapitalize() throws Exception { + capitalizeCollection("http://localhost:" + port + "/stream-capitalize"); + } + + @Test + public void completableFutureCapitalize() throws Exception { + capitalizePojo("http://localhost:" + port + "/completable-future-capitalize"); + } + + @Test + public void singleCapitalize() throws Exception { + capitalizePojo("http://localhost:" + port + "/single-capitalize"); + } + + @Test + public void promiseCapitalize() throws Exception { + capitalizePojo("http://localhost:" + port + "/promise-capitalize"); + } + + + public void serializeAsPojo(String requestUrl) throws Exception { RestTemplate restTemplate = new RestTemplate(); - URI url = new URI("http://localhost:" + port + "/person"); + URI url = new URI(requestUrl); RequestEntity request = RequestEntity.get(url).accept(MediaType.APPLICATION_JSON).build(); ResponseEntity response = restTemplate.exchange(request, Person.class); assertEquals(new Person("Robert"), response.getBody()); } - @Test - public void serializeAsList() throws Exception { - + public void serializeAsCollection(String requestUrl) throws Exception { RestTemplate restTemplate = new RestTemplate(); - URI url = new URI("http://localhost:" + port + "/list"); + URI url = new URI(requestUrl); RequestEntity request = RequestEntity.get(url).accept(MediaType.APPLICATION_JSON).build(); List results = restTemplate.exchange(request, new ParameterizedTypeReference>(){}).getBody(); @@ -105,67 +176,23 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati assertEquals(new Person("Marie"), results.get(1)); } - @Test - public void serializeAsPublisher() throws Exception { + public void capitalizePojo(String requestUrl) throws Exception { RestTemplate restTemplate = new RestTemplate(); - URI url = new URI("http://localhost:" + port + "/publisher"); - RequestEntity request = RequestEntity.get(url).accept(MediaType.APPLICATION_JSON).build(); - List results = restTemplate.exchange(request, new ParameterizedTypeReference>(){}).getBody(); + URI url = new URI(requestUrl); + RequestEntity request = RequestEntity + .post(url) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .body(new Person("Robert")); + ResponseEntity response = restTemplate.exchange(request, Person.class); - assertEquals(2, results.size()); - assertEquals(new Person("Robert"), results.get(0)); - assertEquals(new Person("Marie"), results.get(1)); - } - - @Test - public void serializeAsObservable() throws Exception { - - RestTemplate restTemplate = new RestTemplate(); - - URI url = new URI("http://localhost:" + port + "/observable"); - RequestEntity request = RequestEntity.get(url).accept(MediaType.APPLICATION_JSON).build(); - List results = restTemplate.exchange(request, new ParameterizedTypeReference>() { - }).getBody(); - - assertEquals(2, results.size()); - assertEquals(new Person("Robert"), results.get(0)); - assertEquals(new Person("Marie"), results.get(1)); - } - - @Test - public void serializeAsReactorStream() throws Exception { - - RestTemplate restTemplate = new RestTemplate(); - - URI url = new URI("http://localhost:" + port + "/stream"); - RequestEntity request = RequestEntity.get(url).accept(MediaType.APPLICATION_JSON).build(); - List results = restTemplate.exchange(request, new ParameterizedTypeReference>() { - }).getBody(); - - assertEquals(2, results.size()); - assertEquals(new Person("Robert"), results.get(0)); - assertEquals(new Person("Marie"), results.get(1)); - } - - @Test - public void publisherCapitalize() throws Exception { - capitalize("http://localhost:" + port + "/publisher-capitalize"); - } - - @Test - public void observableCapitalize() throws Exception { - capitalize("http://localhost:" + port + "/observable-capitalize"); - } - - @Test - public void streamCapitalize() throws Exception { - capitalize("http://localhost:" + port + "/stream-capitalize"); + assertEquals(new Person("ROBERT"), response.getBody()); } - public void capitalize(String requestUrl) throws Exception { + public void capitalizeCollection(String requestUrl) throws Exception { RestTemplate restTemplate = new RestTemplate(); URI url = new URI(requestUrl); @@ -199,6 +226,24 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati return new Person("Robert"); } + @RequestMapping("/completable-future") + @ResponseBody + public CompletableFuture completableFutureResponseBody() { + return CompletableFuture.completedFuture(new Person("Robert")); + } + + @RequestMapping("/single") + @ResponseBody + public Single singleResponseBody() { + return Single.just(new Person("Robert")); + } + + @RequestMapping("/promise") + @ResponseBody + public Promise promiseResponseBody() { + return Promises.success(new Person("Robert")); + } + @RequestMapping("/list") @ResponseBody public List listResponseBody() { @@ -250,6 +295,33 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati }); } + @RequestMapping("/completable-future-capitalize") + @ResponseBody + public CompletableFuture completableFutureCapitalize(@RequestBody CompletableFuture personFuture) { + return personFuture.thenApply(person -> { + person.setName(person.getName().toUpperCase()); + return person; + }); + } + + @RequestMapping("/single-capitalize") + @ResponseBody + public Single singleCapitalize(@RequestBody Single personFuture) { + return personFuture.map(person -> { + person.setName(person.getName().toUpperCase()); + return person; + }); + } + + @RequestMapping("/promise-capitalize") + @ResponseBody + public Promise promiseCapitalize(@RequestBody Promise personFuture) { + return personFuture.map(person -> { + person.setName(person.getName().toUpperCase()); + return person; + }); + } + } private static class Person { From 20c401109473a22e6d1bfe638374e885a372d0f8 Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Tue, 29 Sep 2015 14:07:36 +0100 Subject: [PATCH 045/498] initialize reactor support, not passing requestmapping tests yet --- spring-web-reactive/build.gradle | 2 + .../reactor/ReactorServerHttpRequest.java | 79 +++++++++++++++++++ .../reactor/ReactorServerHttpResponse.java | 74 +++++++++++++++++ .../http/reactor/RequestHandlerAdapter.java | 44 +++++++++++ .../AbstractHttpHandlerIntegrationTests.java | 1 + .../reactive/web/http/ReactorHttpServer.java | 75 ++++++++++++++++++ 6 files changed, 275 insertions(+) create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpRequest.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpResponse.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/RequestHandlerAdapter.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/reactive/web/http/ReactorHttpServer.java diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index 5619827d0b..5ab432e69e 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -34,6 +34,8 @@ dependencies { optional "io.reactivex:rxnetty:0.5.0-SNAPSHOT" optional "io.reactivex:rxjava-reactive-streams:1.0.1" + optional "io.projectreactor:reactor-net:2.0.5.RELEASE" + provided "javax.servlet:javax.servlet-api:3.1.0" testCompile "junit:junit:4.12" diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpRequest.java new file mode 100644 index 0000000000..6a9d0ed44d --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpRequest.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2011-2015 Pivotal Software Inc, All Rights Reserved. + * + * 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.reactive.web.http.reactor; + +import org.reactivestreams.Publisher; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.reactive.web.http.ServerHttpRequest; +import org.springframework.util.Assert; +import reactor.io.buffer.Buffer; +import reactor.io.net.http.HttpChannel; + +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.ByteBuffer; + +/** + * @author Stephane Maldini + */ +public class ReactorServerHttpRequest implements ServerHttpRequest { + + private final HttpChannel channel; + + private HttpHeaders headers; + + + public ReactorServerHttpRequest(HttpChannel request) { + Assert.notNull("'request', request must not be null."); + this.channel = request; + } + + + @Override + public HttpHeaders getHeaders() { + if (this.headers == null) { + this.headers = new HttpHeaders(); + for (String name : this.channel.headers().names()) { + for (String value : this.channel.headers().getAll(name)) { + this.headers.add(name, value); + } + } + } + return this.headers; + } + + @Override + public HttpMethod getMethod() { + return HttpMethod.valueOf(this.channel.method().getName()); + } + + @Override + public URI getURI() { + try { + return new URI(this.channel.uri()); + } catch (URISyntaxException ex) { + throw new IllegalStateException("Could not get URI: " + ex.getMessage(), ex); + } + + } + + @Override + public Publisher getBody() { + return this.channel.map(Buffer::byteBuffer); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpResponse.java new file mode 100644 index 0000000000..647994c6bb --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpResponse.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2011-2015 Pivotal Software Inc, All Rights Reserved. + * + * 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.reactive.web.http.reactor; + +import org.reactivestreams.Publisher; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.reactive.web.http.ServerHttpResponse; +import org.springframework.util.Assert; +import reactor.io.buffer.Buffer; +import reactor.io.net.http.HttpChannel; +import reactor.io.net.http.model.Status; +import reactor.rx.Streams; + +import java.nio.ByteBuffer; + +/** + * @author Stephane Maldini + */ +public class ReactorServerHttpResponse implements ServerHttpResponse { + + private final HttpChannel channel; + + private final HttpHeaders headers; + + private boolean headersWritten = false; + + + public ReactorServerHttpResponse(HttpChannel response) { + Assert.notNull("'response', response must not be null."); + this.channel = response; + this.headers = new HttpHeaders(); + } + + + @Override + public void setStatusCode(HttpStatus status) { + this.channel.responseStatus(Status.valueOf(status.value())); + } + + @Override + public HttpHeaders getHeaders() { + return (this.headersWritten ? HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers); + } + + @Override + public Publisher writeWith(Publisher contentPublisher) { + writeHeaders(); + return this.channel.writeWith(Streams.wrap(contentPublisher).map(Buffer::new)); + } + + private void writeHeaders() { + if (!this.headersWritten) { + for (String name : this.headers.keySet()) { + for (String value : this.headers.get(name)) { + this.channel.responseHeaders().add(name, value); + } + } + } + } +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/RequestHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/RequestHandlerAdapter.java new file mode 100644 index 0000000000..c8173299c6 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/RequestHandlerAdapter.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2011-2015 Pivotal Software Inc, All Rights Reserved. + * + * 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.reactive.web.http.reactor; + +import org.reactivestreams.Publisher; +import org.springframework.reactive.web.http.HttpHandler; +import org.springframework.util.Assert; +import reactor.io.buffer.Buffer; +import reactor.io.net.ReactorChannelHandler; +import reactor.io.net.http.HttpChannel; + +/** + * @author Stephane Maldini + */ +public class RequestHandlerAdapter implements ReactorChannelHandler> { + + private final HttpHandler httpHandler; + + + public RequestHandlerAdapter(HttpHandler httpHandler) { + Assert.notNull(httpHandler, "'httpHandler' is required."); + this.httpHandler = httpHandler; + } + + @Override + public Publisher apply(HttpChannel channel) { + ReactorServerHttpRequest adaptedRequest = new ReactorServerHttpRequest(channel); + ReactorServerHttpResponse adaptedResponse = new ReactorServerHttpResponse(channel); + return this.httpHandler.handle(adaptedRequest, adaptedResponse); + } +} diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/AbstractHttpHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/AbstractHttpHandlerIntegrationTests.java index 240c0ff82b..9f0111a6ad 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/AbstractHttpHandlerIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/AbstractHttpHandlerIntegrationTests.java @@ -38,6 +38,7 @@ public abstract class AbstractHttpHandlerIntegrationTests { return new Object[][] { {new JettyHttpServer()}, {new TomcatHttpServer()}, + {new ReactorHttpServer()}, {new RxNettyHttpServer()} }; } diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/ReactorHttpServer.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/ReactorHttpServer.java new file mode 100644 index 0000000000..5ed3e75216 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/ReactorHttpServer.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2011-2015 Pivotal Software Inc, All Rights Reserved. + * + * 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.reactive.web.http; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.reactive.web.http.reactor.RequestHandlerAdapter; +import org.springframework.util.Assert; +import reactor.Environment; +import reactor.bus.selector.Selectors; +import reactor.io.buffer.Buffer; +import reactor.io.net.NetStreams; + + +/** + * @author Stephane Maldini + */ +public class ReactorHttpServer extends HttpServerSupport implements InitializingBean, HttpServer { + + private RequestHandlerAdapter reactorHandler; + + private reactor.io.net.http.HttpServer reactorServer; + + private boolean running; + + + @Override + public boolean isRunning() { + return this.running; + } + + + @Override + public void afterPropertiesSet() throws Exception { + + Assert.notNull(getHttpHandler()); + this.reactorHandler = new RequestHandlerAdapter(getHttpHandler()); + + Environment.initializeIfEmpty(); + + this.reactorServer = (getPort() != -1 ? + NetStreams.httpServer(getPort()) : + NetStreams.httpServer()); + } + + + @Override + public void start() { + if (!this.running) { + this.running = true; + this.reactorServer.route(Selectors.matchAll(), this.reactorHandler).start(); + } + } + + @Override + public void stop() { + if (this.running) { + this.running = false; + this.reactorServer.shutdown(); + } + } + +} From 4ffec61310c2e1a47c524c09eadf8ff1530aa91d Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Tue, 29 Sep 2015 23:16:06 +0100 Subject: [PATCH 046/498] Update to 2.0.6 snapshot for Header sent too soon fix, which allows DispatcherHandler to assign content-type correctly. --- spring-web-reactive/build.gradle | 6 ++++-- .../web/http/AbstractHttpHandlerIntegrationTests.java | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index 5ab432e69e..665a7799a6 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -20,21 +20,23 @@ group = 'org.springframework.reactive' repositories { mavenCentral() + mavenLocal() maven { url 'https://oss.jfrog.org/libs-snapshot' } // RxNetty 0.5.x snapshots + maven { url 'http://repo.spring.io/snapshot' } // Reactor snapshot } dependencies { compile "org.springframework:spring-core:4.2.0.RELEASE" compile "org.springframework:spring-web:4.2.0.RELEASE" compile "org.reactivestreams:reactive-streams:1.0.0" - compile "io.projectreactor:reactor-stream:2.0.5.RELEASE" + compile "io.projectreactor:reactor-stream:2.0.6.BUILD-SNAPSHOT" compile "commons-logging:commons-logging:1.2" compile "com.fasterxml.jackson.core:jackson-databind:2.6.1" optional "io.reactivex:rxnetty:0.5.0-SNAPSHOT" optional "io.reactivex:rxjava-reactive-streams:1.0.1" - optional "io.projectreactor:reactor-net:2.0.5.RELEASE" + optional "io.projectreactor:reactor-net:2.0.6.BUILD-SNAPSHOT" provided "javax.servlet:javax.servlet-api:3.1.0" diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/AbstractHttpHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/AbstractHttpHandlerIntegrationTests.java index 9f0111a6ad..4a01fa334c 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/AbstractHttpHandlerIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/AbstractHttpHandlerIntegrationTests.java @@ -38,8 +38,8 @@ public abstract class AbstractHttpHandlerIntegrationTests { return new Object[][] { {new JettyHttpServer()}, {new TomcatHttpServer()}, - {new ReactorHttpServer()}, - {new RxNettyHttpServer()} + {new RxNettyHttpServer()}, + {new ReactorHttpServer()} }; } From d20fbffcd006a069741f43e927c80c4184224b5f Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Tue, 29 Sep 2015 23:31:02 +0100 Subject: [PATCH 047/498] Update to Reactor 2.1.0.BUILD-SNAPSHOT --- spring-web-reactive/build.gradle | 4 ++-- .../reactive/util/CompletableFutureUtils.java | 5 ++--- .../springframework/reactive/web/http/ReactorHttpServer.java | 3 --- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index 665a7799a6..4d003ee73a 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -29,14 +29,14 @@ dependencies { compile "org.springframework:spring-core:4.2.0.RELEASE" compile "org.springframework:spring-web:4.2.0.RELEASE" compile "org.reactivestreams:reactive-streams:1.0.0" - compile "io.projectreactor:reactor-stream:2.0.6.BUILD-SNAPSHOT" + compile "io.projectreactor:reactor-stream:2.1.0.BUILD-SNAPSHOT" compile "commons-logging:commons-logging:1.2" compile "com.fasterxml.jackson.core:jackson-databind:2.6.1" optional "io.reactivex:rxnetty:0.5.0-SNAPSHOT" optional "io.reactivex:rxjava-reactive-streams:1.0.1" - optional "io.projectreactor:reactor-net:2.0.6.BUILD-SNAPSHOT" + optional "io.projectreactor:reactor-net:2.1.0.BUILD-SNAPSHOT" provided "javax.servlet:javax.servlet-api:3.1.0" diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/util/CompletableFutureUtils.java b/spring-web-reactive/src/main/java/org/springframework/reactive/util/CompletableFutureUtils.java index e26afa24e5..3e92ddf717 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/util/CompletableFutureUtils.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/util/CompletableFutureUtils.java @@ -23,7 +23,7 @@ import java.util.concurrent.CompletableFuture; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; -import reactor.core.support.Exceptions; +import reactor.core.error.Exceptions; import reactor.rx.Stream; import reactor.rx.action.Action; import reactor.rx.subscription.ReactiveSubscription; @@ -131,8 +131,7 @@ public class CompletableFutureUtils { } }); } catch (Throwable throwable) { - Exceptions.throwIfFatal(throwable); - subscriber.onError(throwable); + Exceptions.publisher(throwable); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/ReactorHttpServer.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/ReactorHttpServer.java index 5ed3e75216..028e650137 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/ReactorHttpServer.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/ReactorHttpServer.java @@ -18,7 +18,6 @@ package org.springframework.reactive.web.http; import org.springframework.beans.factory.InitializingBean; import org.springframework.reactive.web.http.reactor.RequestHandlerAdapter; import org.springframework.util.Assert; -import reactor.Environment; import reactor.bus.selector.Selectors; import reactor.io.buffer.Buffer; import reactor.io.net.NetStreams; @@ -48,8 +47,6 @@ public class ReactorHttpServer extends HttpServerSupport implements Initializing Assert.notNull(getHttpHandler()); this.reactorHandler = new RequestHandlerAdapter(getHttpHandler()); - Environment.initializeIfEmpty(); - this.reactorServer = (getPort() != -1 ? NetStreams.httpServer(getPort()) : NetStreams.httpServer()); From 49fc32e2144903704f44a6cc9d7f78e80dfc2e1e Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Thu, 1 Oct 2015 17:07:13 +0200 Subject: [PATCH 048/498] Check new snapshots for every build --- spring-web-reactive/build.gradle | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index 4d003ee73a..5e25378a19 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -25,6 +25,11 @@ repositories { maven { url 'http://repo.spring.io/snapshot' } // Reactor snapshot } +configurations.all { + // check for updates every build + resolutionStrategy.cacheChangingModulesFor 0, 'seconds' +} + dependencies { compile "org.springframework:spring-core:4.2.0.RELEASE" compile "org.springframework:spring-web:4.2.0.RELEASE" From 9516c9992f4ed53bbb0be801bed31b6280eaff8c Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Fri, 2 Oct 2015 15:42:43 +0200 Subject: [PATCH 049/498] Return HandlerResult in HandlerAdapter#handle() This commit updates HandlerAdapter#handle() to return HandlerResult instead of Publisher. A new SimpleHandlerResultHandler class has been introduced for handlers returning Publisher. --- .../web/dispatch/DispatcherHandler.java | 10 ++- .../reactive/web/dispatch/HandlerAdapter.java | 4 +- .../dispatch/SimpleHandlerResultHandler.java | 49 +++++++++++++ .../dispatch/handler/HttpHandlerAdapter.java | 10 +-- .../RequestMappingHandlerAdapter.java | 17 ++--- .../annotation/ResponseBodyResultHandler.java | 11 +-- ...mpleUrlHandlerMappingIntegrationTests.java | 2 + .../ResponseBodyResultHandlerTests.java | 68 +++++++++++++++++++ 8 files changed, 143 insertions(+), 28 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/SimpleHandlerResultHandler.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandlerTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/DispatcherHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/DispatcherHandler.java index 3393e3d463..de38f689aa 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/DispatcherHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/DispatcherHandler.java @@ -91,9 +91,9 @@ public class DispatcherHandler implements HttpHandler, ApplicationContextAware { } HandlerAdapter handlerAdapter = getHandlerAdapter(handler); - Publisher resultPublisher = handlerAdapter.handle(request, response, handler); - return Streams.wrap(resultPublisher).concatMap((HandlerResult result) -> { + try { + HandlerResult result = handlerAdapter.handle(request, response, handler); for (HandlerResultHandler resultHandler : resultHandlers) { if (resultHandler.supports(result)) { return resultHandler.handleResult(request, response, result); @@ -101,7 +101,11 @@ public class DispatcherHandler implements HttpHandler, ApplicationContextAware { } return Streams.fail(new IllegalStateException( "No HandlerResultHandler for " + result.getValue())); - }); + } + catch(Exception ex) { + return Streams.fail(ex); + } + } protected Object getHandler(ServerHttpRequest request) { diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerAdapter.java index 49f778540a..94e8355e47 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerAdapter.java @@ -15,8 +15,6 @@ */ package org.springframework.reactive.web.dispatch; -import org.reactivestreams.Publisher; - import org.springframework.reactive.web.http.ServerHttpRequest; import org.springframework.reactive.web.http.ServerHttpResponse; @@ -27,6 +25,6 @@ public interface HandlerAdapter { boolean supports(Object handler); - Publisher handle(ServerHttpRequest request, ServerHttpResponse response, Object handler); + HandlerResult handle(ServerHttpRequest request, ServerHttpResponse response, Object handler) throws Exception; } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/SimpleHandlerResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/SimpleHandlerResultHandler.java new file mode 100644 index 0000000000..c26000c394 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/SimpleHandlerResultHandler.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2015 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.reactive.web.dispatch; + +import org.reactivestreams.Publisher; + +import org.springframework.core.Ordered; +import org.springframework.reactive.web.http.ServerHttpRequest; +import org.springframework.reactive.web.http.ServerHttpResponse; + +/** + * Supports {@link HandlerResult} with a {@code Publisher} value. + * + * @author Sebastien Deleuze + */ +public class SimpleHandlerResultHandler implements Ordered, HandlerResultHandler { + + private int order = Ordered.LOWEST_PRECEDENCE; + + @Override + public int getOrder() { + return this.order; + } + + @Override + public boolean supports(HandlerResult result) { + Object value = result.getValue(); + return value != null && Publisher.class.isAssignableFrom(value.getClass()); + } + + @Override + public Publisher handleResult(ServerHttpRequest request, ServerHttpResponse response, HandlerResult result) { + return (Publisher)result.getValue(); + } +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/handler/HttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/handler/HttpHandlerAdapter.java index e6a05cbe1c..0dc2e9463d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/handler/HttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/handler/HttpHandlerAdapter.java @@ -16,7 +16,6 @@ package org.springframework.reactive.web.dispatch.handler; import org.reactivestreams.Publisher; -import reactor.rx.Streams; import org.springframework.reactive.web.dispatch.HandlerAdapter; import org.springframework.reactive.web.dispatch.HandlerResult; @@ -34,6 +33,7 @@ import org.springframework.reactive.web.http.ServerHttpResponse; * handler mappings. * * @author Rossen Stoyanchev + * @author Sebastien Deleuze */ public class HttpHandlerAdapter implements HandlerAdapter { @@ -44,10 +44,10 @@ public class HttpHandlerAdapter implements HandlerAdapter { } @Override - public Publisher handle(ServerHttpRequest request, ServerHttpResponse response, Object handler) { - HttpHandler httpHandler = (HttpHandler) handler; - Publisher publisher = httpHandler.handle(request, response); - return Streams.wrap(publisher).map(aVoid -> null); + public HandlerResult handle(ServerHttpRequest request, ServerHttpResponse response, Object handler) { + HttpHandler httpHandler = (HttpHandler)handler; + Publisher completion = httpHandler.handle(request, response); + return new HandlerResult(httpHandler, completion); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java index 5fcab45fd2..e2d1747200 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java @@ -19,9 +19,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import org.reactivestreams.Publisher; -import reactor.rx.Streams; - import org.springframework.beans.factory.InitializingBean; import org.springframework.reactive.codec.decoder.JacksonJsonDecoder; import org.springframework.reactive.codec.decoder.JsonObjectDecoder; @@ -64,21 +61,15 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Initializin } @Override - public Publisher handle(ServerHttpRequest request, ServerHttpResponse response, - Object handler) { + public HandlerResult handle(ServerHttpRequest request, ServerHttpResponse response, + Object handler) throws Exception { final InvocableHandlerMethod invocable = new InvocableHandlerMethod((HandlerMethod) handler); invocable.setHandlerMethodArgumentResolvers(this.argumentResolvers); - Object result; - try { - result = invocable.invokeForRequest(request); - } - catch (Exception ex) { - return Streams.fail(ex); - } + Object result = invocable.invokeForRequest(request); - return Streams.just(new HandlerResult(invocable, result)); + return new HandlerResult(invocable, result); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java index 8109c3f24f..00ccc67ace 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java @@ -16,6 +16,7 @@ package org.springframework.reactive.web.dispatch.method.annotation; import java.lang.reflect.Method; +import java.lang.reflect.Type; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.util.ArrayList; @@ -25,7 +26,6 @@ import java.util.concurrent.CompletableFuture; import org.reactivestreams.Publisher; import reactor.rx.Promise; -import reactor.rx.Stream; import reactor.rx.Streams; import rx.Observable; import rx.RxReactiveStreams; @@ -33,6 +33,7 @@ import rx.Single; import org.springframework.core.MethodParameter; import org.springframework.core.Ordered; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.http.HttpHeaders; @@ -60,7 +61,7 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered private final List> serializers; private final List> postProcessors; - private int order = Ordered.LOWEST_PRECEDENCE; + private int order = 0; public ResponseBodyResultHandler(List> serializers) { @@ -86,8 +87,10 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered public boolean supports(HandlerResult result) { Object handler = result.getHandler(); if (handler instanceof HandlerMethod) { - Method method = ((HandlerMethod) handler).getMethod(); - return AnnotatedElementUtils.isAnnotated(method, ResponseBody.class.getName()); + HandlerMethod handlerMethod = (HandlerMethod) handler; + Type publisherVoidType = new ParameterizedTypeReference>(){}.getType(); + return AnnotatedElementUtils.isAnnotated(handlerMethod.getMethod(), ResponseBody.class.getName()) && + !handlerMethod.getReturnType().getGenericParameterType().equals(publisherVoidType); } return false; } diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/handler/SimpleUrlHandlerMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/handler/SimpleUrlHandlerMappingIntegrationTests.java index b9700a02f1..de986c811f 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/handler/SimpleUrlHandlerMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/handler/SimpleUrlHandlerMappingIntegrationTests.java @@ -28,6 +28,7 @@ import reactor.rx.Streams; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; import org.springframework.reactive.web.dispatch.DispatcherHandler; +import org.springframework.reactive.web.dispatch.SimpleHandlerResultHandler; import org.springframework.reactive.web.http.AbstractHttpHandlerIntegrationTests; import org.springframework.reactive.web.http.HttpHandler; import org.springframework.reactive.web.http.ServerHttpRequest; @@ -52,6 +53,7 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler StaticWebApplicationContext wac = new StaticWebApplicationContext(); wac.registerSingleton("hm", TestHandlerMapping.class); wac.registerSingleton("ha", HttpHandlerAdapter.class); + wac.registerSingleton("hhrh", SimpleHandlerResultHandler.class); wac.refresh(); DispatcherHandler dispatcherHandler = new DispatcherHandler(); diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandlerTests.java new file mode 100644 index 0000000000..b4500ceed6 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandlerTests.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2015 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.reactive.web.dispatch.method.annotation; + +import java.util.Collections; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import org.junit.Test; +import org.reactivestreams.Publisher; + +import org.springframework.reactive.web.dispatch.HandlerResult; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.method.HandlerMethod; + +/** + * @author Sebastien Deleuze + */ +public class ResponseBodyResultHandlerTests { + + @Test + public void supports() throws NoSuchMethodException { + ResponseBodyResultHandler resultHandler = new ResponseBodyResultHandler(Collections.emptyList()); + TestController controller = new TestController(); + + HandlerMethod notAnnotatedMethod = new HandlerMethod(controller, TestController.class.getMethod("notAnnotated")); + assertFalse(resultHandler.supports(new HandlerResult(notAnnotatedMethod, null))); + + HandlerMethod publisherStringMethod = new HandlerMethod(controller, TestController.class.getMethod("publisherString")); + assertTrue(resultHandler.supports(new HandlerResult(publisherStringMethod, null))); + + HandlerMethod publisherVoidMethod = new HandlerMethod(controller, TestController.class.getMethod("publisherVoid")); + assertFalse(resultHandler.supports(new HandlerResult(publisherVoidMethod, null))); + } + + + private static class TestController { + + public Publisher notAnnotated() { + return null; + } + + @ResponseBody + public Publisher publisherString() { + return null; + } + + @ResponseBody + public Publisher publisherVoid() { + return null; + } + } + +} From 32214e0a4960a55f82ab203ff4bc98b73974a862 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Mon, 5 Oct 2015 14:18:15 +0200 Subject: [PATCH 050/498] Add Javadoc for main interfaces/classes --- .../web/dispatch/DispatcherHandler.java | 15 +++++++++++ .../reactive/web/dispatch/HandlerAdapter.java | 27 +++++++++++++++++++ .../reactive/web/dispatch/HandlerResult.java | 2 ++ .../web/dispatch/HandlerResultHandler.java | 20 ++++++++++++++ .../reactive/web/http/HttpHandler.java | 19 +++++++++++++ .../reactive/web/http/ServerHttpRequest.java | 4 +++ .../reactive/web/http/ServerHttpResponse.java | 10 +++++++ 7 files changed, 97 insertions(+) diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/DispatcherHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/DispatcherHandler.java index de38f689aa..a7bc0904ef 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/DispatcherHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/DispatcherHandler.java @@ -35,7 +35,22 @@ import org.springframework.reactive.web.http.ServerHttpRequest; import org.springframework.reactive.web.http.ServerHttpResponse; /** + * Central dispatcher for HTTP request handlers/controllers. Dispatches to registered + * handlers for processing a web request, providing convenient mapping facilities. + * + *
  • It can use any {@link HandlerMapping} implementation to control the routing of + * requests to handler objects. HandlerMapping objects can be defined as beans in + * the application context. + * + *
  • It can use any {@link HandlerAdapter}; this allows for using any handler interface. + * HandlerAdapter objects can be added as beans in the application context. + * + *
  • It can use any {@link HandlerResultHandler}; this allows to process the result of + * the request handling. HandlerResultHandler objects can be added as beans in the + * application context. + * * @author Rossen Stoyanchev + * @author Sebastien Deleuze */ public class DispatcherHandler implements HttpHandler, ApplicationContextAware { diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerAdapter.java index 94e8355e47..01fc9a9f0e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerAdapter.java @@ -19,12 +19,39 @@ import org.springframework.reactive.web.http.ServerHttpRequest; import org.springframework.reactive.web.http.ServerHttpResponse; /** + * Interface that must be implemented for each handler type to handle an HTTP request. + * This interface is used to allow the {@link DispatcherHandler} to be indefinitely + * extensible. The {@code DispatcherHandler} accesses all installed handlers through + * this interface, meaning that it does not contain code specific to any handler type. + * * @author Rossen Stoyanchev + * @author Sebastien Deleuze */ public interface HandlerAdapter { + /** + * Given a handler instance, return whether or not this {@code HandlerAdapter} + * can support it. Typical HandlerAdapters will base the decision on the handler + * type. HandlerAdapters will usually only support one handler type each. + *

    A typical implementation: + *

    {@code + * return (handler instanceof MyHandler); + * } + * @param handler handler object to check + * @return whether or not this object can use the given handler + */ boolean supports(Object handler); + /** + * Use the given handler to handle this request. + * @param request current HTTP request + * @param response current HTTP response + * @param handler handler to use. This object must have previously been passed + * to the {@code supports} method of this interface, which must have + * returned {@code true}. + * @throws Exception in case of errors + * @return An {@link HandlerResult} instance + */ HandlerResult handle(ServerHttpRequest request, ServerHttpResponse response, Object handler) throws Exception; } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerResult.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerResult.java index 4a6ec2dc3f..2b3c912071 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerResult.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerResult.java @@ -17,6 +17,8 @@ package org.springframework.reactive.web.dispatch; /** + * Represent the result of the invocation of an handler. + * * @author Rossen Stoyanchev */ public class HandlerResult { diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerResultHandler.java index 46257e3ec4..7a4a9cb23b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerResultHandler.java @@ -21,12 +21,32 @@ import org.springframework.reactive.web.http.ServerHttpRequest; import org.springframework.reactive.web.http.ServerHttpResponse; /** + * Process the {@link HandlerResult}, usually returned by an {@link HandlerAdapter}. + * * @author Rossen Stoyanchev + * @author Sebastien Deleuze */ public interface HandlerResultHandler { + /** + * Given a handler instance, return whether or not this {@code HandlerResultHandler} + * can support it. + * + * @param result result object to check + * @return whether or not this object can use the given result + */ boolean supports(HandlerResult result); + /** + * Process the given result in an asynchronous non blocking way, by eventually modifying + * response headers, or writing some data stream into the response. + * Implementations should not throw exceptions but signal them via the returned + * {@code Publisher}. + * + * @return A {@code Publisher} used to signal the demand, and receive a notification + * when the handling is complete (success or error) including the flush of the data on the + * network. + */ Publisher handleResult(ServerHttpRequest request, ServerHttpResponse response, HandlerResult result); } \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/HttpHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/HttpHandler.java index 77897de4e7..4158526da8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/HttpHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/HttpHandler.java @@ -20,11 +20,30 @@ import org.reactivestreams.Publisher; /** + * Interface for handlers that process HTTP requests and generate an HTTP response. + * This handler is designed to be called when the HTTP headers have been received, making + * the HTTP request body available as stream. The HTTP response body can also be written + * as a stream. + * * @author Arjen Poutsma * @author Rossen Stoyanchev + * @author Sebastien Deleuze + * @see ServerHttpRequest#getBody() + * @see ServerHttpResponse#writeWith(Publisher) */ public interface HttpHandler { + /** + * Process the given request, generating a response in an asynchronous non blocking way. + * Implementations should not throw exceptions but signal them via the returned + * {@code Publisher}. + * + * @param request current HTTP request, the body can be processed as a data stream. + * @param response current HTTP response, the body can be provided as a data stream. + * @return A {@code Publisher} used to signal the demand, and receive a notification + * when the handling is complete (success or error) including the flush of the data on the + * network. + */ Publisher handle(ServerHttpRequest request, ServerHttpResponse response); } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/ServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/ServerHttpRequest.java index bfbf9771ef..dca48dee1c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/ServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/ServerHttpRequest.java @@ -20,11 +20,15 @@ import java.nio.ByteBuffer; import org.reactivestreams.Publisher; /** + * Represent a server-side HTTP request. * * @author Rossen Stoyanchev */ public interface ServerHttpRequest extends HttpRequest { + /** + * Return the body of the message as a reactive stream. + */ Publisher getBody(); } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/ServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/ServerHttpResponse.java index 3063aa2b2f..ba7cc6f5db 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/ServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/ServerHttpResponse.java @@ -22,12 +22,22 @@ import org.reactivestreams.Publisher; import org.springframework.http.HttpStatus; /** + * Represent a server-side HTTP response. + * * @author Rossen Stoyanchev */ public interface ServerHttpResponse extends HttpMessage { void setStatusCode(HttpStatus status); + /** + * Write the provided reactive stream of bytes to the response body. Most servers + * support multiple {@code writeWith} calls. + * @param contentPublisher the stream to write in the response body. + * @return A {@code Publisher} used to signal the demand, and receive a notification + * when the handling is complete (success or error) including the flush of the data on the + * network. + */ Publisher writeWith(Publisher contentPublisher); } From ec1189b0b5931f9e09d9790936517bf38c3e834c Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Mon, 5 Oct 2015 16:48:54 +0100 Subject: [PATCH 051/498] minor fix and update to work on latest 2.1.0.BUILD-SNAPSHOT to use core check in CFUtils --- .../reactive/util/CompletableFutureUtils.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/util/CompletableFutureUtils.java b/spring-web-reactive/src/main/java/org/springframework/reactive/util/CompletableFutureUtils.java index 3e92ddf717..a2bac8abcf 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/util/CompletableFutureUtils.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/util/CompletableFutureUtils.java @@ -24,6 +24,8 @@ import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import reactor.core.error.Exceptions; +import reactor.core.error.SpecificationExceptions; +import reactor.core.support.BackpressureUtils; import reactor.rx.Stream; import reactor.rx.action.Action; import reactor.rx.subscription.ReactiveSubscription; @@ -111,15 +113,19 @@ public class CompletableFutureUtils { @Override public void request(long elements) { - Action.checkRequest(elements); + try{ + BackpressureUtils.checkRequest(elements); + }catch(SpecificationExceptions.Spec309_NullOrNegativeRequest iae){ + subscriber.onError(iae); + return; + } if (isComplete()) return; try { future.whenComplete((result, error) -> { if (error != null) { onError(error); - } - else { + } else { subscriber.onNext(result); onComplete(); } From 906dead596e7331b8a45a451464f902380169fac Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Wed, 7 Oct 2015 12:13:35 +0200 Subject: [PATCH 052/498] Polishing --- .../reactive/util/CompletableFutureUtils.java | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/util/CompletableFutureUtils.java b/spring-web-reactive/src/main/java/org/springframework/reactive/util/CompletableFutureUtils.java index a2bac8abcf..cb3aa16e8d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/util/CompletableFutureUtils.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/util/CompletableFutureUtils.java @@ -27,7 +27,6 @@ import reactor.core.error.Exceptions; import reactor.core.error.SpecificationExceptions; import reactor.core.support.BackpressureUtils; import reactor.rx.Stream; -import reactor.rx.action.Action; import reactor.rx.subscription.ReactiveSubscription; import org.springframework.util.Assert; @@ -115,12 +114,14 @@ public class CompletableFutureUtils { public void request(long elements) { try{ BackpressureUtils.checkRequest(elements); - }catch(SpecificationExceptions.Spec309_NullOrNegativeRequest iae){ + } + catch (SpecificationExceptions.Spec309_NullOrNegativeRequest iae) { subscriber.onError(iae); return; } - if (isComplete()) return; - + if (isComplete()) { + return; + } try { future.whenComplete((result, error) -> { if (error != null) { @@ -130,17 +131,17 @@ public class CompletableFutureUtils { onComplete(); } }); - - } catch (Throwable e) { + } + catch (Throwable e) { onError(e); } } }); - } catch (Throwable throwable) { + } + catch (Throwable throwable) { Exceptions.publisher(throwable); } } - } } \ No newline at end of file From 0dabdb8207920e875a9020c1bcd8a9220c3c1d88 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Wed, 7 Oct 2015 12:15:39 +0200 Subject: [PATCH 053/498] Add request method based mapping Closes #22 --- .../RequestMappingHandlerMapping.java | 82 +++++++++- .../RequestMappingHandlerMappingTests.java | 143 ++++++++++++++++++ 2 files changed, 218 insertions(+), 7 deletions(-) create mode 100644 spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerMappingTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerMapping.java index 76e175742c..17df6ff819 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerMapping.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerMapping.java @@ -15,8 +15,14 @@ */ package org.springframework.reactive.web.dispatch.method.annotation; -import java.util.LinkedHashMap; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -25,10 +31,12 @@ import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.http.HttpMethod; import org.springframework.reactive.web.dispatch.HandlerMapping; import org.springframework.reactive.web.http.ServerHttpRequest; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.method.HandlerMethod; import org.springframework.web.method.HandlerMethodSelector; @@ -42,7 +50,7 @@ public class RequestMappingHandlerMapping implements HandlerMapping, private static final Log logger = LogFactory.getLog(RequestMappingHandlerMapping.class); - private final Map methodMap = new LinkedHashMap<>(); + private final Map methodMap = new TreeMap<>(); private ApplicationContext applicationContext; @@ -67,11 +75,16 @@ public class RequestMappingHandlerMapping implements HandlerMapping, RequestMapping annotation = AnnotationUtils.findAnnotation(method, RequestMapping.class); if (annotation != null && annotation.value().length > 0) { String path = annotation.value()[0]; + RequestMethod[] methods = annotation.method(); HandlerMethod handlerMethod = new HandlerMethod(bean, method); if (logger.isInfoEnabled()) { logger.info("Mapped \"" + path + "\" onto " + handlerMethod); } - methodMap.put(path, handlerMethod); + RequestMappingInfo info = new RequestMappingInfo(path, methods); + if (this.methodMap.containsKey(info)) { + throw new IllegalStateException("Duplicate mapping found for " + info); + } + methodMap.put(info, handlerMethod); } return false; }); @@ -81,11 +94,66 @@ public class RequestMappingHandlerMapping implements HandlerMapping, @Override public Object getHandler(ServerHttpRequest request) { String path = request.getURI().getPath(); - HandlerMethod handlerMethod = this.methodMap.get(path); - if (logger.isDebugEnabled()) { - logger.debug("Mapped " + path + " to [" + handlerMethod + "]"); + HttpMethod method = request.getMethod(); + for (Map.Entry entry : this.methodMap.entrySet()) { + RequestMappingInfo info = entry.getKey(); + if (path.equals(info.getPath()) && (info.getMethods().isEmpty() || info.getMethods().contains(RequestMethod.valueOf(method.name())))) { + if (logger.isDebugEnabled()) { + logger.debug("Mapped " + method + " " + path + " to [" + entry.getValue() + "]"); + } + return entry.getValue(); + } + } + return null; + } + + + private static class RequestMappingInfo implements Comparable { + + private String path; + + private Set methods; + + + public RequestMappingInfo(String path, RequestMethod... methods) { + this(path, asList(methods)); + } + + public RequestMappingInfo(String path, Collection methods) { + this.path = path; + this.methods = new TreeSet<>(methods); + } + + + public String getPath() { + return path; + } + + public Set getMethods() { + return methods; + } + + private static List asList(RequestMethod... requestMethods) { + return (requestMethods != null ? Arrays.asList(requestMethods) : Collections.emptyList()); + } + + @Override + public int compareTo(Object o) { + RequestMappingInfo other = (RequestMappingInfo)o; + if (!this.path.equals(other.getPath())) { + return -1; + } + if (this.methods.isEmpty() && !other.methods.isEmpty()) { + return 1; + } + if (!this.methods.isEmpty() && other.methods.isEmpty()) { + return -1; + } + if (this.methods.equals(other.methods)) { + return 0; + } + return -1; } - return handlerMethod; } } diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerMappingTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerMappingTests.java new file mode 100644 index 0000000000..9bb4a02aed --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerMappingTests.java @@ -0,0 +1,143 @@ +/* + * Copyright 2002-2015 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.reactive.web.dispatch.method.annotation; + +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.ByteBuffer; + +import static org.junit.Assert.assertEquals; +import org.junit.Before; +import org.junit.Test; +import org.reactivestreams.Publisher; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.reactive.web.http.ServerHttpRequest; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.context.support.StaticWebApplicationContext; +import org.springframework.web.method.HandlerMethod; + +/** + * @author Sebastien Deleuze + */ +public class RequestMappingHandlerMappingTests { + + private RequestMappingHandlerMapping mapping; + + @Before + public void setup() { + StaticWebApplicationContext wac = new StaticWebApplicationContext(); + wac.registerSingleton("handlerMapping", RequestMappingHandlerMapping.class); + wac.registerSingleton("controller", TestController.class); + wac.refresh(); + this.mapping = (RequestMappingHandlerMapping)wac.getBean("handlerMapping"); + } + + @Test + public void path() throws NoSuchMethodException { + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, "boo"); + HandlerMethod handler = (HandlerMethod) this.mapping.getHandler(request); + assertEquals(TestController.class.getMethod("boo"), handler.getMethod()); + } + + @Test + public void method() throws NoSuchMethodException { + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.POST, "foo"); + HandlerMethod handler = (HandlerMethod) this.mapping.getHandler(request); + assertEquals(TestController.class.getMethod("postFoo"), handler.getMethod()); + + request = new MockServerHttpRequest(HttpMethod.GET, "foo"); + handler = (HandlerMethod) this.mapping.getHandler(request); + assertEquals(TestController.class.getMethod("getFoo"), handler.getMethod()); + + request = new MockServerHttpRequest(HttpMethod.PUT, "foo"); + handler = (HandlerMethod) this.mapping.getHandler(request); + assertEquals(TestController.class.getMethod("foo"), handler.getMethod()); + } + + + @Controller + @SuppressWarnings("unused") + private static class TestController { + + @RequestMapping("foo") + public String foo() { + return "foo"; + } + + @RequestMapping(path = "foo", method = RequestMethod.POST) + public String postFoo() { + return "postFoo"; + } + + @RequestMapping(path = "foo", method = RequestMethod.GET) + public String getFoo() { + return "getFoo"; + } + + @RequestMapping("bar") + public String bar() { + return "bar"; + } + + @RequestMapping("boo") + public String boo() { + return "boo"; + } + + } + + private static class MockServerHttpRequest implements ServerHttpRequest{ + + private HttpMethod method; + + private URI uri; + + public MockServerHttpRequest(HttpMethod method, String path) { + this.method = method; + try { + this.uri = new URI(path); + } catch (URISyntaxException ex) { + throw new IllegalStateException("Could not get URI: " + ex.getMessage(), ex); + } + } + + @Override + public Publisher getBody() { + return null; + } + + @Override + public HttpMethod getMethod() { + return this.method; + } + + @Override + public URI getURI() { + return this.uri; + } + + @Override + public HttpHeaders getHeaders() { + return null; + } + } + +} From 464ff1d9606a3189a41bae80e504929dec091d33 Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Mon, 5 Oct 2015 15:35:59 +0100 Subject: [PATCH 054/498] Progress #24 Replace Streams fail, empty and Stream map with Publishers.error, empty and map. Replace Streams calls by Publishers.flatMap, just and Publishers.from Precise cancel and size to toReadQueue to simulate Promise Update build --- spring-web-reactive/build.gradle | 5 ++- .../codec/decoder/JacksonJsonDecoder.java | 25 +++++------ .../reactive/codec/decoder/Jaxb2Decoder.java | 43 ++++++++----------- .../codec/decoder/JsonObjectDecoder.java | 37 ++++++++-------- .../reactive/codec/decoder/StringDecoder.java | 15 +++---- .../codec/encoder/JacksonJsonEncoder.java | 18 +++----- .../reactive/codec/encoder/Jaxb2Encoder.java | 36 +++++++--------- .../reactive/codec/encoder/StringEncoder.java | 13 +++--- .../web/dispatch/DispatcherHandler.java | 19 ++++---- .../RequestBodyArgumentResolver.java | 8 ++-- .../annotation/ResponseBodyResultHandler.java | 38 ++++++++-------- .../reactor/ReactorServerHttpResponse.java | 4 +- 12 files changed, 121 insertions(+), 140 deletions(-) diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index 5e25378a19..d227418046 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -34,9 +34,10 @@ dependencies { compile "org.springframework:spring-core:4.2.0.RELEASE" compile "org.springframework:spring-web:4.2.0.RELEASE" compile "org.reactivestreams:reactive-streams:1.0.0" - compile "io.projectreactor:reactor-stream:2.1.0.BUILD-SNAPSHOT" + compile "io.projectreactor:reactor-core:2.1.0.BUILD-SNAPSHOT" compile "commons-logging:commons-logging:1.2" - compile "com.fasterxml.jackson.core:jackson-databind:2.6.1" + + optional "com.fasterxml.jackson.core:jackson-databind:2.6.1" optional "io.reactivex:rxnetty:0.5.0-SNAPSHOT" optional "io.reactivex:rxjava-reactive-streams:1.0.1" diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoder.java index 150bf33a6e..0562653bdc 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoder.java @@ -16,19 +16,18 @@ package org.springframework.reactive.codec.decoder; -import java.io.IOException; -import java.nio.ByteBuffer; - import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectReader; import org.reactivestreams.Publisher; -import reactor.rx.Streams; - import org.springframework.core.ResolvableType; import org.springframework.http.MediaType; import org.springframework.reactive.codec.CodecException; import org.springframework.reactive.codec.encoder.JacksonJsonEncoder; import org.springframework.reactive.io.ByteBufferInputStream; +import reactor.Publishers; + +import java.io.IOException; +import java.nio.ByteBuffer; /** * Decode from a bytes stream of JSON objects to a stream of {@code Object} (POJO). @@ -56,15 +55,13 @@ public class JacksonJsonDecoder implements ByteToMessageDecoder { @Override public Publisher decode(Publisher inputStream, ResolvableType type, MediaType mediaType, Object... hints) { ObjectReader reader = mapper.readerFor(type.getRawClass()); - return Streams.wrap(inputStream) - .map(chunk -> { - try { - return reader.readValue(new ByteBufferInputStream(chunk)); - } - catch (IOException e) { - throw new CodecException("Error while reading the data", e); - } - }); + return Publishers.map(inputStream, chunk -> { + try { + return reader.readValue(new ByteBufferInputStream(chunk)); + } catch (IOException e) { + throw new CodecException("Error while reading the data", e); + } + }); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/Jaxb2Decoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/Jaxb2Decoder.java index 77ae96a315..bff1ef0334 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/Jaxb2Decoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/Jaxb2Decoder.java @@ -16,33 +16,27 @@ package org.springframework.reactive.codec.decoder; -import java.nio.ByteBuffer; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; - -import javax.xml.bind.JAXBContext; -import javax.xml.bind.JAXBElement; -import javax.xml.bind.JAXBException; -import javax.xml.bind.UnmarshalException; -import javax.xml.bind.Unmarshaller; -import javax.xml.bind.annotation.XmlRootElement; -import javax.xml.transform.Source; -import javax.xml.transform.sax.SAXSource; -import javax.xml.transform.stream.StreamSource; - import org.reactivestreams.Publisher; -import org.xml.sax.InputSource; -import org.xml.sax.SAXException; -import org.xml.sax.XMLReader; -import org.xml.sax.helpers.XMLReaderFactory; -import reactor.rx.Streams; - import org.springframework.core.ResolvableType; import org.springframework.http.MediaType; import org.springframework.reactive.codec.CodecException; import org.springframework.reactive.codec.encoder.Jaxb2Encoder; import org.springframework.reactive.io.ByteBufferPublisherInputStream; import org.springframework.util.Assert; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.XMLReader; +import org.xml.sax.helpers.XMLReaderFactory; +import reactor.Publishers; + +import javax.xml.bind.*; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.transform.Source; +import javax.xml.transform.sax.SAXSource; +import javax.xml.transform.stream.StreamSource; +import java.nio.ByteBuffer; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; /** * Decode from a bytes stream of XML elements to a stream of {@code Object} (POJO). @@ -66,18 +60,19 @@ public class Jaxb2Decoder implements ByteToMessageDecoder { Source source = processSource(new StreamSource(new ByteBufferPublisherInputStream(inputStream))); Unmarshaller unmarshaller = createUnmarshaller(outputClass); if (outputClass.isAnnotationPresent(XmlRootElement.class)) { - return Streams.just(unmarshaller.unmarshal(source)); + return Publishers.just(unmarshaller.unmarshal(source)); } else { JAXBElement jaxbElement = unmarshaller.unmarshal(source, outputClass); - return Streams.just(jaxbElement.getValue()); + return Publishers.just(jaxbElement.getValue()); } } catch (UnmarshalException ex) { - return Streams.fail(new CodecException("Could not unmarshal to [" + outputClass + "]: " + ex.getMessage(), ex)); + return Publishers.error( + new CodecException("Could not unmarshal to [" + outputClass + "]: " + ex.getMessage(), ex)); } catch (JAXBException ex) { - return Streams.fail(new CodecException("Could not instantiate JAXBContext: " + ex.getMessage(), ex)); + return Publishers.error(new CodecException("Could not instantiate JAXBContext: " + ex.getMessage(), ex)); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JsonObjectDecoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JsonObjectDecoder.java index 4aa1edca46..c1f9c35926 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JsonObjectDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JsonObjectDecoder.java @@ -16,22 +16,21 @@ package org.springframework.reactive.codec.decoder; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; - import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; import org.reactivestreams.Publisher; -import reactor.fn.Function; -import reactor.rx.Promise; -import reactor.rx.Streams; -import rx.Observable; - import org.springframework.core.ResolvableType; import org.springframework.http.MediaType; import org.springframework.reactive.codec.encoder.JsonObjectEncoder; +import reactor.Publishers; +import reactor.fn.Function; +import reactor.rx.Promise; +import rx.Observable; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; /** * Decode an arbitrary split byte stream representing JSON objects to a bye stream @@ -98,7 +97,7 @@ public class JsonObjectDecoder implements ByteToMessageDecoder { @Override public Publisher decode(Publisher inputStream, ResolvableType type, MediaType mediaType, Object... hints) { - return Streams.wrap(inputStream).flatMap(new Function>() { + return Publishers.flatMap(inputStream, new Function>() { int openBraces; int idx; @@ -121,17 +120,17 @@ public class JsonObjectDecoder implements ByteToMessageDecoder { } if (state == ST_CORRUPTED) { in.skipBytes(in.readableBytes()); - return Streams.fail(new IllegalStateException("Corrupted stream")); + return Publishers.error(new IllegalStateException("Corrupted stream")); } if (wrtIdx > maxObjectLength) { // buffer size exceeded maxObjectLength; discarding the complete buffer. in.skipBytes(in.readableBytes()); reset(); - return Streams.fail(new IllegalStateException( - "object length exceeds " + maxObjectLength + ": " + - wrtIdx + - " bytes discarded")); + return Publishers.error(new IllegalStateException( + "object length exceeds " + maxObjectLength + ": " + + wrtIdx + + " bytes discarded")); } for (/* use current idx */; idx < wrtIdx; idx++) { @@ -202,16 +201,16 @@ public class JsonObjectDecoder implements ByteToMessageDecoder { } else { state = ST_CORRUPTED; - return Streams.fail(new IllegalStateException( - "invalid JSON received at byte position " + idx + - ": " + ByteBufUtil.hexDump(in))); + return Publishers.error(new IllegalStateException( + "invalid JSON received at byte position " + idx + + ": " + ByteBufUtil.hexDump(in))); } } if (in.readableBytes() == 0) { idx = 0; } - return Streams.from(chunks); + return Publishers.from(chunks); } /** diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/StringDecoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/StringDecoder.java index 3943bf9359..b64bd81edb 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/StringDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/StringDecoder.java @@ -16,18 +16,17 @@ package org.springframework.reactive.codec.decoder; -import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; - import org.reactivestreams.Publisher; -import reactor.io.buffer.Buffer; -import reactor.rx.Streams; - import org.springframework.core.ResolvableType; import org.springframework.http.MediaType; import org.springframework.reactive.codec.encoder.StringEncoder; import org.springframework.reactive.codec.support.HintUtils; +import reactor.Publishers; +import reactor.io.buffer.Buffer; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; /** * Decode from a bytes stream to a String stream. @@ -48,7 +47,7 @@ public class StringDecoder implements ByteToMessageDecoder { @Override public Publisher decode(Publisher inputStream, ResolvableType type, MediaType mediaType, Object... hints) { Charset charset = HintUtils.getHintByClass(Charset.class, hints, DEFAULT_CHARSET); - return Streams.wrap(inputStream).map(chunk -> new String(new Buffer(chunk).asBytes(), charset)); + return Publishers.map(inputStream, chunk -> new String(new Buffer(chunk).asBytes(), charset)); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoder.java index 3fb9200140..efdb652be0 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoder.java @@ -16,20 +16,18 @@ package org.springframework.reactive.codec.encoder; -import java.io.IOException; -import java.nio.ByteBuffer; - import com.fasterxml.jackson.databind.ObjectMapper; import org.reactivestreams.Publisher; -import reactor.io.buffer.Buffer; -import reactor.rx.Stream; -import reactor.rx.Streams; - import org.springframework.core.ResolvableType; import org.springframework.http.MediaType; import org.springframework.reactive.codec.CodecException; import org.springframework.reactive.codec.decoder.JacksonJsonDecoder; import org.springframework.reactive.io.BufferOutputStream; +import reactor.Publishers; +import reactor.io.buffer.Buffer; + +import java.io.IOException; +import java.nio.ByteBuffer; /** * Encode from an {@code Object} stream to a byte stream of JSON objects. @@ -56,19 +54,17 @@ public class JacksonJsonEncoder implements MessageToByteEncoder { @Override public Publisher encode(Publisher messageStream, ResolvableType type, MediaType mediaType, Object... hints) { - Stream stream = Streams.wrap(messageStream).map(value -> { + return Publishers.map(messageStream, value -> { Buffer buffer = new Buffer(); BufferOutputStream outputStream = new BufferOutputStream(buffer); try { this.mapper.writeValue(outputStream, value); - } - catch (IOException e) { + } catch (IOException e) { throw new CodecException("Error while writing the data", e); } buffer.flip(); return buffer.byteBuffer(); }); - return stream; } } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/Jaxb2Encoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/Jaxb2Encoder.java index a1fffb1b9e..3a2887e21f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/Jaxb2Encoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/Jaxb2Encoder.java @@ -16,20 +16,7 @@ package org.springframework.reactive.codec.encoder; -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; - -import javax.xml.bind.JAXBContext; -import javax.xml.bind.JAXBException; -import javax.xml.bind.MarshalException; -import javax.xml.bind.Marshaller; - import org.reactivestreams.Publisher; -import reactor.io.buffer.Buffer; -import reactor.rx.Streams; - import org.springframework.core.ResolvableType; import org.springframework.http.MediaType; import org.springframework.reactive.codec.CodecException; @@ -37,6 +24,17 @@ import org.springframework.reactive.codec.decoder.Jaxb2Decoder; import org.springframework.reactive.io.BufferOutputStream; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import reactor.Publishers; +import reactor.io.buffer.Buffer; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.MarshalException; +import javax.xml.bind.Marshaller; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; /** * Encode from an {@code Object} stream to a byte stream of XML elements. @@ -55,7 +53,7 @@ public class Jaxb2Encoder implements MessageToByteEncoder { @Override public Publisher encode(Publisher messageStream, ResolvableType type, MediaType mediaType, Object... hints) { - return Streams.wrap(messageStream).map(value -> { + return Publishers.map(messageStream, value -> { try { Buffer buffer = new Buffer(); BufferOutputStream outputStream = new BufferOutputStream(buffer); @@ -65,14 +63,12 @@ public class Jaxb2Encoder implements MessageToByteEncoder { marshaller.marshal(value, outputStream); buffer.flip(); return buffer.byteBuffer(); - } - catch (MarshalException ex) { + } catch (MarshalException ex) { throw new CodecException( - "Could not marshal [" + value + "]: " + ex.getMessage(), ex); - } - catch (JAXBException ex) { + "Could not marshal [" + value + "]: " + ex.getMessage(), ex); + } catch (JAXBException ex) { throw new CodecException( - "Could not instantiate JAXBContext: " + ex.getMessage(), ex); + "Could not instantiate JAXBContext: " + ex.getMessage(), ex); } }); } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/StringEncoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/StringEncoder.java index a27adf6e36..487007f7d3 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/StringEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/StringEncoder.java @@ -16,17 +16,16 @@ package org.springframework.reactive.codec.encoder; -import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; - import org.reactivestreams.Publisher; -import reactor.rx.Streams; - import org.springframework.core.ResolvableType; import org.springframework.http.MediaType; import org.springframework.reactive.codec.decoder.StringDecoder; import org.springframework.reactive.codec.support.HintUtils; +import reactor.Publishers; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; /** * Encode from a String stream to a bytes stream. @@ -47,7 +46,7 @@ public class StringEncoder implements MessageToByteEncoder { @Override public Publisher encode(Publisher elementStream, ResolvableType type, MediaType mediaType, Object... hints) { final Charset charset = HintUtils.getHintByClass(Charset.class, hints, DEFAULT_CHARSET); - return Streams.wrap(elementStream).map(s -> ByteBuffer.wrap(s.getBytes(charset))); + return Publishers.map(elementStream, s -> ByteBuffer.wrap(s.getBytes(charset))); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/DispatcherHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/DispatcherHandler.java index a7bc0904ef..8d1fe02ab9 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/DispatcherHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/DispatcherHandler.java @@ -15,15 +15,9 @@ */ package org.springframework.reactive.web.dispatch; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.reactivestreams.Publisher; -import reactor.rx.Streams; - import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.context.ApplicationContext; @@ -33,6 +27,11 @@ import org.springframework.http.HttpStatus; import org.springframework.reactive.web.http.HttpHandler; import org.springframework.reactive.web.http.ServerHttpRequest; import org.springframework.reactive.web.http.ServerHttpResponse; +import reactor.Publishers; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; /** * Central dispatcher for HTTP request handlers/controllers. Dispatches to registered @@ -102,7 +101,7 @@ public class DispatcherHandler implements HttpHandler, ApplicationContextAware { if (handler == null) { // No exception handling mechanism yet response.setStatusCode(HttpStatus.NOT_FOUND); - return Streams.empty(); + return Publishers.empty(); } HandlerAdapter handlerAdapter = getHandlerAdapter(handler); @@ -114,11 +113,11 @@ public class DispatcherHandler implements HttpHandler, ApplicationContextAware { return resultHandler.handleResult(request, response, result); } } - return Streams.fail(new IllegalStateException( - "No HandlerResultHandler for " + result.getValue())); + return Publishers.error(new IllegalStateException( + "No HandlerResultHandler for " + result.getValue())); } catch(Exception ex) { - return Streams.fail(ex); + return Publishers.error(ex); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java index da6d1e1a93..f0607240ac 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java @@ -22,8 +22,10 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; import org.reactivestreams.Publisher; +import reactor.Publishers; import reactor.rx.Promise; import reactor.rx.Stream; import reactor.rx.Streams; @@ -115,13 +117,13 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve } else { try { - return Streams.wrap(elementStream).next().await(); + return Publishers.toReadQueue(elementStream, 1, true).poll(30, TimeUnit.SECONDS); } catch(InterruptedException ex) { - return Streams.fail(new IllegalStateException("Timeout before getter the value")); + return Publishers.error(new IllegalStateException("Timeout before getter the value")); } } } - return Streams.fail(new IllegalStateException("Argument type not supported: " + type)); + return Publishers.error(new IllegalStateException("Argument type not supported: " + type)); } private MediaType resolveMediaType(ServerHttpRequest request) { diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java index 00ccc67ace..9e4431d116 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java @@ -15,22 +15,7 @@ */ package org.springframework.reactive.web.dispatch.method.annotation; -import java.lang.reflect.Method; -import java.lang.reflect.Type; -import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.CompletableFuture; - import org.reactivestreams.Publisher; -import reactor.rx.Promise; -import reactor.rx.Streams; -import rx.Observable; -import rx.RxReactiveStreams; -import rx.Single; - import org.springframework.core.MethodParameter; import org.springframework.core.Ordered; import org.springframework.core.ParameterizedTypeReference; @@ -46,6 +31,19 @@ import org.springframework.reactive.web.http.ServerHttpRequest; import org.springframework.reactive.web.http.ServerHttpResponse; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.method.HandlerMethod; +import reactor.Publishers; +import reactor.rx.Promise; +import rx.Observable; +import rx.RxReactiveStreams; +import rx.Single; + +import java.lang.reflect.Type; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; /** @@ -104,7 +102,7 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered MethodParameter returnType = handlerMethod.getReturnValueType(value); if (value == null) { - return Streams.empty(); + return Publishers.empty(); } MediaType mediaType = resolveMediaType(request); @@ -132,7 +130,7 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered elementStream = (Publisher)value; } else { - elementStream = Streams.just(value); + elementStream = Publishers.just(value); } Publisher outputStream = serializer.encode(elementStream, type, mediaType, hints.toArray()); @@ -141,10 +139,10 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered outputStream = postProcessor.encode(outputStream, type, mediaType, hints.toArray()); } response.getHeaders().setContentType(mediaType); - return response.writeWith(Streams.wrap(outputStream)); + return response.writeWith(outputStream); } - return Streams.fail(new IllegalStateException( - "Return value type not supported: " + returnType)); + return Publishers.error(new IllegalStateException( + "Return value type not supported: " + returnType)); } private MediaType resolveMediaType(ServerHttpRequest request) { diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpResponse.java index 647994c6bb..66761cabd5 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpResponse.java @@ -20,10 +20,10 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.reactive.web.http.ServerHttpResponse; import org.springframework.util.Assert; +import reactor.Publishers; import reactor.io.buffer.Buffer; import reactor.io.net.http.HttpChannel; import reactor.io.net.http.model.Status; -import reactor.rx.Streams; import java.nio.ByteBuffer; @@ -59,7 +59,7 @@ public class ReactorServerHttpResponse implements ServerHttpResponse { @Override public Publisher writeWith(Publisher contentPublisher) { writeHeaders(); - return this.channel.writeWith(Streams.wrap(contentPublisher).map(Buffer::new)); + return this.channel.writeWith(Publishers.map(contentPublisher, Buffer::new)); } private void writeHeaders() { From 07374f48d69dcb246921debf6123546903c692b3 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Wed, 7 Oct 2015 14:06:26 +0200 Subject: [PATCH 055/498] Move server related classes from src/test to src/main --- spring-web-reactive/build.gradle | 12 ++++++------ .../reactive/web/http/HttpServer.java | 0 .../reactive/web/http/HttpServerSupport.java | 0 .../web/http/reactor}/ReactorHttpServer.java | 4 +++- .../web/http/rxnetty}/RxNettyHttpServer.java | 4 +++- .../reactive/web/http/servlet}/JettyHttpServer.java | 5 +++-- .../reactive/web/http/servlet}/TomcatHttpServer.java | 5 +++-- .../http/AbstractHttpHandlerIntegrationTests.java | 4 ++++ 8 files changed, 22 insertions(+), 12 deletions(-) rename spring-web-reactive/src/{test => main}/java/org/springframework/reactive/web/http/HttpServer.java (100%) rename spring-web-reactive/src/{test => main}/java/org/springframework/reactive/web/http/HttpServerSupport.java (100%) rename spring-web-reactive/src/{test/java/org/springframework/reactive/web/http => main/java/org/springframework/reactive/web/http/reactor}/ReactorHttpServer.java (91%) rename spring-web-reactive/src/{test/java/org/springframework/reactive/web/http => main/java/org/springframework/reactive/web/http/rxnetty}/RxNettyHttpServer.java (91%) rename spring-web-reactive/src/{test/java/org/springframework/reactive/web/http => main/java/org/springframework/reactive/web/http/servlet}/JettyHttpServer.java (92%) rename spring-web-reactive/src/{test/java/org/springframework/reactive/web/http => main/java/org/springframework/reactive/web/http/servlet}/TomcatHttpServer.java (92%) diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index d227418046..8d0d6709f5 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -44,17 +44,17 @@ dependencies { optional "io.projectreactor:reactor-net:2.1.0.BUILD-SNAPSHOT" + optional 'org.apache.tomcat:tomcat-util:8.0.24' + optional 'org.apache.tomcat.embed:tomcat-embed-core:8.0.24' + + optional 'org.eclipse.jetty:jetty-server:9.3.2.v20150730' + optional 'org.eclipse.jetty:jetty-servlet:9.3.2.v20150730' + provided "javax.servlet:javax.servlet-api:3.1.0" testCompile "junit:junit:4.12" testCompile "org.springframework:spring-test:4.2.0.RELEASE" - testCompile 'org.apache.tomcat:tomcat-util:8.0.24' - testCompile 'org.apache.tomcat.embed:tomcat-embed-core:8.0.24' - - testCompile 'org.eclipse.jetty:jetty-server:9.3.2.v20150730' - testCompile 'org.eclipse.jetty:jetty-servlet:9.3.2.v20150730' - testCompile "org.slf4j:slf4j-jcl:1.7.12" testCompile "org.slf4j:jul-to-slf4j:1.7.12" testCompile("log4j:log4j:1.2.16") diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/HttpServer.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/HttpServer.java similarity index 100% rename from spring-web-reactive/src/test/java/org/springframework/reactive/web/http/HttpServer.java rename to spring-web-reactive/src/main/java/org/springframework/reactive/web/http/HttpServer.java diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/HttpServerSupport.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/HttpServerSupport.java similarity index 100% rename from spring-web-reactive/src/test/java/org/springframework/reactive/web/http/HttpServerSupport.java rename to spring-web-reactive/src/main/java/org/springframework/reactive/web/http/HttpServerSupport.java diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/ReactorHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorHttpServer.java similarity index 91% rename from spring-web-reactive/src/test/java/org/springframework/reactive/web/http/ReactorHttpServer.java rename to spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorHttpServer.java index 028e650137..063b725e9f 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/ReactorHttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorHttpServer.java @@ -13,9 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.reactive.web.http; +package org.springframework.reactive.web.http.reactor; import org.springframework.beans.factory.InitializingBean; +import org.springframework.reactive.web.http.HttpServer; +import org.springframework.reactive.web.http.HttpServerSupport; import org.springframework.reactive.web.http.reactor.RequestHandlerAdapter; import org.springframework.util.Assert; import reactor.bus.selector.Selectors; diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/RxNettyHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyHttpServer.java similarity index 91% rename from spring-web-reactive/src/test/java/org/springframework/reactive/web/http/RxNettyHttpServer.java rename to spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyHttpServer.java index d3eb174c03..d07cc78c84 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/RxNettyHttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyHttpServer.java @@ -13,11 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.reactive.web.http; +package org.springframework.reactive.web.http.rxnetty; import io.netty.buffer.ByteBuf; import org.springframework.beans.factory.InitializingBean; +import org.springframework.reactive.web.http.HttpServer; +import org.springframework.reactive.web.http.HttpServerSupport; import org.springframework.reactive.web.http.rxnetty.RequestHandlerAdapter; import org.springframework.util.Assert; diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/JettyHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/JettyHttpServer.java similarity index 92% rename from spring-web-reactive/src/test/java/org/springframework/reactive/web/http/JettyHttpServer.java rename to spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/JettyHttpServer.java index ce2bc19e8b..ddfd5d01ab 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/JettyHttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/JettyHttpServer.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.reactive.web.http; +package org.springframework.reactive.web.http.servlet; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; @@ -21,7 +21,8 @@ import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder; import org.springframework.beans.factory.InitializingBean; -import org.springframework.reactive.web.http.servlet.HttpHandlerServlet; +import org.springframework.reactive.web.http.HttpServer; +import org.springframework.reactive.web.http.HttpServerSupport; import org.springframework.util.Assert; import org.springframework.util.SocketUtils; diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/TomcatHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/TomcatHttpServer.java similarity index 92% rename from spring-web-reactive/src/test/java/org/springframework/reactive/web/http/TomcatHttpServer.java rename to spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/TomcatHttpServer.java index 18716d4e97..16e37d0e85 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/TomcatHttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/TomcatHttpServer.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.reactive.web.http; +package org.springframework.reactive.web.http.servlet; import java.io.File; @@ -22,7 +22,8 @@ import org.apache.catalina.LifecycleException; import org.apache.catalina.startup.Tomcat; import org.springframework.beans.factory.InitializingBean; -import org.springframework.reactive.web.http.servlet.HttpHandlerServlet; +import org.springframework.reactive.web.http.HttpServer; +import org.springframework.reactive.web.http.HttpServerSupport; import org.springframework.util.Assert; import org.springframework.util.SocketUtils; diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/AbstractHttpHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/AbstractHttpHandlerIntegrationTests.java index 4a01fa334c..56d36384e8 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/AbstractHttpHandlerIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/AbstractHttpHandlerIntegrationTests.java @@ -21,6 +21,10 @@ import org.junit.Before; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; +import org.springframework.reactive.web.http.reactor.ReactorHttpServer; +import org.springframework.reactive.web.http.rxnetty.RxNettyHttpServer; +import org.springframework.reactive.web.http.servlet.JettyHttpServer; +import org.springframework.reactive.web.http.servlet.TomcatHttpServer; import org.springframework.util.SocketUtils; From 0042a65c532618f2a5af952c9d41b889da5c21de Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Fri, 9 Oct 2015 11:38:35 +0200 Subject: [PATCH 056/498] Await properly Reactor server start/stop The goal is to try to fix #32 With this change de Reactor test seems to execute slowly. --- .../web/http/reactor/ReactorHttpServer.java | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorHttpServer.java index 063b725e9f..a0c0c90237 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorHttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorHttpServer.java @@ -18,7 +18,6 @@ package org.springframework.reactive.web.http.reactor; import org.springframework.beans.factory.InitializingBean; import org.springframework.reactive.web.http.HttpServer; import org.springframework.reactive.web.http.HttpServerSupport; -import org.springframework.reactive.web.http.reactor.RequestHandlerAdapter; import org.springframework.util.Assert; import reactor.bus.selector.Selectors; import reactor.io.buffer.Buffer; @@ -50,24 +49,33 @@ public class ReactorHttpServer extends HttpServerSupport implements Initializing this.reactorHandler = new RequestHandlerAdapter(getHttpHandler()); this.reactorServer = (getPort() != -1 ? - NetStreams.httpServer(getPort()) : - NetStreams.httpServer()); + NetStreams.httpServer(getPort()) : NetStreams.httpServer()); } @Override public void start() { if (!this.running) { - this.running = true; - this.reactorServer.route(Selectors.matchAll(), this.reactorHandler).start(); + try { + this.reactorServer.route(Selectors.matchAll(), this.reactorHandler).start().await(); + this.running = true; + } + catch (InterruptedException ex) { + throw new IllegalStateException(ex); + } } } @Override public void stop() { if (this.running) { - this.running = false; - this.reactorServer.shutdown(); + try { + this.reactorServer.shutdown().await(); + this.running = false; + } + catch (InterruptedException ex) { + throw new IllegalStateException(ex); + } } } From 985640b6ceb24540e056d71423229cd54f32f1ac Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Fri, 9 Oct 2015 16:25:02 +0100 Subject: [PATCH 057/498] Fix ReactorHttpServer with latest changes on shutdown behavior --- .../web/http/reactor/ReactorHttpServer.java | 31 ++++++++----------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorHttpServer.java index a0c0c90237..76deb8ac29 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorHttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorHttpServer.java @@ -13,21 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.reactive.web.http.reactor; +import reactor.bus.selector.Selectors; +import reactor.io.buffer.Buffer; +import reactor.io.net.NetStreams; + import org.springframework.beans.factory.InitializingBean; import org.springframework.reactive.web.http.HttpServer; import org.springframework.reactive.web.http.HttpServerSupport; import org.springframework.util.Assert; -import reactor.bus.selector.Selectors; -import reactor.io.buffer.Buffer; -import reactor.io.net.NetStreams; - /** * @author Stephane Maldini */ -public class ReactorHttpServer extends HttpServerSupport implements InitializingBean, HttpServer { +public class ReactorHttpServer extends HttpServerSupport + implements InitializingBean, HttpServer { private RequestHandlerAdapter reactorHandler; @@ -35,29 +37,27 @@ public class ReactorHttpServer extends HttpServerSupport implements Initializing private boolean running; - @Override public boolean isRunning() { return this.running; } - @Override public void afterPropertiesSet() throws Exception { Assert.notNull(getHttpHandler()); this.reactorHandler = new RequestHandlerAdapter(getHttpHandler()); - this.reactorServer = (getPort() != -1 ? - NetStreams.httpServer(getPort()) : NetStreams.httpServer()); + this.reactorServer = (getPort() != -1 ? NetStreams.httpServer(getPort()) : + NetStreams.httpServer()); } - @Override public void start() { if (!this.running) { try { - this.reactorServer.route(Selectors.matchAll(), this.reactorHandler).start().await(); + this.reactorServer.route(Selectors.matchAll(), this.reactorHandler) + .start().await(); this.running = true; } catch (InterruptedException ex) { @@ -69,13 +69,8 @@ public class ReactorHttpServer extends HttpServerSupport implements Initializing @Override public void stop() { if (this.running) { - try { - this.reactorServer.shutdown().await(); - this.running = false; - } - catch (InterruptedException ex) { - throw new IllegalStateException(ex); - } + this.reactorServer.shutdown(); + this.running = false; } } From b11bef7a26bb7733868df2b92d6e0f41856d71d6 Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Fri, 16 Oct 2015 09:07:19 +0200 Subject: [PATCH 058/498] Eliminate need for rxJava in Json Encoder --- .../codec/encoder/JsonObjectEncoder.java | 84 ++++++++++++++----- 1 file changed, 65 insertions(+), 19 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JsonObjectEncoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JsonObjectEncoder.java index e162dd9ed3..dd324a3c6f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JsonObjectEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JsonObjectEncoder.java @@ -16,48 +16,94 @@ package org.springframework.reactive.codec.encoder; -import java.nio.ByteBuffer; - import org.reactivestreams.Publisher; -import reactor.rx.Promise; -import rx.Observable; -import rx.RxReactiveStreams; - +import org.reactivestreams.Subscriber; import org.springframework.core.ResolvableType; import org.springframework.http.MediaType; import org.springframework.reactive.codec.decoder.JsonObjectDecoder; +import org.springframework.util.ClassUtils; + +import reactor.core.subscriber.SubscriberBarrier; +import reactor.io.buffer.Buffer; +import reactor.rx.Promise; +import rx.Observable; + +import java.nio.ByteBuffer; +import java.util.Arrays; + +import static reactor.Publishers.*; /** * Encode a bye stream of individual JSON element to a byte stream representing a single * JSON array when {@code Hints.ENCODE_AS_ARRAY} is enabled. * * @author Sebastien Deleuze + * @author Stephane Maldini + * * @see JsonObjectDecoder */ public class JsonObjectEncoder implements MessageToByteEncoder { - private final ByteBuffer START_ARRAY = ByteBuffer.wrap("[".getBytes()); + private static final boolean rxJava1Present = + ClassUtils.isPresent("rx.Observable", JsonObjectEncoder.class.getClassLoader()); - private final ByteBuffer END_ARRAY = ByteBuffer.wrap("]".getBytes()); + private static final boolean reactorPresent = + ClassUtils.isPresent("reactor.rx.Promise", JsonObjectEncoder.class.getClassLoader()); - private final ByteBuffer COMMA = ByteBuffer.wrap(",".getBytes()); + final ByteBuffer START_ARRAY = ByteBuffer.wrap("[".getBytes()); + + final ByteBuffer END_ARRAY = ByteBuffer.wrap("]".getBytes()); + + final ByteBuffer COMMA = ByteBuffer.wrap(",".getBytes()); @Override public boolean canEncode(ResolvableType type, MediaType mediaType, Object... hints) { - return mediaType.isCompatibleWith(MediaType.APPLICATION_JSON) && !Promise.class.isAssignableFrom(type.getRawClass()) && - (Observable.class.isAssignableFrom(type.getRawClass()) || Publisher.class.isAssignableFrom(type.getRawClass())); + return mediaType.isCompatibleWith(MediaType.APPLICATION_JSON) && + !(reactorPresent && Promise.class.isAssignableFrom(type.getRawClass())) && + (rxJava1Present && Observable.class.isAssignableFrom(type.getRawClass()) + || Publisher.class.isAssignableFrom(type.getRawClass())); } @Override - public Publisher encode(Publisher messageStream, ResolvableType type, MediaType mediaType, Object... hints) { - // TODO We use RxJava Observable because there is no skipLast() operator in Reactor - // TODO Merge some chunks, there is no need to have chunks with only '[', ']' or ',' characters - return RxReactiveStreams.toPublisher( - Observable.concat( - Observable.just(START_ARRAY), - RxReactiveStreams.toObservable(messageStream).flatMap(b -> Observable.just(b, COMMA)).skipLast(1), - Observable.just(END_ARRAY))); + public Publisher encode(Publisher messageStream, ResolvableType type, MediaType + mediaType, Object... hints) { + //TODO Merge some chunks, there is no need to have chunks with only '[', ']' or ',' characters + return + concat( + from( + Arrays.>asList( + just(START_ARRAY), + lift( + flatMap(messageStream, (ByteBuffer b) -> from(Arrays.asList(b, COMMA))), + sub -> new SkipLastBarrier(sub) + ), + just(END_ARRAY) + ) + ) + ); } + private static class SkipLastBarrier extends SubscriberBarrier { + + public SkipLastBarrier(Subscriber subscriber) { + super(subscriber); + } + + ByteBuffer prev = null; + + @Override + protected void doNext(ByteBuffer next) { + if (prev == null) { + prev = next; + doRequest(1); + return; + } + + ByteBuffer tmp = prev; + prev = next; + subscriber.onNext(tmp); + } + + } } From 80f9a21b9ddc83543f9a48a38cb65538307afbc6 Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Fri, 16 Oct 2015 09:10:07 +0200 Subject: [PATCH 059/498] Tweaks to ByteBufPubInputStream to use available blockingQueue facilities --- .../io/ByteBufferPublisherInputStream.java | 113 ++++--------- .../reactive/util/PublisherSignal.java | 156 ------------------ 2 files changed, 29 insertions(+), 240 deletions(-) delete mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/util/PublisherSignal.java diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteBufferPublisherInputStream.java b/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteBufferPublisherInputStream.java index e5ccea71e4..13f82b1c48 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteBufferPublisherInputStream.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteBufferPublisherInputStream.java @@ -16,28 +16,27 @@ package org.springframework.reactive.io; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscription; +import org.springframework.util.Assert; +import reactor.Publishers; +import reactor.core.error.CancelException; + import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; - -import org.reactivestreams.Publisher; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; - -import org.springframework.reactive.util.PublisherSignal; -import org.springframework.util.Assert; /** * {@code InputStream} implementation based on a byte array {@link Publisher}. + * * @author Arjen Poutsma * @author Sebastien Deleuze + * @author Stephane Maldini */ public class ByteBufferPublisherInputStream extends InputStream { - private final BlockingQueue> queue = - new LinkedBlockingQueue<>(); + private final BlockingQueue queue; private ByteBufferInputStream currentStream; @@ -46,6 +45,7 @@ public class ByteBufferPublisherInputStream extends InputStream { /** * Creates a new {@code ByteArrayPublisherInputStream} based on the given publisher. + * * @param publisher the publisher to use */ public ByteBufferPublisherInputStream(Publisher publisher) { @@ -54,14 +54,15 @@ public class ByteBufferPublisherInputStream extends InputStream { /** * Creates a new {@code ByteArrayPublisherInputStream} based on the given publisher. - * @param publisher the publisher to use + * + * @param publisher the publisher to use * @param requestSize the {@linkplain Subscription#request(long) request size} to use - * on the publisher + * on the publisher bound to Integer MAX */ - public ByteBufferPublisherInputStream(Publisher publisher, long requestSize) { + public ByteBufferPublisherInputStream(Publisher publisher, int requestSize) { Assert.notNull(publisher, "'publisher' must not be null"); - publisher.subscribe(new BlockingQueueSubscriber(requestSize)); + this.queue = Publishers.toReadQueue(publisher, requestSize); } @@ -128,85 +129,29 @@ public class ByteBufferPublisherInputStream extends InputStream { try { if (this.currentStream != null && this.currentStream.available() > 0) { return this.currentStream; - } - else { - // take() blocks, but that's OK since this is a *blocking* InputStream - PublisherSignal signal = this.queue.take(); - - if (signal.isData()) { - ByteBuffer data = signal.data(); - this.currentStream = new ByteBufferInputStream(data); - return this.currentStream; - } - else if (signal.isComplete()) { + } else { + // take() blocks until next or complete() then return null, but that's OK since this is a *blocking* InputStream + ByteBuffer signal = this.queue.take(); + if(signal == null){ this.completed = true; return null; } - else if (signal.isError()) { - Throwable error = signal.error(); - this.completed = true; - if (error instanceof IOException) { - throw (IOException) error; - } - else { - throw new IOException(error); - } - } + this.currentStream = new ByteBufferInputStream(signal); + return this.currentStream; } } + catch (CancelException ce) { + this.completed = true; + return null; + } catch (InterruptedException ex) { Thread.currentThread().interrupt(); } + catch (Throwable error ){ + this.completed = true; + throw new IOException(error); + } throw new IOException(); } - private class BlockingQueueSubscriber implements Subscriber { - - private final long requestSize; - - private Subscription subscription; - - public BlockingQueueSubscriber(long requestSize) { - this.requestSize = requestSize; - } - - @Override - public void onSubscribe(Subscription subscription) { - this.subscription = subscription; - - this.subscription.request(this.requestSize); - } - - @Override - public void onNext(ByteBuffer bytes) { - try { - queue.put(PublisherSignal.data(bytes)); - this.subscription.request(requestSize); - } - catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - } - } - - @Override - public void onError(Throwable t) { - try { - queue.put(PublisherSignal.error(t)); - } - catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - } - } - - @Override - public void onComplete() { - try { - queue.put(PublisherSignal.complete()); - } - catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - } - } - } - } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/util/PublisherSignal.java b/spring-web-reactive/src/main/java/org/springframework/reactive/util/PublisherSignal.java deleted file mode 100644 index 46ab3572eb..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/util/PublisherSignal.java +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Copyright 2002-2015 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.reactive.util; - -import org.reactivestreams.Publisher; - -import org.springframework.util.Assert; - -/** - * Represents a signal value object, useful for wrapping signals as published by a {@link - * Publisher}. Mostly used to store signals in buffers. - * @author Arjen Poutsma - */ -public abstract class PublisherSignal { - - protected PublisherSignal() { - } - - /** - * Indicates whether this signal is an data signal, i.e. if {@link #data()} can be - * called safely. - * @return {@code true} if this signal contains data; {@code false} otherwise - */ - public boolean isData() { - return false; - } - - /** - * Returns the data contained in this signal. Can only be safely called after {@link - * #isData()} returns {@code true}. - * @return the data - * @throws IllegalStateException if this signal does not contain data - */ - public T data() { - throw new IllegalStateException(); - } - - /** - * Indicates whether this signal is an error signal, i.e. if {@link #error()} can be - * called safely. - * @return {@code true} if this signal contains an error; {@code false} otherwise - */ - public boolean isError() { - return false; - } - - /** - * Returns the error contained in this signal. Can only be safely called after {@link - * #isError()} returns {@code true}. - * @return the error - * @throws IllegalStateException if this signal does not contain an error - */ - public Throwable error() { - throw new IllegalStateException(); - } - - /** - * Indicates whether this signal completes the stream. - * @return {@code true} if this signal completes the stream; {@code false} otherwise - */ - public boolean isComplete() { - return false; - } - - /** - * Creates a new data signal with the given {@code t}. - * @param t the data to base the signal on - * @return the newly created signal - */ - public static PublisherSignal data(T t) { - Assert.notNull(t, "'t' must not be null"); - return new DataSignal<>(t); - } - - /** - * Creates a new error signal with the given {@code Throwable}. - * @param t the exception to base the signal on - * @return the newly created signal - */ - public static PublisherSignal error(Throwable t) { - Assert.notNull(t, "'t' must not be null"); - return new ErrorSignal<>(t); - } - - /** - * Returns the complete signal, typically the last signal in a stream. - */ - @SuppressWarnings("unchecked") - public static PublisherSignal complete() { - return (PublisherSignal)ON_COMPLETE; - } - - private static final class DataSignal extends PublisherSignal { - - private final T data; - - public DataSignal(T data) { - this.data = data; - } - - @Override - public boolean isData() { - return true; - } - - @Override - public T data() { - return data; - } - } - - private static final class ErrorSignal extends PublisherSignal { - - private final Throwable error; - - public ErrorSignal(Throwable error) { - this.error = error; - } - - @Override - public boolean isError() { - return true; - } - - @Override - public Throwable error() { - return error; - } - - } - - @SuppressWarnings("rawtypes") - private static final PublisherSignal ON_COMPLETE = new PublisherSignal() { - - @Override - public boolean isComplete() { - return true; - } - - }; - -} From 06a1ddbe93d7da3db4a354c1d46c30385b919f77 Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Fri, 16 Oct 2015 09:11:22 +0200 Subject: [PATCH 060/498] Eliminate need for reactor stream in CompletableFutureUtils + fixes --- .../reactive/util/CompletableFutureUtils.java | 93 ++++++++++--------- 1 file changed, 51 insertions(+), 42 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/util/CompletableFutureUtils.java b/spring-web-reactive/src/main/java/org/springframework/reactive/util/CompletableFutureUtils.java index cb3aa16e8d..ff5e19e1ad 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/util/CompletableFutureUtils.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/util/CompletableFutureUtils.java @@ -16,28 +16,28 @@ package org.springframework.reactive.util; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CompletableFuture; - import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; -import reactor.core.error.Exceptions; -import reactor.core.error.SpecificationExceptions; -import reactor.core.support.BackpressureUtils; -import reactor.rx.Stream; -import reactor.rx.subscription.ReactiveSubscription; - import org.springframework.util.Assert; +import reactor.Publishers; +import reactor.core.error.CancelException; +import reactor.core.error.Exceptions; +import reactor.core.support.BackpressureUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicLongFieldUpdater; /** * @author Sebastien Deleuze + * @author Stephane Maldini */ public class CompletableFutureUtils { public static Publisher toPublisher(CompletableFuture future) { - return new CompletableFutureStream(future); + return new CompletableFuturePublisher(future); } public static CompletableFuture> fromPublisher(Publisher publisher) { @@ -97,46 +97,55 @@ public class CompletableFutureUtils { return future; } - private static class CompletableFutureStream extends Stream { + private static class CompletableFuturePublisher implements Publisher { private final CompletableFuture future; + private final Publisher futurePublisher; - public CompletableFutureStream(CompletableFuture future) { + @SuppressWarnings("unused") + private volatile long requested; + private static final AtomicLongFieldUpdater REQUESTED = + AtomicLongFieldUpdater.newUpdater(CompletableFuturePublisher.class, "requested"); + + public CompletableFuturePublisher(CompletableFuture future) { this.future = future; + this.futurePublisher = Publishers.createWithDemand((n, sub) -> { + + if (!BackpressureUtils.checkRequest(n, sub)) { + return; + } + + if(BackpressureUtils.getAndAdd(REQUESTED, CompletableFuturePublisher.this, n) > 0) { + return; + } + + future.whenComplete((result, error) -> { + if (error != null) { + sub.onError(error); + } else { + sub.onNext(result); + sub.onComplete(); + } + }); + }, null, nothing -> { + if(!future.isDone()){ + future.cancel(true); + } + }); } @Override public void subscribe(final Subscriber subscriber) { try { - subscriber.onSubscribe(new ReactiveSubscription(this, subscriber) { - - @Override - public void request(long elements) { - try{ - BackpressureUtils.checkRequest(elements); - } - catch (SpecificationExceptions.Spec309_NullOrNegativeRequest iae) { - subscriber.onError(iae); - return; - } - if (isComplete()) { - return; - } - try { - future.whenComplete((result, error) -> { - if (error != null) { - onError(error); - } else { - subscriber.onNext(result); - onComplete(); - } - }); - } - catch (Throwable e) { - onError(e); - } - } - }); + if (future.isDone()) { + Publishers.just(future.get()).subscribe(subscriber); + } + else if ( future.isCancelled()){ + Exceptions.publisher(CancelException.get()); + } + else { + futurePublisher.subscribe(subscriber); + } } catch (Throwable throwable) { Exceptions.publisher(throwable); From 8ca04b84259f1840db7e1037828f7120427eb7b1 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Fri, 16 Oct 2015 09:13:44 +0200 Subject: [PATCH 061/498] More accurate ResponseBodyResultHandler error message --- .../dispatch/method/annotation/ResponseBodyResultHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java index 9e4431d116..43b55f5d44 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java @@ -142,7 +142,7 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered return response.writeWith(outputStream); } return Publishers.error(new IllegalStateException( - "Return value type not supported: " + returnType)); + "Return value type '" + returnType.getParameterType().getName() + "' with media type '" + mediaType + "' not supported" )); } private MediaType resolveMediaType(ServerHttpRequest request) { From 3ad765556d1d2b0543bb2a61a8fcb7ce8b47b713 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Fri, 16 Oct 2015 15:35:16 +0200 Subject: [PATCH 062/498] Polish ByteBufferPublisherInputStream exception handling --- .../reactive/io/ByteBufferPublisherInputStream.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteBufferPublisherInputStream.java b/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteBufferPublisherInputStream.java index 13f82b1c48..647e688712 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteBufferPublisherInputStream.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteBufferPublisherInputStream.java @@ -20,7 +20,6 @@ import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; import org.springframework.util.Assert; import reactor.Publishers; -import reactor.core.error.CancelException; import java.io.IOException; import java.io.InputStream; @@ -140,10 +139,6 @@ public class ByteBufferPublisherInputStream extends InputStream { return this.currentStream; } } - catch (CancelException ce) { - this.completed = true; - return null; - } catch (InterruptedException ex) { Thread.currentThread().interrupt(); } From cb09e58a8258eb8a08666f9aeee60ce0ab62424e Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Mon, 19 Oct 2015 11:01:24 +0200 Subject: [PATCH 063/498] Fix SimpleHandlerResultHandler by using Publishers.completable() --- .../reactive/web/dispatch/SimpleHandlerResultHandler.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/SimpleHandlerResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/SimpleHandlerResultHandler.java index c26000c394..c72ad8f5dd 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/SimpleHandlerResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/SimpleHandlerResultHandler.java @@ -17,6 +17,7 @@ package org.springframework.reactive.web.dispatch; import org.reactivestreams.Publisher; +import reactor.Publishers; import org.springframework.core.Ordered; import org.springframework.reactive.web.http.ServerHttpRequest; @@ -44,6 +45,6 @@ public class SimpleHandlerResultHandler implements Ordered, HandlerResultHandler @Override public Publisher handleResult(ServerHttpRequest request, ServerHttpResponse response, HandlerResult result) { - return (Publisher)result.getValue(); + return Publishers.completable((Publisher)result.getValue()); } } From cf2c1514af43bc955571ede20876d6cda1779f11 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Mon, 19 Oct 2015 11:15:03 +0200 Subject: [PATCH 064/498] Use latest Reactor core capabilities No need for rxjava-reactive-streams dependency and CompletableFutureUtils anymore. --- spring-web-reactive/build.gradle | 1 - .../reactive/util/CompletableFutureUtils.java | 156 ------------------ .../RequestBodyArgumentResolver.java | 11 +- .../annotation/ResponseBodyResultHandler.java | 11 +- .../http/rxnetty/RequestHandlerAdapter.java | 4 +- .../rxnetty/RxNettyServerHttpRequest.java | 21 ++- .../rxnetty/RxNettyServerHttpResponse.java | 21 ++- .../util/CompletableFutureUtilsTests.java | 101 ------------ 8 files changed, 40 insertions(+), 286 deletions(-) delete mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/util/CompletableFutureUtils.java delete mode 100644 spring-web-reactive/src/test/java/org/springframework/reactive/util/CompletableFutureUtilsTests.java diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index 8d0d6709f5..f3682d0902 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -40,7 +40,6 @@ dependencies { optional "com.fasterxml.jackson.core:jackson-databind:2.6.1" optional "io.reactivex:rxnetty:0.5.0-SNAPSHOT" - optional "io.reactivex:rxjava-reactive-streams:1.0.1" optional "io.projectreactor:reactor-net:2.1.0.BUILD-SNAPSHOT" diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/util/CompletableFutureUtils.java b/spring-web-reactive/src/main/java/org/springframework/reactive/util/CompletableFutureUtils.java deleted file mode 100644 index ff5e19e1ad..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/util/CompletableFutureUtils.java +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Copyright 2002-2015 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.reactive.util; - -import org.reactivestreams.Publisher; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; -import org.springframework.util.Assert; -import reactor.Publishers; -import reactor.core.error.CancelException; -import reactor.core.error.Exceptions; -import reactor.core.support.BackpressureUtils; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.atomic.AtomicLongFieldUpdater; - -/** - * @author Sebastien Deleuze - * @author Stephane Maldini - */ -public class CompletableFutureUtils { - - public static Publisher toPublisher(CompletableFuture future) { - return new CompletableFuturePublisher(future); - } - - public static CompletableFuture> fromPublisher(Publisher publisher) { - final CompletableFuture> future = new CompletableFuture<>(); - publisher.subscribe(new Subscriber() { - private final List values = new ArrayList<>(); - - @Override - public void onSubscribe(Subscription s) { - s.request(Long.MAX_VALUE); - } - - @Override - public void onNext(T t) { - values.add(t); - } - - @Override - public void onError(Throwable t) { - future.completeExceptionally(t); - } - - @Override - public void onComplete() { - future.complete(values); - } - }); - return future; - } - - public static CompletableFuture fromSinglePublisher(Publisher publisher) { - final CompletableFuture future = new CompletableFuture<>(); - publisher.subscribe(new Subscriber() { - private T value; - - @Override - public void onSubscribe(Subscription s) { - s.request(Long.MAX_VALUE); - } - - @Override - public void onNext(T t) { - Assert.state(value == null, "This publisher should not publish multiple values"); - value = t; - } - - @Override - public void onError(Throwable t) { - future.completeExceptionally(t); - } - - @Override - public void onComplete() { - future.complete(value); - } - }); - return future; - } - - private static class CompletableFuturePublisher implements Publisher { - - private final CompletableFuture future; - private final Publisher futurePublisher; - - @SuppressWarnings("unused") - private volatile long requested; - private static final AtomicLongFieldUpdater REQUESTED = - AtomicLongFieldUpdater.newUpdater(CompletableFuturePublisher.class, "requested"); - - public CompletableFuturePublisher(CompletableFuture future) { - this.future = future; - this.futurePublisher = Publishers.createWithDemand((n, sub) -> { - - if (!BackpressureUtils.checkRequest(n, sub)) { - return; - } - - if(BackpressureUtils.getAndAdd(REQUESTED, CompletableFuturePublisher.this, n) > 0) { - return; - } - - future.whenComplete((result, error) -> { - if (error != null) { - sub.onError(error); - } else { - sub.onNext(result); - sub.onComplete(); - } - }); - }, null, nothing -> { - if(!future.isDone()){ - future.cancel(true); - } - }); - } - - @Override - public void subscribe(final Subscriber subscriber) { - try { - if (future.isDone()) { - Publishers.just(future.get()).subscribe(subscriber); - } - else if ( future.isCancelled()){ - Exceptions.publisher(CancelException.get()); - } - else { - futurePublisher.subscribe(subscriber); - } - } - catch (Throwable throwable) { - Exceptions.publisher(throwable); - } - } - } - -} \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java index f0607240ac..8a77f0b298 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java @@ -26,11 +26,13 @@ import java.util.concurrent.TimeUnit; import org.reactivestreams.Publisher; import reactor.Publishers; +import reactor.core.publisher.convert.CompletableFutureConverter; +import reactor.core.publisher.convert.RxJava1Converter; +import reactor.core.publisher.convert.RxJava1SingleConverter; import reactor.rx.Promise; import reactor.rx.Stream; import reactor.rx.Streams; import rx.Observable; -import rx.RxReactiveStreams; import rx.Single; import org.springframework.core.MethodParameter; @@ -38,7 +40,6 @@ import org.springframework.core.ResolvableType; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.reactive.codec.decoder.ByteToMessageDecoder; -import org.springframework.reactive.util.CompletableFutureUtils; import org.springframework.reactive.web.dispatch.method.HandlerMethodArgumentResolver; import org.springframework.reactive.web.http.ServerHttpRequest; import org.springframework.web.bind.annotation.RequestBody; @@ -104,13 +105,13 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve return Streams.wrap(elementStream).take(1).next(); } else if (Observable.class.isAssignableFrom(type.getRawClass())) { - return RxReactiveStreams.toObservable(elementStream); + return RxJava1Converter.from(elementStream); } else if (Single.class.isAssignableFrom(type.getRawClass())) { - return RxReactiveStreams.toObservable(elementStream).toSingle(); + return RxJava1SingleConverter.from(elementStream); } else if (CompletableFuture.class.isAssignableFrom(type.getRawClass())) { - return CompletableFutureUtils.fromSinglePublisher(elementStream); + return CompletableFutureConverter.fromSingle(elementStream); } else if (Publisher.class.isAssignableFrom(type.getRawClass())) { return elementStream; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java index 43b55f5d44..eee28f723e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java @@ -24,7 +24,6 @@ import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.reactive.codec.encoder.MessageToByteEncoder; -import org.springframework.reactive.util.CompletableFutureUtils; import org.springframework.reactive.web.dispatch.HandlerResult; import org.springframework.reactive.web.dispatch.HandlerResultHandler; import org.springframework.reactive.web.http.ServerHttpRequest; @@ -32,9 +31,11 @@ import org.springframework.reactive.web.http.ServerHttpResponse; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.method.HandlerMethod; import reactor.Publishers; +import reactor.core.publisher.convert.CompletableFutureConverter; +import reactor.core.publisher.convert.RxJava1Converter; +import reactor.core.publisher.convert.RxJava1SingleConverter; import reactor.rx.Promise; import rx.Observable; -import rx.RxReactiveStreams; import rx.Single; import java.lang.reflect.Type; @@ -118,13 +119,13 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered elementStream = ((Promise)value).stream(); } else if (Observable.class.isAssignableFrom(type.getRawClass())) { - elementStream = RxReactiveStreams.toPublisher((Observable) value); + elementStream = RxJava1Converter.from((Observable) value); } else if (Single.class.isAssignableFrom(type.getRawClass())) { - elementStream = RxReactiveStreams.toPublisher(((Single)value).toObservable()); + elementStream = RxJava1SingleConverter.from((Single)value); } else if (CompletableFuture.class.isAssignableFrom(type.getRawClass())) { - elementStream = CompletableFutureUtils.toPublisher((CompletableFuture) value); + elementStream = CompletableFutureConverter.from((CompletableFuture) value); } else if (Publisher.class.isAssignableFrom(type.getRawClass())) { elementStream = (Publisher)value; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RequestHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RequestHandlerAdapter.java index 0b22686e0d..4149dce5ea 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RequestHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RequestHandlerAdapter.java @@ -20,8 +20,8 @@ import io.reactivex.netty.protocol.http.server.HttpServerRequest; import io.reactivex.netty.protocol.http.server.HttpServerResponse; import io.reactivex.netty.protocol.http.server.RequestHandler; import org.reactivestreams.Publisher; +import reactor.core.publisher.convert.RxJava1Converter; import rx.Observable; -import rx.RxReactiveStreams; import org.springframework.reactive.web.http.HttpHandler; import org.springframework.util.Assert; @@ -44,7 +44,7 @@ public class RequestHandlerAdapter implements RequestHandler { RxNettyServerHttpRequest adaptedRequest = new RxNettyServerHttpRequest(request); RxNettyServerHttpResponse adaptedResponse = new RxNettyServerHttpResponse(response); Publisher result = this.httpHandler.handle(adaptedRequest, adaptedResponse); - return RxReactiveStreams.toObservable(result); + return RxJava1Converter.from(result); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpRequest.java index e422bb7502..7aa05a50cd 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpRequest.java @@ -15,22 +15,24 @@ */ package org.springframework.reactive.web.http.rxnetty; -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.ByteBuffer; - import io.netty.buffer.ByteBuf; import io.reactivex.netty.protocol.http.server.HttpServerRequest; import org.reactivestreams.Publisher; -import rx.Observable; - import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.reactive.web.http.ServerHttpRequest; import org.springframework.util.Assert; +import reactor.core.publisher.convert.RxJava1Converter; +import rx.Observable; + +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.ByteBuffer; + /** * @author Rossen Stoyanchev + * @author Stephane Maldini */ public class RxNettyServerHttpRequest implements ServerHttpRequest { @@ -76,8 +78,11 @@ public class RxNettyServerHttpRequest implements ServerHttpRequest { @Override public Publisher getBody() { - Observable bytesContent = this.request.getContent().map(byteBuf -> byteBuf.nioBuffer()); - return rx.RxReactiveStreams.toPublisher(bytesContent); + Observable bytesContent = this.request.getContent().map(ByteBuf::nioBuffer); + return RxJava1Converter.from(bytesContent); } + public Observable asObservable() { + return this.request.getContent().map(ByteBuf::nioBuffer); + } } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpResponse.java index f8e941d639..161589cdcd 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpResponse.java @@ -15,22 +15,23 @@ */ package org.springframework.reactive.web.http.rxnetty; -import java.nio.ByteBuffer; - import io.netty.handler.codec.http.HttpResponseStatus; import io.reactivex.netty.protocol.http.server.HttpServerResponse; import org.reactivestreams.Publisher; -import reactor.io.buffer.Buffer; -import rx.Observable; -import rx.RxReactiveStreams; - import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.reactive.web.http.ServerHttpResponse; import org.springframework.util.Assert; +import reactor.core.publisher.convert.RxJava1Converter; +import reactor.io.buffer.Buffer; +import rx.Observable; + +import java.nio.ByteBuffer; + /** * @author Rossen Stoyanchev + * @author Stephane Maldini */ public class RxNettyServerHttpResponse implements ServerHttpResponse { @@ -58,11 +59,15 @@ public class RxNettyServerHttpResponse implements ServerHttpResponse { return (this.headersWritten ? HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers); } + public Observable writeWith(Observable contentPublisher) { + return this.response.writeBytes(contentPublisher.map(content -> new Buffer(content).asBytes())); + } + @Override public Publisher writeWith(Publisher contentPublisher) { writeHeaders(); - Observable contentObservable = RxReactiveStreams.toObservable(contentPublisher).map(content -> new Buffer(content).asBytes()); - return RxReactiveStreams.toPublisher(this.response.writeBytes(contentObservable)); + Observable contentObservable = RxJava1Converter.from(contentPublisher).map(content -> new Buffer(content).asBytes()); + return RxJava1Converter.from(this.response.writeBytes(contentObservable)); } private void writeHeaders() { diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/util/CompletableFutureUtilsTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/util/CompletableFutureUtilsTests.java deleted file mode 100644 index 2ea5a305e4..0000000000 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/util/CompletableFutureUtilsTests.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright 2002-2015 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.reactive.util; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import org.junit.Test; -import org.reactivestreams.Publisher; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; -import reactor.rx.Streams; - -/** - * @author Sebastien Deleuze - */ -public class CompletableFutureUtilsTests { - - private CountDownLatch lock = new CountDownLatch(1); - private final List results = new ArrayList<>(); - private final List errors = new ArrayList<>(); - - @Test - public void fromPublisher() throws InterruptedException { - Publisher publisher = Streams.just(true, false); - CompletableFuture> future = CompletableFutureUtils.fromPublisher(publisher); - future.whenComplete((result, error) -> { - if (error != null) { - errors.add(error); - } - else { - results.addAll(result); - } - lock.countDown(); - }); - lock.await(2000, TimeUnit.MILLISECONDS); - assertEquals("onError not expected: " + errors.toString(), 0, errors.size()); - assertEquals(2, results.size()); - assertTrue(results.get(0)); - assertFalse(results.get(1)); - } - - @Test - public void fromSinglePublisher() throws InterruptedException { - Publisher publisher = Streams.just(true); - CompletableFuture future = CompletableFutureUtils.fromSinglePublisher(publisher); - future.whenComplete((result, error) -> { - if (error != null) { - errors.add(error); - } - else { - results.add(result); - } - lock.countDown(); - }); - lock.await(2000, TimeUnit.MILLISECONDS); - assertEquals("onError not expected: " + errors.toString(), 0, errors.size()); - assertEquals(1, results.size()); - assertTrue(results.get(0)); - } - - @Test - public void fromSinglePublisherWithMultipleValues() throws InterruptedException { - Publisher publisher = Streams.just(true, false); - CompletableFuture future = CompletableFutureUtils.fromSinglePublisher(publisher); - future.whenComplete((result, error) -> { - if (error != null) { - errors.add(error); - } - else { - results.add(result); - } - lock.countDown(); - }); - lock.await(2000, TimeUnit.MILLISECONDS); - assertEquals(1, errors.size()); - assertEquals(IllegalStateException.class, errors.get(0).getClass()); - assertEquals(0, results.size()); - } - -} \ No newline at end of file From adc50bbfb9e95e742c36ee3e99c1550e6638bcfa Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Mon, 19 Oct 2015 17:00:52 +0200 Subject: [PATCH 065/498] Add handler method parameter and result converters This commit introduces the following changes: - Publisher -> Observable/Stream/etc. conversion is now managed in a dedicated ConversionService instead of directly in RequestBodyArgumentResolver and ResponseBodyResultHandler - More isolated logic that decides if the stream should be serialized as a JSON array or not - Publisher are now handled by regular ByteBufferEncoder and ByteBufferDecoder - Handle Publisher return value properly - Ensure that the headers are properly written even for response without body - Improve JsonObjectEncoder to autodetect JSON arrays --- spring-web-reactive/build.gradle | 16 +-- ...veStreamsToCompletableFutureConverter.java | 53 ++++++++++ .../ReactiveStreamsToReactorConverter.java | 62 ++++++++++++ .../ReactiveStreamsToRxJava1Converter.java | 65 +++++++++++++ .../codec/decoder/ByteBufferDecoder.java | 40 ++++++++ .../codec/decoder/ByteToMessageDecoder.java | 6 +- .../codec/decoder/JsonObjectDecoder.java | 11 +-- .../reactive/codec/decoder/StringDecoder.java | 3 +- .../codec/encoder/ByteBufferEncoder.java | 41 ++++++++ .../codec/encoder/JsonObjectEncoder.java | 75 ++++++-------- .../codec/encoder/MessageToByteEncoder.java | 6 +- .../reactive/codec/encoder/StringEncoder.java | 3 +- .../web/dispatch/DispatcherHandler.java | 1 + .../dispatch/SimpleHandlerResultHandler.java | 6 +- .../annotation/DefaultConversionService.java | 47 +++++++++ .../RequestBodyArgumentResolver.java | 97 ++++++------------- .../RequestMappingHandlerAdapter.java | 9 +- .../annotation/ResponseBodyResultHandler.java | 65 ++++++------- .../reactive/web/http/ServerHttpResponse.java | 11 ++- .../reactor/ReactorServerHttpRequest.java | 4 +- .../reactor/ReactorServerHttpResponse.java | 17 +++- .../rxnetty/RxNettyServerHttpResponse.java | 15 ++- .../servlet/ServletServerHttpResponse.java | 11 ++- .../src/main/resources/log4j.properties | 1 + .../codec/decoder/ByteBufferDecoderTests.java | 58 +++++++++++ .../codec/decoder/JsonObjectDecoderTests.java | 30 +++++- .../codec/decoder/StringDecoderTests.java | 11 ++- .../encoder/ByteBufferDecoderEncoder.java | 58 +++++++++++ .../codec/encoder/JsonObjectEncoderTests.java | 36 ++++++- .../codec/encoder/StringEncoderTests.java | 7 +- .../RequestMappingIntegrationTests.java | 93 +++++++++++++++++- .../ResponseBodyResultHandlerTests.java | 2 +- 32 files changed, 758 insertions(+), 202 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToCompletableFutureConverter.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToReactorConverter.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToRxJava1Converter.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/ByteBufferDecoder.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/ByteBufferEncoder.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/DefaultConversionService.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/ByteBufferDecoderTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/ByteBufferDecoderEncoder.java diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index f3682d0902..51509778de 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -31,28 +31,28 @@ configurations.all { } dependencies { - compile "org.springframework:spring-core:4.2.0.RELEASE" - compile "org.springframework:spring-web:4.2.0.RELEASE" + compile "org.springframework:spring-core:4.2.2.RELEASE" + compile "org.springframework:spring-web:4.2.2.RELEASE" compile "org.reactivestreams:reactive-streams:1.0.0" compile "io.projectreactor:reactor-core:2.1.0.BUILD-SNAPSHOT" compile "commons-logging:commons-logging:1.2" - optional "com.fasterxml.jackson.core:jackson-databind:2.6.1" + optional "com.fasterxml.jackson.core:jackson-databind:2.6.2" optional "io.reactivex:rxnetty:0.5.0-SNAPSHOT" optional "io.projectreactor:reactor-net:2.1.0.BUILD-SNAPSHOT" - optional 'org.apache.tomcat:tomcat-util:8.0.24' - optional 'org.apache.tomcat.embed:tomcat-embed-core:8.0.24' + optional 'org.apache.tomcat:tomcat-util:8.0.28' + optional 'org.apache.tomcat.embed:tomcat-embed-core:8.0.28' - optional 'org.eclipse.jetty:jetty-server:9.3.2.v20150730' - optional 'org.eclipse.jetty:jetty-servlet:9.3.2.v20150730' + optional 'org.eclipse.jetty:jetty-server:9.3.5.v20151012' + optional 'org.eclipse.jetty:jetty-servlet:9.3.5.v20151012' provided "javax.servlet:javax.servlet-api:3.1.0" testCompile "junit:junit:4.12" - testCompile "org.springframework:spring-test:4.2.0.RELEASE" + testCompile "org.springframework:spring-test:4.2.2.RELEASE" testCompile "org.slf4j:slf4j-jcl:1.7.12" testCompile "org.slf4j:jul-to-slf4j:1.7.12" diff --git a/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToCompletableFutureConverter.java b/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToCompletableFutureConverter.java new file mode 100644 index 0000000000..046ab43a74 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToCompletableFutureConverter.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2015 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.convert.support; + +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.concurrent.CompletableFuture; + +import org.reactivestreams.Publisher; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.GenericConverter; + +/** + * @author Sebastien Deleuze + */ +public class ReactiveStreamsToCompletableFutureConverter implements GenericConverter { + + @Override + public Set getConvertibleTypes() { + Set convertibleTypes = new LinkedHashSet<>(); + convertibleTypes.add(new GenericConverter.ConvertiblePair(Publisher.class, CompletableFuture.class)); + convertibleTypes.add(new GenericConverter.ConvertiblePair(CompletableFuture.class, Publisher.class)); + return convertibleTypes; + } + + @Override + public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (source != null) { + if (CompletableFuture.class.isAssignableFrom(source.getClass())) { + return reactor.core.publisher.convert.CompletableFutureConverter.from((CompletableFuture)source); + } else if (CompletableFuture.class.isAssignableFrom(targetType.getResolvableType().getRawClass())) { + return reactor.core.publisher.convert.CompletableFutureConverter.fromSingle((Publisher)source); + } + } + return null; + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToReactorConverter.java b/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToReactorConverter.java new file mode 100644 index 0000000000..52d894048c --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToReactorConverter.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2011-2015 Pivotal Software Inc, All Rights Reserved. + * + * 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.convert.support; + +import java.util.LinkedHashSet; +import java.util.Set; + +import org.reactivestreams.Publisher; +import reactor.rx.Promise; +import reactor.rx.Stream; +import reactor.rx.Streams; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.GenericConverter; + +/** + * @author Stephane Maldini + * @author Sebastien Deleuze + */ +public final class ReactiveStreamsToReactorConverter implements GenericConverter { + + @Override + public Set getConvertibleTypes() { + Set convertibleTypes = new LinkedHashSet<>(); + convertibleTypes.add(new GenericConverter.ConvertiblePair(Publisher.class, Stream.class)); + convertibleTypes.add(new GenericConverter.ConvertiblePair(Stream.class, Publisher.class)); + convertibleTypes.add(new GenericConverter.ConvertiblePair(Publisher.class, Promise.class)); + convertibleTypes.add(new GenericConverter.ConvertiblePair(Promise.class, Publisher.class)); + return convertibleTypes; + } + + @Override + public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (source != null) { + if (Stream.class.isAssignableFrom(source.getClass())) { + return source; + } else if (Stream.class.isAssignableFrom(targetType.getResolvableType().getRawClass())) { + return Streams.wrap((Publisher)source); + } else if (Promise.class.isAssignableFrom(source.getClass())) { + return ((Promise)source); + } else if (Promise.class.isAssignableFrom(targetType.getResolvableType().getRawClass())) { + return Streams.wrap((Publisher)source).next(); + } + } + return null; + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToRxJava1Converter.java b/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToRxJava1Converter.java new file mode 100644 index 0000000000..b4b2043dcb --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToRxJava1Converter.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2011-2015 Pivotal Software Inc, All Rights Reserved. + * + * 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.convert.support; + +import java.util.LinkedHashSet; +import java.util.Set; + +import org.reactivestreams.Publisher; +import reactor.core.publisher.convert.RxJava1Converter; +import rx.Observable; +import rx.Single; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.GenericConverter; + +/** + * TODO Avoid classpath exception for older RxJava1 version without Single type + * @author Stephane Maldini + * @author Sebastien Deleuze + */ +public final class ReactiveStreamsToRxJava1Converter implements GenericConverter { + + @Override + public Set getConvertibleTypes() { + Set convertibleTypes = new LinkedHashSet<>(); + convertibleTypes.add(new GenericConverter.ConvertiblePair(Publisher.class, Observable.class)); + convertibleTypes.add(new GenericConverter.ConvertiblePair(Observable.class, Publisher.class)); + convertibleTypes.add(new GenericConverter.ConvertiblePair(Publisher.class, Single.class)); + convertibleTypes.add(new GenericConverter.ConvertiblePair(Single.class, Publisher.class)); + return convertibleTypes; + } + + @Override + public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (source != null) { + if (Observable.class.isAssignableFrom(source.getClass())) { + return RxJava1Converter.from((Observable) source); + } + else if (Observable.class.isAssignableFrom(targetType.getResolvableType().getRawClass())) { + return RxJava1Converter.from((Publisher)source); + } + else if (Single.class.isAssignableFrom(source.getClass())) { + return reactor.core.publisher.convert.RxJava1SingleConverter.from((Single) source); + } else if (Single.class.isAssignableFrom(targetType.getResolvableType().getRawClass())) { + return reactor.core.publisher.convert.RxJava1SingleConverter.from((Publisher)source); + } + } + return null; + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/ByteBufferDecoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/ByteBufferDecoder.java new file mode 100644 index 0000000000..b740dab95b --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/ByteBufferDecoder.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2015 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.reactive.codec.decoder; + +import java.nio.ByteBuffer; + +import org.reactivestreams.Publisher; + +import org.springframework.core.ResolvableType; +import org.springframework.http.MediaType; + +/** + * @author Sebastien Deleuze + */ +public class ByteBufferDecoder implements ByteToMessageDecoder { + + @Override + public boolean canDecode(ResolvableType type, MediaType mediaType, Object... hints) { + return ByteBuffer.class.isAssignableFrom(type.getRawClass()); + } + + @Override + public Publisher decode(Publisher inputStream, ResolvableType type, MediaType mediaType, Object... hints) { + return inputStream; + } +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/ByteToMessageDecoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/ByteToMessageDecoder.java index 888bb69571..3be8b83dae 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/ByteToMessageDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/ByteToMessageDecoder.java @@ -34,8 +34,7 @@ public interface ByteToMessageDecoder { /** * Indicate whether the given type and media type can be processed by this decoder. - * @param type the (potentially generic) type to ultimately decode to. - * Could be different from {@code T} type. + * @param type the stream element type to ultimately decode to. * @param mediaType the media type to decode from. * Typically the value of a {@code Content-Type} header for HTTP request. * @param hints Additional information about how to do decode, optional. @@ -46,8 +45,7 @@ public interface ByteToMessageDecoder { /** * Decode a bytes stream to a message stream. * @param inputStream the input stream that represent the whole object to decode. - * @param type the (potentially generic) type to ultimately decode to. - * Could be different from {@code T} type. + * @param type the stream element type to ultimately decode to. * @param hints Additional information about how to do decode, optional. * @return the decoded message stream */ diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JsonObjectDecoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JsonObjectDecoder.java index c1f9c35926..9831b4035f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JsonObjectDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JsonObjectDecoder.java @@ -23,22 +23,18 @@ import org.reactivestreams.Publisher; import org.springframework.core.ResolvableType; import org.springframework.http.MediaType; import org.springframework.reactive.codec.encoder.JsonObjectEncoder; + import reactor.Publishers; import reactor.fn.Function; -import reactor.rx.Promise; -import rx.Observable; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; /** - * Decode an arbitrary split byte stream representing JSON objects to a bye stream + * Decode an arbitrary split byte stream representing JSON objects to a byte stream * where each chunk is a well-formed JSON object. * - * If {@code Hints.STREAM_ARRAY_ELEMENTS} is enabled, each element of top level JSON array - * will be streamed as an individual JSON object. - * * This class does not do any real parsing or validation. A sequence of bytes is considered a JSON object/array * if it contains a matching number of opening and closing braces/brackets. * @@ -90,8 +86,7 @@ public class JsonObjectDecoder implements ByteToMessageDecoder { @Override public boolean canDecode(ResolvableType type, MediaType mediaType, Object... hints) { - return mediaType.isCompatibleWith(MediaType.APPLICATION_JSON) && !Promise.class.isAssignableFrom(type.getRawClass()) && - (Observable.class.isAssignableFrom(type.getRawClass()) || Publisher.class.isAssignableFrom(type.getRawClass())); + return mediaType.isCompatibleWith(MediaType.APPLICATION_JSON); } @Override diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/StringDecoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/StringDecoder.java index b64bd81edb..91742a84b7 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/StringDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/StringDecoder.java @@ -41,7 +41,8 @@ public class StringDecoder implements ByteToMessageDecoder { @Override public boolean canDecode(ResolvableType type, MediaType mediaType, Object... hints) { - return mediaType.isCompatibleWith(MediaType.TEXT_PLAIN); + return mediaType.isCompatibleWith(MediaType.TEXT_PLAIN) + && String.class.isAssignableFrom(type.getRawClass()); } @Override diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/ByteBufferEncoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/ByteBufferEncoder.java new file mode 100644 index 0000000000..9b829f3d09 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/ByteBufferEncoder.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2015 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.reactive.codec.encoder; + +import java.nio.ByteBuffer; + +import org.reactivestreams.Publisher; + +import org.springframework.core.ResolvableType; +import org.springframework.http.MediaType; + +/** + * @author Sebastien Deleuze + */ +public class ByteBufferEncoder implements MessageToByteEncoder { + + @Override + public boolean canEncode(ResolvableType type, MediaType mediaType, Object... hints) { + return ByteBuffer.class.isAssignableFrom(type.getRawClass()); + } + + @Override + public Publisher encode(Publisher messageStream, ResolvableType type, MediaType mediaType, Object... hints) { + return (Publisher)messageStream; + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JsonObjectEncoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JsonObjectEncoder.java index dd324a3c6f..da7856a571 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JsonObjectEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JsonObjectEncoder.java @@ -21,21 +21,17 @@ import org.reactivestreams.Subscriber; import org.springframework.core.ResolvableType; import org.springframework.http.MediaType; import org.springframework.reactive.codec.decoder.JsonObjectDecoder; -import org.springframework.util.ClassUtils; import reactor.core.subscriber.SubscriberBarrier; -import reactor.io.buffer.Buffer; -import reactor.rx.Promise; -import rx.Observable; import java.nio.ByteBuffer; -import java.util.Arrays; import static reactor.Publishers.*; +import reactor.io.buffer.Buffer; /** - * Encode a bye stream of individual JSON element to a byte stream representing a single - * JSON array when {@code Hints.ENCODE_AS_ARRAY} is enabled. + * Encode a byte stream of individual JSON element to a byte stream representing a single + * JSON array when if it contains more than one element. * * @author Sebastien Deleuze * @author Stephane Maldini @@ -44,57 +40,31 @@ import static reactor.Publishers.*; */ public class JsonObjectEncoder implements MessageToByteEncoder { - private static final boolean rxJava1Present = - ClassUtils.isPresent("rx.Observable", JsonObjectEncoder.class.getClassLoader()); - - private static final boolean reactorPresent = - ClassUtils.isPresent("reactor.rx.Promise", JsonObjectEncoder.class.getClassLoader()); - - final ByteBuffer START_ARRAY = ByteBuffer.wrap("[".getBytes()); - - final ByteBuffer END_ARRAY = ByteBuffer.wrap("]".getBytes()); - - final ByteBuffer COMMA = ByteBuffer.wrap(",".getBytes()); - @Override public boolean canEncode(ResolvableType type, MediaType mediaType, Object... hints) { - return mediaType.isCompatibleWith(MediaType.APPLICATION_JSON) && - !(reactorPresent && Promise.class.isAssignableFrom(type.getRawClass())) && - (rxJava1Present && Observable.class.isAssignableFrom(type.getRawClass()) - || Publisher.class.isAssignableFrom(type.getRawClass())); + return mediaType.isCompatibleWith(MediaType.APPLICATION_JSON); } @Override - public Publisher encode(Publisher messageStream, ResolvableType type, MediaType - mediaType, Object... hints) { - //TODO Merge some chunks, there is no need to have chunks with only '[', ']' or ',' characters - return - concat( - from( - Arrays.>asList( - just(START_ARRAY), - lift( - flatMap(messageStream, (ByteBuffer b) -> from(Arrays.asList(b, COMMA))), - sub -> new SkipLastBarrier(sub) - ), - just(END_ARRAY) - ) - ) - ); + public Publisher encode(Publisher messageStream, + ResolvableType type, MediaType mediaType, Object... hints) { + return lift(messageStream, sub -> new JsonEncoderBarrier(sub)); } - private static class SkipLastBarrier extends SubscriberBarrier { + private static class JsonEncoderBarrier extends SubscriberBarrier { - public SkipLastBarrier(Subscriber subscriber) { + public JsonEncoderBarrier(Subscriber subscriber) { super(subscriber); } ByteBuffer prev = null; + long count = 0; @Override protected void doNext(ByteBuffer next) { - if (prev == null) { + count++; + if (count == 1) { prev = next; doRequest(1); return; @@ -102,8 +72,27 @@ public class JsonObjectEncoder implements MessageToByteEncoder { ByteBuffer tmp = prev; prev = next; - subscriber.onNext(tmp); + Buffer buffer = new Buffer(); + if (count == 2) { + buffer.append("["); + } + buffer.append(tmp); + buffer.append(","); + buffer.flip(); + subscriber.onNext(buffer.byteBuffer()); } + @Override + protected void doComplete() { + Buffer buffer = new Buffer(); + buffer.append(prev); + if (count > 1) { + buffer.append("]"); + } + buffer.flip(); + subscriber.onNext(buffer.byteBuffer()); + subscriber.onComplete(); + } } + } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/MessageToByteEncoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/MessageToByteEncoder.java index 9a1b67162f..0dea2db8bb 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/MessageToByteEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/MessageToByteEncoder.java @@ -34,8 +34,7 @@ public interface MessageToByteEncoder { /** * Indicate whether the given type and media type can be processed by this encoder. - * @param type the (potentially generic) type to ultimately encode from. - * Could be different from {@code T} type. + * @param type the stream element type to encode. * @param mediaType the media type to encode. * Typically the value of an {@code Accept} header for HTTP request. * @param hints Additional information about how to encode, optional. @@ -46,8 +45,7 @@ public interface MessageToByteEncoder { /** * Encode a given message stream to the given output byte stream. * @param messageStream the message stream to encode. - * @param type the (potentially generic) type to ultimately encode from. - * Could be different from {@code T} type. + * @param type the stream element type to encode. * @param mediaType the media type to encode. * Typically the value of an {@code Accept} header for HTTP request. * @param hints Additional information about how to encode, optional. diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/StringEncoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/StringEncoder.java index 487007f7d3..ac267de1c1 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/StringEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/StringEncoder.java @@ -40,7 +40,8 @@ public class StringEncoder implements MessageToByteEncoder { @Override public boolean canEncode(ResolvableType type, MediaType mediaType, Object... hints) { - return mediaType.isCompatibleWith(MediaType.TEXT_PLAIN); + return mediaType.isCompatibleWith(MediaType.TEXT_PLAIN) + && String.class.isAssignableFrom(type.getRawClass()); } @Override diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/DispatcherHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/DispatcherHandler.java index 8d1fe02ab9..b50aebff18 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/DispatcherHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/DispatcherHandler.java @@ -101,6 +101,7 @@ public class DispatcherHandler implements HttpHandler, ApplicationContextAware { if (handler == null) { // No exception handling mechanism yet response.setStatusCode(HttpStatus.NOT_FOUND); + response.writeHeaders(); return Publishers.empty(); } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/SimpleHandlerResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/SimpleHandlerResultHandler.java index c72ad8f5dd..0e9556eb30 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/SimpleHandlerResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/SimpleHandlerResultHandler.java @@ -16,6 +16,8 @@ package org.springframework.reactive.web.dispatch; +import java.util.Arrays; + import org.reactivestreams.Publisher; import reactor.Publishers; @@ -45,6 +47,8 @@ public class SimpleHandlerResultHandler implements Ordered, HandlerResultHandler @Override public Publisher handleResult(ServerHttpRequest request, ServerHttpResponse response, HandlerResult result) { - return Publishers.completable((Publisher)result.getValue()); + Publisher handleComplete = Publishers.completable((Publisher)result.getValue()); + return Publishers.concat(Publishers.from(Arrays.asList(handleComplete, response.writeHeaders()))); } + } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/DefaultConversionService.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/DefaultConversionService.java new file mode 100644 index 0000000000..327bd8d8f8 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/DefaultConversionService.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2011-2015 Pivotal Software Inc, All Rights Reserved. + * + * 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.reactive.web.dispatch.method.annotation; + +import reactor.core.publisher.convert.DependencyUtils; + +import org.springframework.core.convert.converter.ConverterRegistry; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter; +import org.springframework.core.convert.support.ReactiveStreamsToReactorConverter; +import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter; + +/** + * TODO temporary class designed to be replaced by org.springframework.core.convert.support.DefaultConversionService when it will contain Reactive Streams converter + * @author Sebastien Deleuze + */ +class DefaultConversionService extends GenericConversionService { + + public DefaultConversionService() { + addDefaultConverters(this); + } + + public static void addDefaultConverters(ConverterRegistry converterRegistry) { + converterRegistry.addConverter(new ReactiveStreamsToCompletableFutureConverter()); + if (DependencyUtils.hasReactorStream()) { + converterRegistry.addConverter(new ReactiveStreamsToReactorConverter()); + } + if (DependencyUtils.hasRxJava1()) { + converterRegistry.addConverter(new ReactiveStreamsToRxJava1Converter()); + } + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java index 8a77f0b298..daf5821168 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java @@ -16,27 +16,10 @@ package org.springframework.reactive.web.dispatch.method.annotation; -import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; - import org.reactivestreams.Publisher; -import reactor.Publishers; -import reactor.core.publisher.convert.CompletableFutureConverter; -import reactor.core.publisher.convert.RxJava1Converter; -import reactor.core.publisher.convert.RxJava1SingleConverter; -import reactor.rx.Promise; -import reactor.rx.Stream; -import reactor.rx.Streams; -import rx.Observable; -import rx.Single; - import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; +import org.springframework.core.convert.ConversionService; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.reactive.codec.decoder.ByteToMessageDecoder; @@ -44,8 +27,15 @@ import org.springframework.reactive.web.dispatch.method.HandlerMethodArgumentRes import org.springframework.reactive.web.http.ServerHttpRequest; import org.springframework.web.bind.annotation.RequestBody; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + /** * @author Sebastien Deleuze + * @author Stephane Maldini */ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolver { @@ -53,14 +43,19 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve private final List> deserializers; private final List> preProcessors; + private final ConversionService conversionService; - public RequestBodyArgumentResolver(List> deserializers) { - this(deserializers, Collections.EMPTY_LIST); + public RequestBodyArgumentResolver(List> deserializers, + ConversionService conversionService) { + this(deserializers, conversionService, Collections.EMPTY_LIST); } - public RequestBodyArgumentResolver(List> deserializers, List> preProcessors) { + public RequestBodyArgumentResolver(List> deserializers, + ConversionService conversionService, + List> preProcessors) { this.deserializers = deserializers; + this.conversionService = conversionService; this.preProcessors = preProcessors; } @@ -70,61 +65,31 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve } @Override + @SuppressWarnings("unchecked") public Object resolveArgument(MethodParameter parameter, ServerHttpRequest request) { MediaType mediaType = resolveMediaType(request); ResolvableType type = ResolvableType.forMethodParameter(parameter); List hints = new ArrayList<>(); hints.add(UTF_8); - - // TODO: Refactor type conversion - ResolvableType readType = type; - if (Observable.class.isAssignableFrom(type.getRawClass()) || - Single.class.isAssignableFrom(type.getRawClass()) || - Promise.class.isAssignableFrom(type.getRawClass()) || - Publisher.class.isAssignableFrom(type.getRawClass()) || - CompletableFuture.class.isAssignableFrom(type.getRawClass())) { - readType = type.getGeneric(0); - } - - ByteToMessageDecoder deserializer = resolveDeserializers(request, type, mediaType, hints.toArray()); + Publisher inputStream = request.getBody(); + Publisher elementStream = inputStream; + ResolvableType elementType = type.hasGenerics() ? type.getGeneric(0) : type; + ByteToMessageDecoder deserializer = resolveDeserializers(request, elementType, mediaType, hints.toArray()); if (deserializer != null) { - - Publisher inputStream = request.getBody(); - List> preProcessors = resolvePreProcessors(request, type, mediaType, hints.toArray()); + List> preProcessors = + resolvePreProcessors(request, elementType, mediaType,hints.toArray()); for (ByteToMessageDecoder preProcessor : preProcessors) { - inputStream = preProcessor.decode(inputStream, type, mediaType, hints.toArray()); - } - Publisher elementStream = deserializer.decode(inputStream, readType, mediaType, UTF_8); - - // TODO: Refactor type conversion - if (Stream.class.isAssignableFrom(type.getRawClass())) { - return Streams.wrap(elementStream); - } - else if (Promise.class.isAssignableFrom(type.getRawClass())) { - return Streams.wrap(elementStream).take(1).next(); - } - else if (Observable.class.isAssignableFrom(type.getRawClass())) { - return RxJava1Converter.from(elementStream); - } - else if (Single.class.isAssignableFrom(type.getRawClass())) { - return RxJava1SingleConverter.from(elementStream); - } - else if (CompletableFuture.class.isAssignableFrom(type.getRawClass())) { - return CompletableFutureConverter.fromSingle(elementStream); - } - else if (Publisher.class.isAssignableFrom(type.getRawClass())) { - return elementStream; - } - else { - try { - return Publishers.toReadQueue(elementStream, 1, true).poll(30, TimeUnit.SECONDS); - } catch(InterruptedException ex) { - return Publishers.error(new IllegalStateException("Timeout before getter the value")); - } + inputStream = preProcessor.decode(inputStream, elementType, mediaType, hints.toArray()); } + elementStream = deserializer.decode(inputStream, elementType, mediaType, hints.toArray()); + } + if (conversionService.canConvert(Publisher.class, type.getRawClass())) { + return conversionService.convert(elementStream, type.getRawClass()); + } + else { + return elementStream; } - return Publishers.error(new IllegalStateException("Argument type not supported: " + type)); } private MediaType resolveMediaType(ServerHttpRequest request) { diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java index e2d1747200..6afcb3d44d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java @@ -15,11 +15,14 @@ */ package org.springframework.reactive.web.dispatch.method.annotation; +import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import org.springframework.beans.factory.InitializingBean; +import org.springframework.reactive.codec.decoder.ByteBufferDecoder; +import org.springframework.reactive.codec.decoder.ByteToMessageDecoder; import org.springframework.reactive.codec.decoder.JacksonJsonDecoder; import org.springframework.reactive.codec.decoder.JsonObjectDecoder; import org.springframework.reactive.codec.decoder.StringDecoder; @@ -51,7 +54,11 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Initializin if (this.argumentResolvers == null) { this.argumentResolvers = new ArrayList<>(); this.argumentResolvers.add(new RequestParamArgumentResolver()); - this.argumentResolvers.add(new RequestBodyArgumentResolver(Arrays.asList(new StringDecoder(), new JacksonJsonDecoder()), Arrays.asList(new JsonObjectDecoder(true)))); + List> deserializers = Arrays.asList(new ByteBufferDecoder(), + new StringDecoder(), new JacksonJsonDecoder()); + List> preProcessors = Arrays.asList(new JsonObjectDecoder()); + this.argumentResolvers.add(new RequestBodyArgumentResolver(deserializers, + new DefaultConversionService(), preProcessors)); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java index eee28f723e..bfd39cdbff 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java @@ -18,9 +18,9 @@ package org.springframework.reactive.web.dispatch.method.annotation; import org.reactivestreams.Publisher; import org.springframework.core.MethodParameter; import org.springframework.core.Ordered; -import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.convert.ConversionService; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.reactive.codec.encoder.MessageToByteEncoder; @@ -31,26 +31,20 @@ import org.springframework.reactive.web.http.ServerHttpResponse; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.method.HandlerMethod; import reactor.Publishers; -import reactor.core.publisher.convert.CompletableFutureConverter; -import reactor.core.publisher.convert.RxJava1Converter; -import reactor.core.publisher.convert.RxJava1SingleConverter; -import reactor.rx.Promise; -import rx.Observable; -import rx.Single; -import java.lang.reflect.Type; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.concurrent.CompletableFuture; /** * First version using {@link MessageToByteEncoder}s * * @author Rossen Stoyanchev + * @author Stephane Maldini + * @author Sebastien Deleuze */ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered { @@ -59,6 +53,7 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered private final List> serializers; private final List> postProcessors; + private final ConversionService conversionService; private int order = 0; @@ -68,8 +63,14 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered } public ResponseBodyResultHandler(List> serializers, List> postProcessors) { + this(serializers, postProcessors, new DefaultConversionService()); + } + + public ResponseBodyResultHandler(List> serializers, List> + postProcessors, ConversionService conversionService) { this.serializers = serializers; this.postProcessors = postProcessors; + this.conversionService = conversionService; } public void setOrder(int order) { @@ -87,14 +88,13 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered Object handler = result.getHandler(); if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod = (HandlerMethod) handler; - Type publisherVoidType = new ParameterizedTypeReference>(){}.getType(); - return AnnotatedElementUtils.isAnnotated(handlerMethod.getMethod(), ResponseBody.class.getName()) && - !handlerMethod.getReturnType().getGenericParameterType().equals(publisherVoidType); + return AnnotatedElementUtils.isAnnotated(handlerMethod.getMethod(), ResponseBody.class.getName()); } return false; } @Override + @SuppressWarnings("unchecked") public Publisher handleResult(ServerHttpRequest request, ServerHttpResponse response, HandlerResult result) { @@ -106,38 +106,27 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered return Publishers.empty(); } - MediaType mediaType = resolveMediaType(request); ResolvableType type = ResolvableType.forMethodParameter(returnType); + MediaType mediaType = resolveMediaType(request); List hints = new ArrayList<>(); hints.add(UTF_8); - MessageToByteEncoder serializer = (MessageToByteEncoder)resolveSerializer(request, type, mediaType, hints.toArray()); + Publisher elementStream; + ResolvableType elementType; + if (conversionService.canConvert(type.getRawClass(), Publisher.class)) { + elementStream = conversionService.convert(value, Publisher.class); + elementType = type.getGeneric(0); + } + else { + elementStream = Publishers.just(value); + elementType = type; + } + + MessageToByteEncoder serializer = (MessageToByteEncoder) resolveSerializer(request, elementType, mediaType, hints.toArray()); if (serializer != null) { - Publisher elementStream; - - // TODO: Refactor type conversion - if (Promise.class.isAssignableFrom(type.getRawClass())) { - elementStream = ((Promise)value).stream(); - } - else if (Observable.class.isAssignableFrom(type.getRawClass())) { - elementStream = RxJava1Converter.from((Observable) value); - } - else if (Single.class.isAssignableFrom(type.getRawClass())) { - elementStream = RxJava1SingleConverter.from((Single)value); - } - else if (CompletableFuture.class.isAssignableFrom(type.getRawClass())) { - elementStream = CompletableFutureConverter.from((CompletableFuture) value); - } - else if (Publisher.class.isAssignableFrom(type.getRawClass())) { - elementStream = (Publisher)value; - } - else { - elementStream = Publishers.just(value); - } - Publisher outputStream = serializer.encode(elementStream, type, mediaType, hints.toArray()); - List> postProcessors = resolvePostProcessors(request, type, mediaType, hints.toArray()); + List> postProcessors = resolvePostProcessors(request, elementType, mediaType, hints.toArray()); for (MessageToByteEncoder postProcessor : postProcessors) { - outputStream = postProcessor.encode(outputStream, type, mediaType, hints.toArray()); + outputStream = postProcessor.encode(outputStream, elementType, mediaType, hints.toArray()); } response.getHeaders().setContentType(mediaType); return response.writeWith(outputStream); diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/ServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/ServerHttpResponse.java index ba7cc6f5db..1f64f2dd6e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/ServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/ServerHttpResponse.java @@ -30,9 +30,18 @@ public interface ServerHttpResponse extends HttpMessage { void setStatusCode(HttpStatus status); + /** + * Write the response headers. This method must be invoked to send responses without body. + * @return A {@code Publisher} used to signal the demand, and receive a notification + * when the handling is complete (success or error) including the flush of the data on the + * network. + */ + Publisher writeHeaders(); + /** * Write the provided reactive stream of bytes to the response body. Most servers - * support multiple {@code writeWith} calls. + * support multiple {@code writeWith} calls. Headers are written automatically + * before the body, so not need to call {@link #writeHeaders()} explicitly. * @param contentPublisher the stream to write in the response body. * @return A {@code Publisher} used to signal the demand, and receive a notification * when the handling is complete (success or error) including the flush of the data on the diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpRequest.java index 6a9d0ed44d..5f727d9cd9 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpRequest.java @@ -15,13 +15,13 @@ */ package org.springframework.reactive.web.http.reactor; -import org.reactivestreams.Publisher; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.reactive.web.http.ServerHttpRequest; import org.springframework.util.Assert; import reactor.io.buffer.Buffer; import reactor.io.net.http.HttpChannel; +import reactor.rx.Stream; import java.net.URI; import java.net.URISyntaxException; @@ -72,7 +72,7 @@ public class ReactorServerHttpRequest implements ServerHttpRequest { } @Override - public Publisher getBody() { + public Stream getBody() { return this.channel.map(Buffer::byteBuffer); } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpResponse.java index 66761cabd5..7bd45b2786 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpResponse.java @@ -24,6 +24,7 @@ import reactor.Publishers; import reactor.io.buffer.Buffer; import reactor.io.net.http.HttpChannel; import reactor.io.net.http.model.Status; +import reactor.rx.Stream; import java.nio.ByteBuffer; @@ -57,18 +58,28 @@ public class ReactorServerHttpResponse implements ServerHttpResponse { } @Override - public Publisher writeWith(Publisher contentPublisher) { - writeHeaders(); + public Publisher writeHeaders() { + if (this.headersWritten) { + return Publishers.empty(); + } + applyHeaders(); + return this.channel.writeHeaders(); + } + + @Override + public Stream writeWith(Publisher contentPublisher) { + applyHeaders(); return this.channel.writeWith(Publishers.map(contentPublisher, Buffer::new)); } - private void writeHeaders() { + private void applyHeaders() { if (!this.headersWritten) { for (String name : this.headers.keySet()) { for (String value : this.headers.get(name)) { this.channel.responseHeaders().add(name, value); } } + this.headersWritten = true; } } } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpResponse.java index 161589cdcd..1143663e66 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpResponse.java @@ -23,6 +23,7 @@ import org.springframework.http.HttpStatus; import org.springframework.reactive.web.http.ServerHttpResponse; import org.springframework.util.Assert; +import reactor.Publishers; import reactor.core.publisher.convert.RxJava1Converter; import reactor.io.buffer.Buffer; import rx.Observable; @@ -59,24 +60,30 @@ public class RxNettyServerHttpResponse implements ServerHttpResponse { return (this.headersWritten ? HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers); } - public Observable writeWith(Observable contentPublisher) { - return this.response.writeBytes(contentPublisher.map(content -> new Buffer(content).asBytes())); + @Override + public Publisher writeHeaders() { + if (this.headersWritten) { + return Publishers.empty(); + } + applyHeaders(); + return RxJava1Converter.from(this.response.sendHeaders()); } @Override public Publisher writeWith(Publisher contentPublisher) { - writeHeaders(); + applyHeaders(); Observable contentObservable = RxJava1Converter.from(contentPublisher).map(content -> new Buffer(content).asBytes()); return RxJava1Converter.from(this.response.writeBytes(contentObservable)); } - private void writeHeaders() { + private void applyHeaders() { if (!this.headersWritten) { for (String name : this.headers.keySet()) { for (String value : this.headers.get(name)) { this.response.addHeader(name, value); } } + this.headersWritten = true; } } } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpResponse.java index 37987c36ee..ff462ec158 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpResponse.java @@ -22,6 +22,7 @@ import java.util.Map; import javax.servlet.http.HttpServletResponse; import org.reactivestreams.Publisher; +import reactor.Publishers; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -61,13 +62,19 @@ public class ServletServerHttpResponse implements ServerHttpResponse { return (this.headersWritten ? HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers); } + @Override + public Publisher writeHeaders() { + applyHeaders(); + return Publishers.empty(); + } + @Override public Publisher writeWith(final Publisher contentPublisher) { - writeHeaders(); + applyHeaders(); return (s -> contentPublisher.subscribe(responseSubscriber)); } - private void writeHeaders() { + private void applyHeaders() { if (!this.headersWritten) { for (Map.Entry> entry : this.headers.entrySet()) { String headerName = entry.getKey(); diff --git a/spring-web-reactive/src/main/resources/log4j.properties b/spring-web-reactive/src/main/resources/log4j.properties index 8e3253db66..b5bf0d4dfb 100644 --- a/spring-web-reactive/src/main/resources/log4j.properties +++ b/spring-web-reactive/src/main/resources/log4j.properties @@ -2,6 +2,7 @@ log4j.rootCategory=WARN, stdout log4j.logger.org.springframework.reactive=DEBUG log4j.logger.org.springframework.web=DEBUG +log4j.logger.reactor=INFO log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/ByteBufferDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/ByteBufferDecoderTests.java new file mode 100644 index 0000000000..96fa509745 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/ByteBufferDecoderTests.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2015 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.reactive.codec.decoder; + +import java.nio.ByteBuffer; +import java.util.List; + +import static org.junit.Assert.*; +import org.junit.Test; +import org.reactivestreams.Publisher; +import reactor.io.buffer.Buffer; +import reactor.rx.Stream; +import reactor.rx.Streams; + +import org.springframework.core.ResolvableType; +import org.springframework.http.MediaType; + +/** + * @author Sebastien Deleuze + */ +public class ByteBufferDecoderTests { + + private final ByteBufferDecoder decoder = new ByteBufferDecoder(); + + @Test + public void canDecode() { + assertTrue(decoder.canDecode(ResolvableType.forClass(ByteBuffer.class), MediaType.TEXT_PLAIN)); + assertFalse(decoder.canDecode(ResolvableType.forClass(Integer.class), MediaType.TEXT_PLAIN)); + assertTrue(decoder.canDecode(ResolvableType.forClass(ByteBuffer.class), MediaType.APPLICATION_JSON)); + } + + @Test + public void decode() throws InterruptedException { + ByteBuffer fooBuffer = Buffer.wrap("foo").byteBuffer(); + ByteBuffer barBuffer = Buffer.wrap("bar").byteBuffer(); + Stream source = Streams.just(fooBuffer, barBuffer); + List results = Streams.wrap(decoder.decode(source, + ResolvableType.forClassWithGenerics(Publisher.class, ByteBuffer.class), null)).toList().await(); + assertEquals(2, results.size()); + assertEquals(fooBuffer, results.get(0)); + assertEquals(barBuffer, results.get(1)); + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/JsonObjectDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/JsonObjectDecoderTests.java index d683796e6d..52bc2253c6 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/JsonObjectDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/JsonObjectDecoderTests.java @@ -31,9 +31,35 @@ import reactor.rx.Streams; */ public class JsonObjectDecoderTests { + @Test + public void decodeSingleChunkToJsonObject() throws InterruptedException { + JsonObjectDecoder decoder = new JsonObjectDecoder(); + Stream source = Streams.just(Buffer.wrap("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}").byteBuffer()); + List results = Streams.wrap(decoder.decode(source, null, null)).map(chunk -> { + byte[] b = new byte[chunk.remaining()]; + chunk.get(b); + return new String(b, StandardCharsets.UTF_8); + }).toList().await(); + assertEquals(1, results.size()); + assertEquals("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}", results.get(0)); + } + + @Test + public void decodeMultipleChunksToJsonObject() throws InterruptedException { + JsonObjectDecoder decoder = new JsonObjectDecoder(); + Stream source = Streams.just(Buffer.wrap("{\"foo\": \"foofoo\"").byteBuffer(), Buffer.wrap(", \"bar\": \"barbar\"}").byteBuffer()); + List results = Streams.wrap(decoder.decode(source, null, null)).map(chunk -> { + byte[] b = new byte[chunk.remaining()]; + chunk.get(b); + return new String(b, StandardCharsets.UTF_8); + }).toList().await(); + assertEquals(1, results.size()); + assertEquals("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}", results.get(0)); + } + @Test public void decodeSingleChunkToArray() throws InterruptedException { - JsonObjectDecoder decoder = new JsonObjectDecoder(true); + JsonObjectDecoder decoder = new JsonObjectDecoder(); Stream source = Streams.just(Buffer.wrap("[{\"foo\": \"foofoo\", \"bar\": \"barbar\"},{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}]").byteBuffer()); List results = Streams.wrap(decoder.decode(source, null, null)).map(chunk -> { byte[] b = new byte[chunk.remaining()]; @@ -47,7 +73,7 @@ public class JsonObjectDecoderTests { @Test public void decodeMultipleChunksToArray() throws InterruptedException { - JsonObjectDecoder decoder = new JsonObjectDecoder(true); + JsonObjectDecoder decoder = new JsonObjectDecoder(); Stream source = Streams.just(Buffer.wrap("[{\"foo\": \"foofoo\", \"bar\"").byteBuffer(), Buffer.wrap(": \"barbar\"},{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}]").byteBuffer()); List results = Streams.wrap(decoder.decode(source, null, null)).map(chunk -> { byte[] b = new byte[chunk.remaining()]; diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/StringDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/StringDecoderTests.java index ce707c40c9..cc9dc6c14c 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/StringDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/StringDecoderTests.java @@ -23,13 +23,13 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import org.junit.Test; +import org.reactivestreams.Publisher; import reactor.io.buffer.Buffer; import reactor.rx.Stream; import reactor.rx.Streams; import org.springframework.core.ResolvableType; import org.springframework.http.MediaType; -import org.springframework.reactive.codec.Pojo; /** * @author Sebastien Deleuze @@ -40,15 +40,16 @@ public class StringDecoderTests { @Test public void canDecode() { - assertTrue(decoder.canDecode(null, MediaType.TEXT_PLAIN)); - assertFalse(decoder.canDecode(null, MediaType.APPLICATION_JSON)); + assertTrue(decoder.canDecode(ResolvableType.forClass(String.class), MediaType.TEXT_PLAIN)); + assertFalse(decoder.canDecode(ResolvableType.forClass(Integer.class), MediaType.TEXT_PLAIN)); + assertFalse(decoder.canDecode(ResolvableType.forClass(String.class), MediaType.APPLICATION_JSON)); } @Test public void decode() throws InterruptedException { Stream source = Streams.just(Buffer.wrap("foo").byteBuffer(), Buffer.wrap("bar").byteBuffer()); - List results = Streams.wrap(decoder.decode(source, ResolvableType.forClass(Pojo.class), null)) - .toList().await(); + List results = Streams.wrap(decoder.decode(source, + ResolvableType.forClassWithGenerics(Publisher.class, String.class), null)).toList().await(); assertEquals(2, results.size()); assertEquals("foo", results.get(0)); assertEquals("bar", results.get(1)); diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/ByteBufferDecoderEncoder.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/ByteBufferDecoderEncoder.java new file mode 100644 index 0000000000..3fac6e58a9 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/ByteBufferDecoderEncoder.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2015 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.reactive.codec.encoder; + +import java.nio.ByteBuffer; +import java.util.List; + +import static org.junit.Assert.*; +import org.junit.Test; +import org.reactivestreams.Publisher; +import reactor.io.buffer.Buffer; +import reactor.rx.Stream; +import reactor.rx.Streams; + +import org.springframework.core.ResolvableType; +import org.springframework.http.MediaType; + +/** + * @author Sebastien Deleuze + */ +public class ByteBufferDecoderEncoder { + + private final ByteBufferEncoder encoder = new ByteBufferEncoder(); + + @Test + public void canDecode() { + assertTrue(encoder.canEncode(ResolvableType.forClass(ByteBuffer.class), MediaType.TEXT_PLAIN)); + assertFalse(encoder.canEncode(ResolvableType.forClass(Integer.class), MediaType.TEXT_PLAIN)); + assertTrue(encoder.canEncode(ResolvableType.forClass(ByteBuffer.class), MediaType.APPLICATION_JSON)); + } + + @Test + public void decode() throws InterruptedException { + ByteBuffer fooBuffer = Buffer.wrap("foo").byteBuffer(); + ByteBuffer barBuffer = Buffer.wrap("bar").byteBuffer(); + Stream source = Streams.just(fooBuffer, barBuffer); + List results = Streams.wrap(encoder.encode(source, + ResolvableType.forClassWithGenerics(Publisher.class, ByteBuffer.class), null)).toList().await(); + assertEquals(2, results.size()); + assertEquals(fooBuffer, results.get(0)); + assertEquals(barBuffer, results.get(1)); + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/JsonObjectEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/JsonObjectEncoderTests.java index 47726014f0..9f4eed32b9 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/JsonObjectEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/JsonObjectEncoderTests.java @@ -32,9 +32,24 @@ import reactor.rx.Streams; public class JsonObjectEncoderTests { @Test - public void encodeToArray() throws InterruptedException { + public void encodeSingleElement() throws InterruptedException { JsonObjectEncoder encoder = new JsonObjectEncoder(); - Stream source = Streams.just(Buffer.wrap("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}").byteBuffer(), Buffer.wrap("{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}").byteBuffer()); + Stream source = Streams.just(Buffer.wrap("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}").byteBuffer()); + List results = Streams.wrap(encoder.encode(source, null, null)).map(chunk -> { + byte[] b = new byte[chunk.remaining()]; + chunk.get(b); + return new String(b, StandardCharsets.UTF_8); + }).toList().await(); + String result = String.join("", results); + assertEquals("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}", result); + } + + @Test + public void encodeTwoElements() throws InterruptedException { + JsonObjectEncoder encoder = new JsonObjectEncoder(); + Stream source = Streams.just( + Buffer.wrap("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}").byteBuffer(), + Buffer.wrap("{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}").byteBuffer()); List results = Streams.wrap(encoder.encode(source, null, null)).map(chunk -> { byte[] b = new byte[chunk.remaining()]; chunk.get(b); @@ -44,4 +59,21 @@ public class JsonObjectEncoderTests { assertEquals("[{\"foo\": \"foofoo\", \"bar\": \"barbar\"},{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}]", result); } + @Test + public void encodeThreeElements() throws InterruptedException { + JsonObjectEncoder encoder = new JsonObjectEncoder(); + Stream source = Streams.just( + Buffer.wrap("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}").byteBuffer(), + Buffer.wrap("{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}").byteBuffer(), + Buffer.wrap("{\"foo\": \"foofoofoofoo\", \"bar\": \"barbarbarbar\"}").byteBuffer() + ); + List results = Streams.wrap(encoder.encode(source, null, null)).map(chunk -> { + byte[] b = new byte[chunk.remaining()]; + chunk.get(b); + return new String(b, StandardCharsets.UTF_8); + }).toList().await(); + String result = String.join("", results); + assertEquals("[{\"foo\": \"foofoo\", \"bar\": \"barbar\"},{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"},{\"foo\": \"foofoofoofoo\", \"bar\": \"barbarbarbar\"}]", result); + } + } diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/StringEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/StringEncoderTests.java index ce67e5e29a..151c4f8562 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/StringEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/StringEncoderTests.java @@ -23,8 +23,10 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import org.junit.Test; +import org.reactivestreams.Publisher; import reactor.rx.Streams; +import org.springframework.core.ResolvableType; import org.springframework.http.MediaType; /** @@ -36,8 +38,9 @@ public class StringEncoderTests { @Test public void canWrite() { - assertTrue(encoder.canEncode(null, MediaType.TEXT_PLAIN)); - assertFalse(encoder.canEncode(null, MediaType.APPLICATION_JSON)); + assertTrue(encoder.canEncode(ResolvableType.forClass(String.class), MediaType.TEXT_PLAIN)); + assertFalse(encoder.canEncode(ResolvableType.forClass(Integer.class), MediaType.TEXT_PLAIN)); + assertFalse(encoder.canEncode(ResolvableType.forClass(String.class), MediaType.APPLICATION_JSON)); } @Test diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingIntegrationTests.java index 8bf6234fbf..6aa048e259 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingIntegrationTests.java @@ -17,13 +17,20 @@ package org.springframework.reactive.web.dispatch.method.annotation; import java.net.URI; +import java.nio.ByteBuffer; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.CompletableFuture; import static org.junit.Assert.assertEquals; + import org.junit.Test; import org.reactivestreams.Publisher; + +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.core.ResolvableType; +import reactor.io.buffer.Buffer; import reactor.rx.Promise; import reactor.rx.Promises; import reactor.rx.Stream; @@ -32,13 +39,16 @@ import rx.Observable; import rx.Single; import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; +import org.springframework.reactive.codec.encoder.ByteBufferEncoder; import org.springframework.reactive.codec.encoder.JacksonJsonEncoder; import org.springframework.reactive.codec.encoder.JsonObjectEncoder; import org.springframework.reactive.codec.encoder.StringEncoder; import org.springframework.reactive.web.dispatch.DispatcherHandler; +import org.springframework.reactive.web.dispatch.SimpleHandlerResultHandler; import org.springframework.reactive.web.http.AbstractHttpHandlerIntegrationTests; import org.springframework.reactive.web.http.HttpHandler; import org.springframework.stereotype.Controller; @@ -52,18 +62,25 @@ import org.springframework.web.context.support.StaticWebApplicationContext; /** * @author Rossen Stoyanchev * @author Sebastien Deleuze + * @author Stephane Maldini */ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrationTests { + private TestController controller; + @Override protected HttpHandler createHttpHandler() { StaticWebApplicationContext wac = new StaticWebApplicationContext(); + DefaultListableBeanFactory factory = wac.getDefaultListableBeanFactory(); wac.registerSingleton("handlerMapping", RequestMappingHandlerMapping.class); wac.registerSingleton("handlerAdapter", RequestMappingHandlerAdapter.class); - wac.getDefaultListableBeanFactory().registerSingleton("responseBodyResultHandler", - new ResponseBodyResultHandler(Arrays.asList(new StringEncoder(), new JacksonJsonEncoder()), Arrays.asList(new JsonObjectEncoder()))); - wac.registerSingleton("controller", TestController.class); + factory.registerSingleton("responseBodyResultHandler", + new ResponseBodyResultHandler(Arrays.asList(new ByteBufferEncoder(), new StringEncoder(), new JacksonJsonEncoder()), Arrays.asList + (new JsonObjectEncoder()))); + wac.registerSingleton("simpleResultHandler", SimpleHandlerResultHandler.class); + this.controller = new TestController(); + factory.registerSingleton("controller", this.controller); wac.refresh(); DispatcherHandler dispatcherHandler = new DispatcherHandler(); @@ -83,6 +100,30 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati assertEquals("Hello George!", response.getBody()); } + @Test + public void rawPojoResponse() throws Exception { + + RestTemplate restTemplate = new RestTemplate(); + + URI url = new URI("http://localhost:" + port + "/raw"); + RequestEntity request = RequestEntity.get(url).build(); + Person person = restTemplate.exchange(request, Person.class).getBody(); + + assertEquals(new Person("Robert"), person); + } + + @Test + public void rawHelloResponse() throws Exception { + + RestTemplate restTemplate = new RestTemplate(); + + URI url = new URI("http://localhost:" + port + "/raw-observable"); + RequestEntity request = RequestEntity.get(url).build(); + ResponseEntity response = restTemplate.exchange(request, String.class); + + assertEquals("Hello!", response.getBody()); + } + @Test public void serializeAsPojo() throws Exception { serializeAsPojo("http://localhost:" + port + "/person"); @@ -153,6 +194,19 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati capitalizePojo("http://localhost:" + port + "/promise-capitalize"); } + @Test + public void create() throws Exception { + RestTemplate restTemplate = new RestTemplate(); + + URI url = new URI("http://localhost:" + port + "/create"); + List persons = Arrays.asList(new Person("Robert"), new Person("Marie")); + RequestEntity> request = RequestEntity.post(url).contentType(MediaType.APPLICATION_JSON).body(persons); + ResponseEntity response = restTemplate.exchange(request, Void.class); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals(2, this.controller.persons.size()); + } + public void serializeAsPojo(String requestUrl) throws Exception { RestTemplate restTemplate = new RestTemplate(); @@ -164,6 +218,17 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati assertEquals(new Person("Robert"), response.getBody()); } + public void postAsPojo(String requestUrl) throws Exception { + RestTemplate restTemplate = new RestTemplate(); + + URI url = new URI(requestUrl); + RequestEntity request = RequestEntity.post(url).accept(MediaType.APPLICATION_JSON).body(new Person + ("Robert")); + ResponseEntity response = restTemplate.exchange(request, Person.class); + + assertEquals(new Person("Robert"), response.getBody()); + } + public void serializeAsCollection(String requestUrl) throws Exception { RestTemplate restTemplate = new RestTemplate(); @@ -214,6 +279,8 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @SuppressWarnings("unused") private static class TestController { + final List persons = new ArrayList<>(); + @RequestMapping("/param") @ResponseBody public Publisher handleWithParam(@RequestParam String name) { @@ -232,6 +299,19 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati return CompletableFuture.completedFuture(new Person("Robert")); } + @RequestMapping("/raw") + @ResponseBody + public Publisher rawResponseBody() { + JacksonJsonEncoder encoder = new JacksonJsonEncoder(); + return encoder.encode(Streams.just(new Person("Robert")), ResolvableType.forClass(Person.class), MediaType.APPLICATION_JSON); + } + + @RequestMapping("/raw-observable") + @ResponseBody + public Observable rawObservableResponseBody() { + return Observable.just(Buffer.wrap("Hello!").byteBuffer()); + } + @RequestMapping("/single") @ResponseBody public Single singleResponseBody() { @@ -322,6 +402,13 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati }); } + @RequestMapping("/create") + public Publisher create(@RequestBody Stream personStream) { + return personStream.toList().onSuccess(personList -> persons.addAll(personList)).after(); + } + + //TODO add mixed and T request mappings tests + } private static class Person { diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandlerTests.java index b4500ceed6..4cdd620432 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandlerTests.java @@ -44,7 +44,7 @@ public class ResponseBodyResultHandlerTests { assertTrue(resultHandler.supports(new HandlerResult(publisherStringMethod, null))); HandlerMethod publisherVoidMethod = new HandlerMethod(controller, TestController.class.getMethod("publisherVoid")); - assertFalse(resultHandler.supports(new HandlerResult(publisherVoidMethod, null))); + assertTrue(resultHandler.supports(new HandlerResult(publisherVoidMethod, null))); } From fd52ae999bd4c48cf1431531737aa3978af70728 Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Thu, 29 Oct 2015 00:40:02 +0000 Subject: [PATCH 066/498] Update to latest reactor-net Latest reactor-net doesn't depend on reactor-stream anymore (neither reactor-codec and reactor-bus, it only depends on reactor-core). --- spring-web-reactive/build.gradle | 1 + .../PublisherReactorServerHttpRequest.java | 81 ++++++++++++++++++ .../PublisherReactorServerHttpResponse.java | 85 +++++++++++++++++++ .../web/http/reactor/ReactorHttpServer.java | 10 +-- .../reactor/ReactorServerHttpRequest.java | 52 ++---------- .../reactor/ReactorServerHttpResponse.java | 47 ++-------- .../http/reactor/RequestHandlerAdapter.java | 24 ++++-- .../AbstractHttpHandlerIntegrationTests.java | 6 +- 8 files changed, 204 insertions(+), 102 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/PublisherReactorServerHttpRequest.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/PublisherReactorServerHttpResponse.java diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index 51509778de..1f416da579 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -41,6 +41,7 @@ dependencies { optional "io.reactivex:rxnetty:0.5.0-SNAPSHOT" + optional "io.projectreactor:reactor-stream:2.1.0.BUILD-SNAPSHOT" optional "io.projectreactor:reactor-net:2.1.0.BUILD-SNAPSHOT" optional 'org.apache.tomcat:tomcat-util:8.0.28' diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/PublisherReactorServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/PublisherReactorServerHttpRequest.java new file mode 100644 index 0000000000..10960ab2ae --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/PublisherReactorServerHttpRequest.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2011-2015 Pivotal Software Inc, All Rights Reserved. + * + * 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.reactive.web.http.reactor; + +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.ByteBuffer; + +import org.reactivestreams.Publisher; +import reactor.Publishers; +import reactor.io.buffer.Buffer; +import reactor.io.net.http.HttpChannel; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.reactive.web.http.ServerHttpRequest; +import org.springframework.util.Assert; + +/** + * @author Stephane Maldini + */ +public class PublisherReactorServerHttpRequest implements ServerHttpRequest { + + private final HttpChannel channel; + + private HttpHeaders headers; + + + public PublisherReactorServerHttpRequest(HttpChannel request) { + Assert.notNull("'request', request must not be null."); + this.channel = request; + } + + + @Override + public HttpHeaders getHeaders() { + if (this.headers == null) { + this.headers = new HttpHeaders(); + for (String name : this.channel.headers().names()) { + for (String value : this.channel.headers().getAll(name)) { + this.headers.add(name, value); + } + } + } + return this.headers; + } + + @Override + public HttpMethod getMethod() { + return HttpMethod.valueOf(this.channel.method().getName()); + } + + @Override + public URI getURI() { + try { + return new URI(this.channel.uri()); + } catch (URISyntaxException ex) { + throw new IllegalStateException("Could not get URI: " + ex.getMessage(), ex); + } + + } + + @Override + public Publisher getBody() { + return Publishers.map(channel.input(), Buffer::byteBuffer); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/PublisherReactorServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/PublisherReactorServerHttpResponse.java new file mode 100644 index 0000000000..4f52316968 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/PublisherReactorServerHttpResponse.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2011-2015 Pivotal Software Inc, All Rights Reserved. + * + * 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.reactive.web.http.reactor; + +import java.nio.ByteBuffer; + +import org.reactivestreams.Publisher; +import reactor.Publishers; +import reactor.io.buffer.Buffer; +import reactor.io.net.http.HttpChannel; +import reactor.io.net.http.model.Status; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.reactive.web.http.ServerHttpResponse; +import org.springframework.util.Assert; + +/** + * @author Stephane Maldini + */ +public class PublisherReactorServerHttpResponse implements ServerHttpResponse { + + private final HttpChannel channel; + + private final HttpHeaders headers; + + private boolean headersWritten = false; + + + public PublisherReactorServerHttpResponse(HttpChannel response) { + Assert.notNull("'response', response must not be null."); + this.channel = response; + this.headers = new HttpHeaders(); + } + + + @Override + public void setStatusCode(HttpStatus status) { + this.channel.responseStatus(Status.valueOf(status.value())); + } + + @Override + public HttpHeaders getHeaders() { + return (this.headersWritten ? HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers); + } + + @Override + public Publisher writeHeaders() { + if (this.headersWritten) { + return Publishers.empty(); + } + applyHeaders(); + return this.channel.writeHeaders(); + } + + @Override + public Publisher writeWith(Publisher contentPublisher) { + applyHeaders(); + return this.channel.writeWith(Publishers.map(contentPublisher, Buffer::new)); + } + + private void applyHeaders() { + if (!this.headersWritten) { + for (String name : this.headers.keySet()) { + for (String value : this.headers.get(name)) { + this.channel.responseHeaders().add(name, value); + } + } + this.headersWritten = true; + } + } +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorHttpServer.java index 76deb8ac29..9cbd3976e9 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorHttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorHttpServer.java @@ -16,9 +16,8 @@ package org.springframework.reactive.web.http.reactor; -import reactor.bus.selector.Selectors; import reactor.io.buffer.Buffer; -import reactor.io.net.NetStreams; +import reactor.io.net.ReactiveNet; import org.springframework.beans.factory.InitializingBean; import org.springframework.reactive.web.http.HttpServer; @@ -48,16 +47,15 @@ public class ReactorHttpServer extends HttpServerSupport Assert.notNull(getHttpHandler()); this.reactorHandler = new RequestHandlerAdapter(getHttpHandler()); - this.reactorServer = (getPort() != -1 ? NetStreams.httpServer(getPort()) : - NetStreams.httpServer()); + this.reactorServer = (getPort() != -1 ? ReactiveNet.httpServer(getPort()) : + ReactiveNet.httpServer()); } @Override public void start() { if (!this.running) { try { - this.reactorServer.route(Selectors.matchAll(), this.reactorHandler) - .start().await(); + this.reactorServer.startAndAwait(reactorHandler); this.running = true; } catch (InterruptedException ex) { diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpRequest.java index 5f727d9cd9..75b22152d7 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpRequest.java @@ -15,65 +15,25 @@ */ package org.springframework.reactive.web.http.reactor; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.reactive.web.http.ServerHttpRequest; -import org.springframework.util.Assert; +import java.nio.ByteBuffer; + import reactor.io.buffer.Buffer; import reactor.io.net.http.HttpChannel; import reactor.rx.Stream; - -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.ByteBuffer; +import reactor.rx.Streams; /** * @author Stephane Maldini */ -public class ReactorServerHttpRequest implements ServerHttpRequest { - - private final HttpChannel channel; - - private HttpHeaders headers; - +public class ReactorServerHttpRequest extends PublisherReactorServerHttpRequest { public ReactorServerHttpRequest(HttpChannel request) { - Assert.notNull("'request', request must not be null."); - this.channel = request; - } - - - @Override - public HttpHeaders getHeaders() { - if (this.headers == null) { - this.headers = new HttpHeaders(); - for (String name : this.channel.headers().names()) { - for (String value : this.channel.headers().getAll(name)) { - this.headers.add(name, value); - } - } - } - return this.headers; - } - - @Override - public HttpMethod getMethod() { - return HttpMethod.valueOf(this.channel.method().getName()); - } - - @Override - public URI getURI() { - try { - return new URI(this.channel.uri()); - } catch (URISyntaxException ex) { - throw new IllegalStateException("Could not get URI: " + ex.getMessage(), ex); - } - + super(request); } @Override public Stream getBody() { - return this.channel.map(Buffer::byteBuffer); + return Streams.wrap(super.getBody()); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpResponse.java index 7bd45b2786..eec16e8521 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpResponse.java @@ -25,61 +25,26 @@ import reactor.io.buffer.Buffer; import reactor.io.net.http.HttpChannel; import reactor.io.net.http.model.Status; import reactor.rx.Stream; +import reactor.rx.Streams; import java.nio.ByteBuffer; /** * @author Stephane Maldini */ -public class ReactorServerHttpResponse implements ServerHttpResponse { - - private final HttpChannel channel; - - private final HttpHeaders headers; - - private boolean headersWritten = false; - +public class ReactorServerHttpResponse extends PublisherReactorServerHttpResponse { public ReactorServerHttpResponse(HttpChannel response) { - Assert.notNull("'response', response must not be null."); - this.channel = response; - this.headers = new HttpHeaders(); - } - - - @Override - public void setStatusCode(HttpStatus status) { - this.channel.responseStatus(Status.valueOf(status.value())); + super(response); } @Override - public HttpHeaders getHeaders() { - return (this.headersWritten ? HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers); - } - - @Override - public Publisher writeHeaders() { - if (this.headersWritten) { - return Publishers.empty(); - } - applyHeaders(); - return this.channel.writeHeaders(); + public Stream writeHeaders() { + return Streams.wrap(super.writeHeaders()); } @Override public Stream writeWith(Publisher contentPublisher) { - applyHeaders(); - return this.channel.writeWith(Publishers.map(contentPublisher, Buffer::new)); - } - - private void applyHeaders() { - if (!this.headersWritten) { - for (String name : this.headers.keySet()) { - for (String value : this.headers.get(name)) { - this.channel.responseHeaders().add(name, value); - } - } - this.headersWritten = true; - } + return Streams.wrap(super.writeWith(contentPublisher)); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/RequestHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/RequestHandlerAdapter.java index c8173299c6..ecf4f560ba 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/RequestHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/RequestHandlerAdapter.java @@ -16,16 +16,19 @@ package org.springframework.reactive.web.http.reactor; import org.reactivestreams.Publisher; +import reactor.core.publisher.convert.DependencyUtils; +import reactor.io.buffer.Buffer; +import reactor.io.net.ReactiveChannelHandler; +import reactor.io.net.http.HttpChannel; + import org.springframework.reactive.web.http.HttpHandler; import org.springframework.util.Assert; -import reactor.io.buffer.Buffer; -import reactor.io.net.ReactorChannelHandler; -import reactor.io.net.http.HttpChannel; /** * @author Stephane Maldini */ -public class RequestHandlerAdapter implements ReactorChannelHandler> { +public class RequestHandlerAdapter + implements ReactiveChannelHandler> { private final HttpHandler httpHandler; @@ -37,8 +40,17 @@ public class RequestHandlerAdapter implements ReactorChannelHandler apply(HttpChannel channel) { - ReactorServerHttpRequest adaptedRequest = new ReactorServerHttpRequest(channel); - ReactorServerHttpResponse adaptedResponse = new ReactorServerHttpResponse(channel); + final PublisherReactorServerHttpRequest adaptedRequest; + final PublisherReactorServerHttpResponse adaptedResponse; + + if(DependencyUtils.hasReactorStream()){ + adaptedRequest = new ReactorServerHttpRequest(channel); + adaptedResponse = new ReactorServerHttpResponse(channel); + } + else{ + adaptedRequest = new PublisherReactorServerHttpRequest(channel); + adaptedResponse = new PublisherReactorServerHttpResponse(channel); + } return this.httpHandler.handle(adaptedRequest, adaptedResponse); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/AbstractHttpHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/AbstractHttpHandlerIntegrationTests.java index 56d36384e8..604f8570e6 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/AbstractHttpHandlerIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/AbstractHttpHandlerIntegrationTests.java @@ -40,10 +40,10 @@ public abstract class AbstractHttpHandlerIntegrationTests { @Parameterized.Parameters(name = "server [{0}]") public static Object[][] arguments() { return new Object[][] { - {new JettyHttpServer()}, - {new TomcatHttpServer()}, + /*{new JettyHttpServer()}, {new RxNettyHttpServer()}, - {new ReactorHttpServer()} + {new ReactorHttpServer()},*/ + {new TomcatHttpServer()} }; } From 3864fc24ffa82df887730a9a24dee52a9053719b Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Thu, 29 Oct 2015 01:14:46 +0000 Subject: [PATCH 067/498] Fix JSON encoding issue --- .../codec/encoder/JsonObjectEncoder.java | 56 +++++++++++++++---- .../AbstractHttpHandlerIntegrationTests.java | 4 +- 2 files changed, 47 insertions(+), 13 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JsonObjectEncoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JsonObjectEncoder.java index da7856a571..d39e530f73 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JsonObjectEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JsonObjectEncoder.java @@ -25,8 +25,12 @@ import org.springframework.reactive.codec.decoder.JsonObjectDecoder; import reactor.core.subscriber.SubscriberBarrier; import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import java.util.concurrent.atomic.AtomicLongFieldUpdater; import static reactor.Publishers.*; + +import reactor.core.support.BackpressureUtils; import reactor.io.buffer.Buffer; /** @@ -49,24 +53,44 @@ public class JsonObjectEncoder implements MessageToByteEncoder { @Override public Publisher encode(Publisher messageStream, ResolvableType type, MediaType mediaType, Object... hints) { - return lift(messageStream, sub -> new JsonEncoderBarrier(sub)); + return lift(messageStream, bbs -> new JsonEncoderBarrier(bbs)); } private static class JsonEncoderBarrier extends SubscriberBarrier { + private volatile long requested; + @SuppressWarnings("rawtypes") + static final AtomicLongFieldUpdater REQUESTED = + AtomicLongFieldUpdater.newUpdater(JsonEncoderBarrier.class, "requested"); + + private volatile int terminated; + static final AtomicIntegerFieldUpdater TERMINATED = + AtomicIntegerFieldUpdater.newUpdater(JsonEncoderBarrier.class, "terminated"); + + ByteBuffer prev = null; + long count = 0; + public JsonEncoderBarrier(Subscriber subscriber) { super(subscriber); } - ByteBuffer prev = null; - long count = 0; + @Override + protected void doRequest(long n) { + BackpressureUtils.getAndAdd(REQUESTED, this, n); + if(TERMINATED.compareAndSet(this, 1, 2)){ + drainLast(); + } + else { + super.doRequest(n); + } + } @Override protected void doNext(ByteBuffer next) { count++; if (count == 1) { prev = next; - doRequest(1); + super.doRequest(1); return; } @@ -79,19 +103,29 @@ public class JsonObjectEncoder implements MessageToByteEncoder { buffer.append(tmp); buffer.append(","); buffer.flip(); + + BackpressureUtils.getAndSub(REQUESTED, this, 1L); subscriber.onNext(buffer.byteBuffer()); } + protected void drainLast(){ + if(BackpressureUtils.getAndSub(REQUESTED, this, 1L) > 0) { + Buffer buffer = new Buffer(); + buffer.append(prev); + if (count > 1) { + buffer.append("]"); + } + buffer.flip(); + subscriber.onNext(buffer.byteBuffer()); + super.doComplete(); + } + } + @Override protected void doComplete() { - Buffer buffer = new Buffer(); - buffer.append(prev); - if (count > 1) { - buffer.append("]"); + if(TERMINATED.compareAndSet(this, 0, 1)){ + drainLast(); } - buffer.flip(); - subscriber.onNext(buffer.byteBuffer()); - subscriber.onComplete(); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/AbstractHttpHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/AbstractHttpHandlerIntegrationTests.java index 604f8570e6..e03b75180d 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/AbstractHttpHandlerIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/AbstractHttpHandlerIntegrationTests.java @@ -40,9 +40,9 @@ public abstract class AbstractHttpHandlerIntegrationTests { @Parameterized.Parameters(name = "server [{0}]") public static Object[][] arguments() { return new Object[][] { - /*{new JettyHttpServer()}, + {new JettyHttpServer()}, {new RxNettyHttpServer()}, - {new ReactorHttpServer()},*/ + {new ReactorHttpServer()}, {new TomcatHttpServer()} }; } From bab3b6fd1c4946cdba59aca365aba1a7f4ca6033 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Tue, 13 Oct 2015 12:33:26 +0200 Subject: [PATCH 068/498] Introduce HttpMessage hierarchy This commit introduces "reactive" sub-interfaces of the HttpMessage interface found in the Spring Framework. --- .../ReactiveHttpInputMessage.java} | 15 +++-- .../http/ReactiveHttpOutputMessage.java | 41 +++++++++++++ .../client/ReactiveClientHttpRequest.java} | 16 +++-- .../client/ReactiveClientHttpResponse.java | 59 +++++++++++++++++++ .../server/ReactiveServerHttpRequest.java} | 14 +++-- .../server/ReactiveServerHttpResponse.java} | 32 ++++------ .../web/dispatch/DispatcherHandler.java | 20 ++++--- .../reactive/web/dispatch/HandlerAdapter.java | 7 ++- .../reactive/web/dispatch/HandlerMapping.java | 5 +- .../web/dispatch/HandlerResultHandler.java | 7 ++- .../dispatch/SimpleHandlerResultHandler.java | 7 +-- .../dispatch/handler/HttpHandlerAdapter.java | 8 +-- .../handler/SimpleUrlHandlerMapping.java | 6 +- .../method/HandlerMethodArgumentResolver.java | 6 +- .../method/InvocableHandlerMethod.java | 6 +- .../RequestBodyArgumentResolver.java | 42 ++++++++----- .../RequestMappingHandlerAdapter.java | 7 ++- .../RequestMappingHandlerMapping.java | 5 +- .../RequestParamArgumentResolver.java | 5 +- .../annotation/ResponseBodyResultHandler.java | 39 +++++++----- .../reactive/web/http/HttpHandler.java | 8 ++- .../reactor/ReactorServerHttpRequest.java | 16 ++++- .../reactor/ReactorServerHttpResponse.java | 4 +- .../rxnetty/RxNettyServerHttpRequest.java | 25 ++++---- .../rxnetty/RxNettyServerHttpResponse.java | 13 ++-- .../servlet/ServletServerHttpRequest.java | 6 +- .../servlet/ServletServerHttpResponse.java | 8 +-- ...mpleUrlHandlerMappingIntegrationTests.java | 13 ++-- .../RequestMappingHandlerMappingTests.java | 11 ++-- .../reactive/web/http/EchoHandler.java | 7 ++- .../reactive/web/http/RandomHandler.java | 7 ++- .../reactive/web/http/XmlHandler.java | 12 ++-- 32 files changed, 317 insertions(+), 160 deletions(-) rename spring-web-reactive/src/main/java/org/springframework/{reactive/web/http/ServerHttpRequest.java => http/ReactiveHttpInputMessage.java} (60%) create mode 100644 spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java rename spring-web-reactive/src/main/java/org/springframework/{reactive/web/http/HttpRequest.java => http/client/ReactiveClientHttpRequest.java} (66%) create mode 100644 spring-web-reactive/src/main/java/org/springframework/http/client/ReactiveClientHttpResponse.java rename spring-web-reactive/src/main/java/org/springframework/{reactive/web/http/HttpMessage.java => http/server/ReactiveServerHttpRequest.java} (66%) rename spring-web-reactive/src/main/java/org/springframework/{reactive/web/http/ServerHttpResponse.java => http/server/ReactiveServerHttpResponse.java} (58%) diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/ServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpInputMessage.java similarity index 60% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/http/ServerHttpRequest.java rename to spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpInputMessage.java index dca48dee1c..8cd4535b6b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/ServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpInputMessage.java @@ -13,21 +13,26 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.reactive.web.http; + +package org.springframework.http; import java.nio.ByteBuffer; import org.reactivestreams.Publisher; /** - * Represent a server-side HTTP request. + * Represents a "reactive" HTTP input message, consisting of {@linkplain #getHeaders() headers} + * and a readable {@linkplain #getBody() streaming body }. * - * @author Rossen Stoyanchev + *

    Typically implemented by an HTTP request on the server-side, or a response on the client-side. + * + * @author Arjen Poutsma */ -public interface ServerHttpRequest extends HttpRequest { +public interface ReactiveHttpInputMessage extends HttpMessage { /** - * Return the body of the message as a reactive stream. + * Return the body of the message as an publisher of {@code ByteBuffer}s. + * @return the body */ Publisher getBody(); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java b/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java new file mode 100644 index 0000000000..bbd5022a88 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2015 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; + +import java.nio.ByteBuffer; + +import org.reactivestreams.Publisher; + +/** + * Represents a "reactive" HTTP output message, consisting of {@linkplain #getHeaders() headers} + * and the capability to add a {@linkplain #addBody(Publisher) body}. + * + *

    Typically implemented by an HTTP request on the client-side, or a response on the server-side. + * + * @author Arjen Poutsma + */ +public interface ReactiveHttpOutputMessage extends HttpMessage { + + /** + * Adds the given publisher of {@link ByteBuffer}s as a body. A HTTP/1.1 message has + * one body, but HTTP/1.2 supports multiple bodies. + * @param body the body to add + * @return a publisher that indicates completion + */ + Publisher addBody(Publisher body); + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/HttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/client/ReactiveClientHttpRequest.java similarity index 66% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/http/HttpRequest.java rename to spring-web-reactive/src/main/java/org/springframework/http/client/ReactiveClientHttpRequest.java index 96767fe2fe..591a017727 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/HttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/ReactiveClientHttpRequest.java @@ -13,19 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.reactive.web.http; -import java.net.URI; +package org.springframework.http.client; -import org.springframework.http.HttpMethod; +import org.springframework.http.HttpRequest; +import org.springframework.http.ReactiveHttpOutputMessage; /** - * @author Rossen Stoyanchev + * Represents a "reactive" client-side HTTP request. + * + * @author Arjen Poutsma */ -public interface HttpRequest extends HttpMessage { - - HttpMethod getMethod(); - - URI getURI(); +public interface ReactiveClientHttpRequest extends HttpRequest, ReactiveHttpOutputMessage { } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/ReactiveClientHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/client/ReactiveClientHttpResponse.java new file mode 100644 index 0000000000..b5f619bdc9 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/ReactiveClientHttpResponse.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2015 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.client; + +import java.io.Closeable; +import java.io.IOException; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ReactiveHttpInputMessage; + +/** + * Represents a "reactive" client-side HTTP response. + * + * @author Arjen Poutsma + */ +public interface ReactiveClientHttpResponse extends ReactiveHttpInputMessage, Closeable { + + /** + * Return the HTTP status code of the response. + * @return the HTTP status as an HttpStatus enum value + * @throws IOException in case of I/O errors + */ + HttpStatus getStatusCode() throws IOException; + + /** + * Return the HTTP status code of the response as integer + * @return the HTTP status as an integer + * @throws IOException in case of I/O errors + */ + int getRawStatusCode() throws IOException; + + /** + * Return the HTTP status text of the response. + * @return the HTTP status text + * @throws IOException in case of I/O errors + */ + String getStatusText() throws IOException; + + /** + * Close this response, freeing any resources created. + */ + @Override + void close(); + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/HttpMessage.java b/spring-web-reactive/src/main/java/org/springframework/http/server/ReactiveServerHttpRequest.java similarity index 66% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/http/HttpMessage.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/ReactiveServerHttpRequest.java index a3c6092d31..4b5d0d1fa2 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/HttpMessage.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/ReactiveServerHttpRequest.java @@ -13,15 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.reactive.web.http; -import org.springframework.http.HttpHeaders; +package org.springframework.http.server; + +import org.springframework.http.HttpRequest; +import org.springframework.http.ReactiveHttpInputMessage; /** - * @author Rossen Stoyanchev + * Represents a "reactive" server-side HTTP request + * + * @author Arjen Poutsma */ -public interface HttpMessage { - - HttpHeaders getHeaders(); +public interface ReactiveServerHttpRequest extends HttpRequest, ReactiveHttpInputMessage { } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/ServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/ReactiveServerHttpResponse.java similarity index 58% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/http/ServerHttpResponse.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/ReactiveServerHttpResponse.java index 1f64f2dd6e..514592df51 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/ServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/ReactiveServerHttpResponse.java @@ -13,23 +13,27 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.reactive.web.http; -import java.nio.ByteBuffer; - -import org.reactivestreams.Publisher; +package org.springframework.http.server; import org.springframework.http.HttpStatus; +import org.reactivestreams.Publisher; +import org.springframework.http.ReactiveHttpOutputMessage; /** - * Represent a server-side HTTP response. + * Represents a "reactive" server-side HTTP response. * - * @author Rossen Stoyanchev + * @author Arjen Poutsma */ -public interface ServerHttpResponse extends HttpMessage { +public interface ReactiveServerHttpResponse + extends ReactiveHttpOutputMessage { + /** + * Set the HTTP status code of the response. + * @param status the HTTP status as an HttpStatus enum value + */ void setStatusCode(HttpStatus status); - + /** * Write the response headers. This method must be invoked to send responses without body. * @return A {@code Publisher} used to signal the demand, and receive a notification @@ -37,16 +41,4 @@ public interface ServerHttpResponse extends HttpMessage { * network. */ Publisher writeHeaders(); - - /** - * Write the provided reactive stream of bytes to the response body. Most servers - * support multiple {@code writeWith} calls. Headers are written automatically - * before the body, so not need to call {@link #writeHeaders()} explicitly. - * @param contentPublisher the stream to write in the response body. - * @return A {@code Publisher} used to signal the demand, and receive a notification - * when the handling is complete (success or error) including the flush of the data on the - * network. - */ - Publisher writeWith(Publisher contentPublisher); - } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/DispatcherHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/DispatcherHandler.java index b50aebff18..89c372946e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/DispatcherHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/DispatcherHandler.java @@ -13,25 +13,27 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.reactive.web.dispatch; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.reactivestreams.Publisher; +import reactor.Publishers; + import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.http.HttpStatus; +import org.springframework.http.server.ReactiveServerHttpRequest; +import org.springframework.http.server.ReactiveServerHttpResponse; import org.springframework.reactive.web.http.HttpHandler; -import org.springframework.reactive.web.http.ServerHttpRequest; -import org.springframework.reactive.web.http.ServerHttpResponse; -import reactor.Publishers; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; /** * Central dispatcher for HTTP request handlers/controllers. Dispatches to registered @@ -91,7 +93,7 @@ public class DispatcherHandler implements HttpHandler, ApplicationContextAware { @Override - public Publisher handle(ServerHttpRequest request, ServerHttpResponse response) { + public Publisher handle(ReactiveServerHttpRequest request, ReactiveServerHttpResponse response) { if (logger.isDebugEnabled()) { logger.debug("Processing " + request.getMethod() + " request for [" + request.getURI() + "]"); @@ -123,7 +125,7 @@ public class DispatcherHandler implements HttpHandler, ApplicationContextAware { } - protected Object getHandler(ServerHttpRequest request) { + protected Object getHandler(ReactiveServerHttpRequest request) { Object handler = null; for (HandlerMapping handlerMapping : this.handlerMappings) { handler = handlerMapping.getHandler(request); diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerAdapter.java index 01fc9a9f0e..91d9420412 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerAdapter.java @@ -13,10 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.reactive.web.dispatch; -import org.springframework.reactive.web.http.ServerHttpRequest; -import org.springframework.reactive.web.http.ServerHttpResponse; +import org.springframework.http.server.ReactiveServerHttpRequest; +import org.springframework.http.server.ReactiveServerHttpResponse; /** * Interface that must be implemented for each handler type to handle an HTTP request. @@ -52,6 +53,6 @@ public interface HandlerAdapter { * @throws Exception in case of errors * @return An {@link HandlerResult} instance */ - HandlerResult handle(ServerHttpRequest request, ServerHttpResponse response, Object handler) throws Exception; + HandlerResult handle(ReactiveServerHttpRequest request, ReactiveServerHttpResponse response, Object handler) throws Exception; } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerMapping.java index e3472eba53..2a087989a7 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerMapping.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerMapping.java @@ -13,15 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.reactive.web.dispatch; -import org.springframework.reactive.web.http.ServerHttpRequest; +import org.springframework.http.server.ReactiveServerHttpRequest; /** * @author Rossen Stoyanchev */ public interface HandlerMapping { - Object getHandler(ServerHttpRequest request); + Object getHandler(ReactiveServerHttpRequest request); } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerResultHandler.java index 7a4a9cb23b..aee3d6eab5 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerResultHandler.java @@ -13,12 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.reactive.web.dispatch; import org.reactivestreams.Publisher; -import org.springframework.reactive.web.http.ServerHttpRequest; -import org.springframework.reactive.web.http.ServerHttpResponse; +import org.springframework.http.server.ReactiveServerHttpRequest; +import org.springframework.http.server.ReactiveServerHttpResponse; /** * Process the {@link HandlerResult}, usually returned by an {@link HandlerAdapter}. @@ -47,6 +48,6 @@ public interface HandlerResultHandler { * when the handling is complete (success or error) including the flush of the data on the * network. */ - Publisher handleResult(ServerHttpRequest request, ServerHttpResponse response, HandlerResult result); + Publisher handleResult(ReactiveServerHttpRequest request, ReactiveServerHttpResponse response, HandlerResult result); } \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/SimpleHandlerResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/SimpleHandlerResultHandler.java index 0e9556eb30..0fe01b02be 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/SimpleHandlerResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/SimpleHandlerResultHandler.java @@ -22,8 +22,8 @@ import org.reactivestreams.Publisher; import reactor.Publishers; import org.springframework.core.Ordered; -import org.springframework.reactive.web.http.ServerHttpRequest; -import org.springframework.reactive.web.http.ServerHttpResponse; +import org.springframework.http.server.ReactiveServerHttpRequest; +import org.springframework.http.server.ReactiveServerHttpResponse; /** * Supports {@link HandlerResult} with a {@code Publisher} value. @@ -46,9 +46,8 @@ public class SimpleHandlerResultHandler implements Ordered, HandlerResultHandler } @Override - public Publisher handleResult(ServerHttpRequest request, ServerHttpResponse response, HandlerResult result) { + public Publisher handleResult(ReactiveServerHttpRequest request, ReactiveServerHttpResponse response, HandlerResult result) { Publisher handleComplete = Publishers.completable((Publisher)result.getValue()); return Publishers.concat(Publishers.from(Arrays.asList(handleComplete, response.writeHeaders()))); } - } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/handler/HttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/handler/HttpHandlerAdapter.java index 0dc2e9463d..67c563355a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/handler/HttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/handler/HttpHandlerAdapter.java @@ -13,16 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.reactive.web.dispatch.handler; import org.reactivestreams.Publisher; +import org.springframework.http.server.ReactiveServerHttpRequest; +import org.springframework.http.server.ReactiveServerHttpResponse; import org.springframework.reactive.web.dispatch.HandlerAdapter; import org.springframework.reactive.web.dispatch.HandlerResult; import org.springframework.reactive.web.http.HttpHandler; -import org.springframework.reactive.web.http.ServerHttpRequest; -import org.springframework.reactive.web.http.ServerHttpResponse; - /** * Support use of {@link HttpHandler} with @@ -44,7 +44,7 @@ public class HttpHandlerAdapter implements HandlerAdapter { } @Override - public HandlerResult handle(ServerHttpRequest request, ServerHttpResponse response, Object handler) { + public HandlerResult handle(ReactiveServerHttpRequest request, ReactiveServerHttpResponse response, Object handler) { HttpHandler httpHandler = (HttpHandler)handler; Publisher completion = httpHandler.handle(request, response); return new HandlerResult(httpHandler, completion); diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/handler/SimpleUrlHandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/handler/SimpleUrlHandlerMapping.java index 64bae7f0ae..c49f9fa0c5 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/handler/SimpleUrlHandlerMapping.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/handler/SimpleUrlHandlerMapping.java @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.reactive.web.dispatch.handler; import java.util.HashMap; import java.util.Map; +import org.springframework.http.server.ReactiveServerHttpRequest; import org.springframework.reactive.web.dispatch.HandlerMapping; -import org.springframework.reactive.web.http.ServerHttpRequest; - /** * @author Rossen Stoyanchev @@ -39,7 +39,7 @@ public class SimpleUrlHandlerMapping implements HandlerMapping { @Override - public Object getHandler(ServerHttpRequest request) { + public Object getHandler(ReactiveServerHttpRequest request) { return this.handlerMap.get(request.getURI().getPath()); } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/HandlerMethodArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/HandlerMethodArgumentResolver.java index e4d77d3dbc..f09fa42830 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/HandlerMethodArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/HandlerMethodArgumentResolver.java @@ -13,11 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.reactive.web.dispatch.method; - import org.springframework.core.MethodParameter; -import org.springframework.reactive.web.http.ServerHttpRequest; +import org.springframework.http.server.ReactiveServerHttpRequest; /** @@ -27,6 +27,6 @@ public interface HandlerMethodArgumentResolver { boolean supportsParameter(MethodParameter parameter); - Object resolveArgument(MethodParameter parameter, ServerHttpRequest request); + Object resolveArgument(MethodParameter parameter, ReactiveServerHttpRequest request); } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/InvocableHandlerMethod.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/InvocableHandlerMethod.java index ab49c6b6e1..7507677180 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/InvocableHandlerMethod.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/InvocableHandlerMethod.java @@ -26,7 +26,7 @@ import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.GenericTypeResolver; import org.springframework.core.MethodParameter; import org.springframework.core.ParameterNameDiscoverer; -import org.springframework.reactive.web.http.ServerHttpRequest; +import org.springframework.http.server.ReactiveServerHttpRequest; import org.springframework.util.ReflectionUtils; import org.springframework.web.method.HandlerMethod; @@ -55,7 +55,7 @@ public class InvocableHandlerMethod extends HandlerMethod { } - public Object invokeForRequest(ServerHttpRequest request, Object... providedArgs) throws Exception { + public Object invokeForRequest(ReactiveServerHttpRequest request, Object... providedArgs) throws Exception { Object[] args = getMethodArgumentValues(request, providedArgs); if (logger.isTraceEnabled()) { logger.trace("Invoking [" + getBeanType().getSimpleName() + "." + @@ -68,7 +68,7 @@ public class InvocableHandlerMethod extends HandlerMethod { return returnValue; } - private Object[] getMethodArgumentValues(ServerHttpRequest request, Object... providedArgs) throws Exception { + private Object[] getMethodArgumentValues(ReactiveServerHttpRequest request, Object... providedArgs) throws Exception { MethodParameter[] parameters = getMethodParameters(); Object[] args = new Object[parameters.length]; for (int i = 0; i < parameters.length; i++) { diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java index daf5821168..0dea573a22 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java @@ -16,22 +16,34 @@ package org.springframework.reactive.web.dispatch.method.annotation; -import org.reactivestreams.Publisher; -import org.springframework.core.MethodParameter; -import org.springframework.core.ResolvableType; -import org.springframework.core.convert.ConversionService; -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; -import org.springframework.reactive.codec.decoder.ByteToMessageDecoder; -import org.springframework.reactive.web.dispatch.method.HandlerMethodArgumentResolver; -import org.springframework.reactive.web.http.ServerHttpRequest; -import org.springframework.web.bind.annotation.RequestBody; - import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import org.reactivestreams.Publisher; +import reactor.Publishers; +import reactor.core.publisher.convert.CompletableFutureConverter; +import reactor.core.publisher.convert.RxJava1Converter; +import reactor.core.publisher.convert.RxJava1SingleConverter; +import reactor.rx.Promise; +import reactor.rx.Stream; +import reactor.rx.Streams; +import rx.Observable; +import rx.Single; + +import org.springframework.core.MethodParameter; +import org.springframework.core.ResolvableType; +import org.springframework.core.convert.ConversionService; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.server.ReactiveServerHttpRequest; +import org.springframework.reactive.codec.decoder.ByteToMessageDecoder; +import org.springframework.reactive.web.dispatch.method.HandlerMethodArgumentResolver; +import org.springframework.web.bind.annotation.RequestBody; /** * @author Sebastien Deleuze @@ -66,7 +78,7 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve @Override @SuppressWarnings("unchecked") - public Object resolveArgument(MethodParameter parameter, ServerHttpRequest request) { + public Object resolveArgument(MethodParameter parameter, ReactiveServerHttpRequest request) { MediaType mediaType = resolveMediaType(request); ResolvableType type = ResolvableType.forMethodParameter(parameter); @@ -92,14 +104,14 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve } } - private MediaType resolveMediaType(ServerHttpRequest request) { + private MediaType resolveMediaType(ReactiveServerHttpRequest request) { String acceptHeader = request.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE); List mediaTypes = MediaType.parseMediaTypes(acceptHeader); MediaType.sortBySpecificityAndQuality(mediaTypes); return ( mediaTypes.size() > 0 ? mediaTypes.get(0) : MediaType.TEXT_PLAIN); } - private ByteToMessageDecoder resolveDeserializers(ServerHttpRequest request, ResolvableType type, MediaType mediaType, Object[] hints) { + private ByteToMessageDecoder resolveDeserializers(ReactiveServerHttpRequest request, ResolvableType type, MediaType mediaType, Object[] hints) { for (ByteToMessageDecoder deserializer : this.deserializers) { if (deserializer.canDecode(type, mediaType, hints)) { return deserializer; @@ -108,7 +120,7 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve return null; } - private List> resolvePreProcessors(ServerHttpRequest request, ResolvableType type, MediaType mediaType, Object[] hints) { + private List> resolvePreProcessors(ReactiveServerHttpRequest request, ResolvableType type, MediaType mediaType, Object[] hints) { List> preProcessors = new ArrayList<>(); for (ByteToMessageDecoder preProcessor : this.preProcessors) { if (preProcessor.canDecode(type, mediaType, hints)) { diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java index 6afcb3d44d..a24859915b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.reactive.web.dispatch.method.annotation; import java.nio.ByteBuffer; @@ -23,6 +24,8 @@ import java.util.List; import org.springframework.beans.factory.InitializingBean; import org.springframework.reactive.codec.decoder.ByteBufferDecoder; import org.springframework.reactive.codec.decoder.ByteToMessageDecoder; +import org.springframework.http.server.ReactiveServerHttpRequest; +import org.springframework.http.server.ReactiveServerHttpResponse; import org.springframework.reactive.codec.decoder.JacksonJsonDecoder; import org.springframework.reactive.codec.decoder.JsonObjectDecoder; import org.springframework.reactive.codec.decoder.StringDecoder; @@ -30,8 +33,6 @@ import org.springframework.reactive.web.dispatch.HandlerAdapter; import org.springframework.reactive.web.dispatch.HandlerResult; import org.springframework.reactive.web.dispatch.method.HandlerMethodArgumentResolver; import org.springframework.reactive.web.dispatch.method.InvocableHandlerMethod; -import org.springframework.reactive.web.http.ServerHttpRequest; -import org.springframework.reactive.web.http.ServerHttpResponse; import org.springframework.web.method.HandlerMethod; @@ -68,7 +69,7 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Initializin } @Override - public HandlerResult handle(ServerHttpRequest request, ServerHttpResponse response, + public HandlerResult handle(ReactiveServerHttpRequest request, ReactiveServerHttpResponse response, Object handler) throws Exception { final InvocableHandlerMethod invocable = new InvocableHandlerMethod((HandlerMethod) handler); diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerMapping.java index 17df6ff819..3dd42b3831 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerMapping.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerMapping.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.reactive.web.dispatch.method.annotation; import java.util.Arrays; @@ -32,8 +33,8 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.http.HttpMethod; +import org.springframework.http.server.ReactiveServerHttpRequest; import org.springframework.reactive.web.dispatch.HandlerMapping; -import org.springframework.reactive.web.http.ServerHttpRequest; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @@ -92,7 +93,7 @@ public class RequestMappingHandlerMapping implements HandlerMapping, } @Override - public Object getHandler(ServerHttpRequest request) { + public Object getHandler(ReactiveServerHttpRequest request) { String path = request.getURI().getPath(); HttpMethod method = request.getMethod(); for (Map.Entry entry : this.methodMap.entrySet()) { diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestParamArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestParamArgumentResolver.java index e3aaed2238..ab7fa92d85 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestParamArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestParamArgumentResolver.java @@ -13,12 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.reactive.web.dispatch.method.annotation; import org.springframework.core.MethodParameter; +import org.springframework.http.server.ReactiveServerHttpRequest; import org.springframework.reactive.web.dispatch.method.HandlerMethodArgumentResolver; -import org.springframework.reactive.web.http.ServerHttpRequest; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; @@ -39,7 +40,7 @@ public class RequestParamArgumentResolver implements HandlerMethodArgumentResolv @Override - public Object resolveArgument(MethodParameter param, ServerHttpRequest request) { + public Object resolveArgument(MethodParameter param, ReactiveServerHttpRequest request) { RequestParam annotation = param.getParameterAnnotation(RequestParam.class); String name = (annotation.value().length() != 0 ? annotation.value() : param.getParameterName()); UriComponents uriComponents = UriComponentsBuilder.fromUri(request.getURI()).build(); diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java index bfd39cdbff..221b0cbd61 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java @@ -13,9 +13,26 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.reactive.web.dispatch.method.annotation; +import java.lang.reflect.Type; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; + import org.reactivestreams.Publisher; +import reactor.Publishers; +import reactor.core.publisher.convert.CompletableFutureConverter; +import reactor.core.publisher.convert.RxJava1Converter; +import reactor.core.publisher.convert.RxJava1SingleConverter; +import reactor.rx.Promise; +import rx.Observable; +import rx.Single; + import org.springframework.core.MethodParameter; import org.springframework.core.Ordered; import org.springframework.core.ResolvableType; @@ -23,20 +40,13 @@ import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.convert.ConversionService; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; +import org.springframework.http.server.ReactiveServerHttpRequest; +import org.springframework.http.server.ReactiveServerHttpResponse; import org.springframework.reactive.codec.encoder.MessageToByteEncoder; import org.springframework.reactive.web.dispatch.HandlerResult; import org.springframework.reactive.web.dispatch.HandlerResultHandler; -import org.springframework.reactive.web.http.ServerHttpRequest; -import org.springframework.reactive.web.http.ServerHttpResponse; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.method.HandlerMethod; -import reactor.Publishers; - -import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; /** @@ -95,7 +105,8 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered @Override @SuppressWarnings("unchecked") - public Publisher handleResult(ServerHttpRequest request, ServerHttpResponse response, + public Publisher handleResult(ReactiveServerHttpRequest request, + ReactiveServerHttpResponse response, HandlerResult result) { Object value = result.getValue(); @@ -129,20 +140,20 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered outputStream = postProcessor.encode(outputStream, elementType, mediaType, hints.toArray()); } response.getHeaders().setContentType(mediaType); - return response.writeWith(outputStream); + return response.addBody(outputStream); } return Publishers.error(new IllegalStateException( "Return value type '" + returnType.getParameterType().getName() + "' with media type '" + mediaType + "' not supported" )); } - private MediaType resolveMediaType(ServerHttpRequest request) { + private MediaType resolveMediaType(ReactiveServerHttpRequest request) { String acceptHeader = request.getHeaders().getFirst(HttpHeaders.ACCEPT); List mediaTypes = MediaType.parseMediaTypes(acceptHeader); MediaType.sortBySpecificityAndQuality(mediaTypes); return ( mediaTypes.size() > 0 ? mediaTypes.get(0) : MediaType.TEXT_PLAIN); } - private MessageToByteEncoder resolveSerializer(ServerHttpRequest request, ResolvableType type, MediaType mediaType, Object[] hints) { + private MessageToByteEncoder resolveSerializer(ReactiveServerHttpRequest request, ResolvableType type, MediaType mediaType, Object[] hints) { for (MessageToByteEncoder codec : this.serializers) { if (codec.canEncode(type, mediaType, hints)) { return codec; @@ -151,7 +162,7 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered return null; } - private List> resolvePostProcessors(ServerHttpRequest request, ResolvableType type, MediaType mediaType, Object[] hints) { + private List> resolvePostProcessors(ReactiveServerHttpRequest request, ResolvableType type, MediaType mediaType, Object[] hints) { List> postProcessors = new ArrayList<>(); for (MessageToByteEncoder postProcessor : this.postProcessors) { if (postProcessor.canEncode(type, mediaType, hints)) { diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/HttpHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/HttpHandler.java index 4158526da8..87076451b6 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/HttpHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/HttpHandler.java @@ -18,6 +18,8 @@ package org.springframework.reactive.web.http; import org.reactivestreams.Publisher; +import org.springframework.http.server.ReactiveServerHttpRequest; +import org.springframework.http.server.ReactiveServerHttpResponse; /** * Interface for handlers that process HTTP requests and generate an HTTP response. @@ -28,8 +30,8 @@ import org.reactivestreams.Publisher; * @author Arjen Poutsma * @author Rossen Stoyanchev * @author Sebastien Deleuze - * @see ServerHttpRequest#getBody() - * @see ServerHttpResponse#writeWith(Publisher) + * @see ReactiveServerHttpRequest#getBody() + * @see ReactiveServerHttpResponse#addBody(Publisher) */ public interface HttpHandler { @@ -44,6 +46,6 @@ public interface HttpHandler { * when the handling is complete (success or error) including the flush of the data on the * network. */ - Publisher handle(ServerHttpRequest request, ServerHttpResponse response); + Publisher handle(ReactiveServerHttpRequest request, ReactiveServerHttpResponse response); } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpRequest.java index 75b22152d7..b79763bd33 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpRequest.java @@ -1,11 +1,11 @@ /* - * Copyright (c) 2011-2015 Pivotal Software Inc, All Rights Reserved. + * Copyright 2002-2015 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 + * 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, @@ -21,6 +21,18 @@ import reactor.io.buffer.Buffer; import reactor.io.net.http.HttpChannel; import reactor.rx.Stream; import reactor.rx.Streams; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.ByteBuffer; + +import org.reactivestreams.Publisher; +import reactor.io.buffer.Buffer; +import reactor.io.net.http.HttpChannel; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.server.ReactiveServerHttpRequest; +import org.springframework.util.Assert; /** * @author Stephane Maldini diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpResponse.java index eec16e8521..12426ba5c1 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpResponse.java @@ -1,11 +1,11 @@ /* - * Copyright (c) 2011-2015 Pivotal Software Inc, All Rights Reserved. + * Copyright 2002-2015 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 + * 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, diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpRequest.java index 7aa05a50cd..5f3be593e6 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpRequest.java @@ -13,28 +13,29 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.reactive.web.http.rxnetty; -import io.netty.buffer.ByteBuf; -import io.reactivex.netty.protocol.http.server.HttpServerRequest; -import org.reactivestreams.Publisher; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.reactive.web.http.ServerHttpRequest; -import org.springframework.util.Assert; - -import reactor.core.publisher.convert.RxJava1Converter; -import rx.Observable; - import java.net.URI; import java.net.URISyntaxException; import java.nio.ByteBuffer; +import io.netty.buffer.ByteBuf; +import io.reactivex.netty.protocol.http.server.HttpServerRequest; +import org.reactivestreams.Publisher; +import reactor.core.publisher.convert.RxJava1Converter; +import rx.Observable; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.server.ReactiveServerHttpRequest; +import org.springframework.util.Assert; + /** * @author Rossen Stoyanchev * @author Stephane Maldini */ -public class RxNettyServerHttpRequest implements ServerHttpRequest { +public class RxNettyServerHttpRequest implements ReactiveServerHttpRequest { private final HttpServerRequest request; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpResponse.java index 1143663e66..91989ec9c2 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpResponse.java @@ -13,14 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.reactive.web.http.rxnetty; +import java.nio.ByteBuffer; + import io.netty.handler.codec.http.HttpResponseStatus; import io.reactivex.netty.protocol.http.server.HttpServerResponse; import org.reactivestreams.Publisher; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; -import org.springframework.reactive.web.http.ServerHttpResponse; import org.springframework.util.Assert; import reactor.Publishers; @@ -28,13 +30,16 @@ import reactor.core.publisher.convert.RxJava1Converter; import reactor.io.buffer.Buffer; import rx.Observable; -import java.nio.ByteBuffer; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.server.ReactiveServerHttpResponse; +import org.springframework.util.Assert; /** * @author Rossen Stoyanchev * @author Stephane Maldini */ -public class RxNettyServerHttpResponse implements ServerHttpResponse { +public class RxNettyServerHttpResponse implements ReactiveServerHttpResponse { private final HttpServerResponse response; @@ -70,7 +75,7 @@ public class RxNettyServerHttpResponse implements ServerHttpResponse { } @Override - public Publisher writeWith(Publisher contentPublisher) { + public Publisher addBody(Publisher contentPublisher) { applyHeaders(); Observable contentObservable = RxJava1Converter.from(contentPublisher).map(content -> new Buffer(content).asBytes()); return RxJava1Converter.from(this.response.writeBytes(contentObservable)); diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpRequest.java index b6aaea8fb7..80a1406351 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpRequest.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.reactive.web.http.servlet; import java.net.URI; @@ -21,7 +22,6 @@ import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.util.Enumeration; import java.util.Map; - import javax.servlet.http.HttpServletRequest; import org.reactivestreams.Publisher; @@ -29,7 +29,7 @@ import org.reactivestreams.Publisher; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; -import org.springframework.reactive.web.http.ServerHttpRequest; +import org.springframework.http.server.ReactiveServerHttpRequest; import org.springframework.util.Assert; import org.springframework.util.LinkedCaseInsensitiveMap; import org.springframework.util.StringUtils; @@ -37,7 +37,7 @@ import org.springframework.util.StringUtils; /** * @author Rossen Stoyanchev */ -public class ServletServerHttpRequest implements ServerHttpRequest { +public class ServletServerHttpRequest implements ReactiveServerHttpRequest { private final HttpServletRequest servletRequest; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpResponse.java index ff462ec158..614942283a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpResponse.java @@ -13,12 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.reactive.web.http.servlet; import java.nio.ByteBuffer; import java.util.List; import java.util.Map; - import javax.servlet.http.HttpServletResponse; import org.reactivestreams.Publisher; @@ -26,13 +26,13 @@ import reactor.Publishers; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; -import org.springframework.reactive.web.http.ServerHttpResponse; +import org.springframework.http.server.ReactiveServerHttpResponse; import org.springframework.util.Assert; /** * @author Rossen Stoyanchev */ -public class ServletServerHttpResponse implements ServerHttpResponse { +public class ServletServerHttpResponse implements ReactiveServerHttpResponse { private final HttpServletResponse servletResponse; @@ -69,7 +69,7 @@ public class ServletServerHttpResponse implements ServerHttpResponse { } @Override - public Publisher writeWith(final Publisher contentPublisher) { + public Publisher addBody(final Publisher contentPublisher) { applyHeaders(); return (s -> contentPublisher.subscribe(responseSubscriber)); } diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/handler/SimpleUrlHandlerMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/handler/SimpleUrlHandlerMappingIntegrationTests.java index de986c811f..764a66ce56 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/handler/SimpleUrlHandlerMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/handler/SimpleUrlHandlerMappingIntegrationTests.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.reactive.web.dispatch.handler; import java.net.URI; @@ -27,12 +28,12 @@ import reactor.rx.Streams; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; +import org.springframework.http.server.ReactiveServerHttpRequest; +import org.springframework.http.server.ReactiveServerHttpResponse; import org.springframework.reactive.web.dispatch.DispatcherHandler; import org.springframework.reactive.web.dispatch.SimpleHandlerResultHandler; import org.springframework.reactive.web.http.AbstractHttpHandlerIntegrationTests; import org.springframework.reactive.web.http.HttpHandler; -import org.springframework.reactive.web.http.ServerHttpRequest; -import org.springframework.reactive.web.http.ServerHttpResponse; import org.springframework.web.client.RestTemplate; import org.springframework.web.context.support.StaticWebApplicationContext; @@ -99,16 +100,16 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler private static class FooHandler implements HttpHandler { @Override - public Publisher handle(ServerHttpRequest request, ServerHttpResponse response) { - return response.writeWith(Streams.just(Buffer.wrap("foo").byteBuffer())); + public Publisher handle(ReactiveServerHttpRequest request, ReactiveServerHttpResponse response) { + return response.addBody(Streams.just(Buffer.wrap("foo").byteBuffer())); } } private static class BarHandler implements HttpHandler { @Override - public Publisher handle(ServerHttpRequest request, ServerHttpResponse response) { - return response.writeWith(Streams.just(Buffer.wrap("bar").byteBuffer())); + public Publisher handle(ReactiveServerHttpRequest request, ReactiveServerHttpResponse response) { + return response.addBody(Streams.just(Buffer.wrap("bar").byteBuffer())); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerMappingTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerMappingTests.java index 9bb4a02aed..0d53aac9bf 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerMappingTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerMappingTests.java @@ -20,20 +20,21 @@ import java.net.URI; import java.net.URISyntaxException; import java.nio.ByteBuffer; -import static org.junit.Assert.assertEquals; import org.junit.Before; import org.junit.Test; import org.reactivestreams.Publisher; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; -import org.springframework.reactive.web.http.ServerHttpRequest; +import org.springframework.http.server.ReactiveServerHttpRequest; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.context.support.StaticWebApplicationContext; import org.springframework.web.method.HandlerMethod; +import static org.junit.Assert.assertEquals; + /** * @author Sebastien Deleuze */ @@ -52,14 +53,14 @@ public class RequestMappingHandlerMappingTests { @Test public void path() throws NoSuchMethodException { - ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, "boo"); + ReactiveServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, "boo"); HandlerMethod handler = (HandlerMethod) this.mapping.getHandler(request); assertEquals(TestController.class.getMethod("boo"), handler.getMethod()); } @Test public void method() throws NoSuchMethodException { - ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.POST, "foo"); + ReactiveServerHttpRequest request = new MockServerHttpRequest(HttpMethod.POST, "foo"); HandlerMethod handler = (HandlerMethod) this.mapping.getHandler(request); assertEquals(TestController.class.getMethod("postFoo"), handler.getMethod()); @@ -104,7 +105,7 @@ public class RequestMappingHandlerMappingTests { } - private static class MockServerHttpRequest implements ServerHttpRequest{ + private static class MockServerHttpRequest implements ReactiveServerHttpRequest{ private HttpMethod method; diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/EchoHandler.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/EchoHandler.java index a5241cc934..62a29932fc 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/EchoHandler.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/EchoHandler.java @@ -18,13 +18,16 @@ package org.springframework.reactive.web.http; import org.reactivestreams.Publisher; +import org.springframework.http.server.ReactiveServerHttpRequest; +import org.springframework.http.server.ReactiveServerHttpResponse; + /** * @author Arjen Poutsma */ public class EchoHandler implements HttpHandler { @Override - public Publisher handle(ServerHttpRequest request, ServerHttpResponse response) { - return response.writeWith(request.getBody()); + public Publisher handle(ReactiveServerHttpRequest request, ReactiveServerHttpResponse response) { + return response.addBody(request.getBody()); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/RandomHandler.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/RandomHandler.java index 69244b098c..592b510c38 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/RandomHandler.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/RandomHandler.java @@ -27,6 +27,9 @@ import org.reactivestreams.Subscription; import reactor.io.buffer.Buffer; import reactor.rx.Streams; +import org.springframework.http.server.ReactiveServerHttpRequest; +import org.springframework.http.server.ReactiveServerHttpResponse; + import static org.junit.Assert.assertEquals; /** @@ -41,7 +44,7 @@ public class RandomHandler implements HttpHandler { private final Random rnd = new Random(); @Override - public Publisher handle(ServerHttpRequest request, ServerHttpResponse response) { + public Publisher handle(ReactiveServerHttpRequest request, ReactiveServerHttpResponse response) { request.getBody().subscribe(new Subscriber() { private Subscription s; @@ -73,7 +76,7 @@ public class RandomHandler implements HttpHandler { }); response.getHeaders().setContentLength(RESPONSE_SIZE); - return response.writeWith(Streams.just(ByteBuffer.wrap(randomBytes()))); + return response.addBody(Streams.just(ByteBuffer.wrap(randomBytes()))); } private byte[] randomBytes() { diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/XmlHandler.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/XmlHandler.java index dcf95a8dc2..fc18ec885f 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/XmlHandler.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/XmlHandler.java @@ -23,14 +23,16 @@ import javax.xml.bind.Unmarshaller; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.reactivestreams.Publisher; +import reactor.io.buffer.Buffer; +import reactor.rx.Streams; import org.springframework.http.MediaType; +import org.springframework.http.server.ReactiveServerHttpRequest; +import org.springframework.http.server.ReactiveServerHttpResponse; import org.springframework.reactive.io.BufferOutputStream; import org.springframework.reactive.io.ByteBufferPublisherInputStream; import static org.junit.Assert.fail; -import reactor.io.buffer.Buffer; -import reactor.rx.Streams; /** * @author Arjen Poutsma @@ -40,8 +42,8 @@ public class XmlHandler implements HttpHandler { private static final Log logger = LogFactory.getLog(XmlHandler.class); @Override - public Publisher handle(ServerHttpRequest request, - ServerHttpResponse response) { + public Publisher handle(ReactiveServerHttpRequest request, + ReactiveServerHttpResponse response) { try { JAXBContext jaxbContext = JAXBContext.newInstance(XmlHandlerIntegrationTests.Person.class); Unmarshaller unmarshaller = jaxbContext.createUnmarshaller(); @@ -73,7 +75,7 @@ public class XmlHandler implements HttpHandler { bos.close(); buffer.flip(); - return response.writeWith(Streams.just(buffer.byteBuffer())); + return response.addBody(Streams.just(buffer.byteBuffer())); } catch (Exception ex) { logger.error(ex, ex); From 5727abc82d8583c67b3d01e375309fbd9ecf8c57 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Thu, 29 Oct 2015 16:08:10 +0100 Subject: [PATCH 069/498] Resolved merge conflicts --- .../reactor/PublisherReactorServerHttpRequest.java | 4 ++-- .../PublisherReactorServerHttpResponse.java | 6 +++--- .../http/reactor/ReactorServerHttpResponse.java | 14 ++++---------- 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/PublisherReactorServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/PublisherReactorServerHttpRequest.java index 10960ab2ae..28f7a0e5ba 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/PublisherReactorServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/PublisherReactorServerHttpRequest.java @@ -26,13 +26,13 @@ import reactor.io.net.http.HttpChannel; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; -import org.springframework.reactive.web.http.ServerHttpRequest; +import org.springframework.http.server.ReactiveServerHttpRequest; import org.springframework.util.Assert; /** * @author Stephane Maldini */ -public class PublisherReactorServerHttpRequest implements ServerHttpRequest { +public class PublisherReactorServerHttpRequest implements ReactiveServerHttpRequest { private final HttpChannel channel; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/PublisherReactorServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/PublisherReactorServerHttpResponse.java index 4f52316968..0e9827ad2f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/PublisherReactorServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/PublisherReactorServerHttpResponse.java @@ -25,13 +25,13 @@ import reactor.io.net.http.model.Status; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; -import org.springframework.reactive.web.http.ServerHttpResponse; +import org.springframework.http.server.ReactiveServerHttpResponse; import org.springframework.util.Assert; /** * @author Stephane Maldini */ -public class PublisherReactorServerHttpResponse implements ServerHttpResponse { +public class PublisherReactorServerHttpResponse implements ReactiveServerHttpResponse { private final HttpChannel channel; @@ -67,7 +67,7 @@ public class PublisherReactorServerHttpResponse implements ServerHttpResponse { } @Override - public Publisher writeWith(Publisher contentPublisher) { + public Publisher addBody(Publisher contentPublisher) { applyHeaders(); return this.channel.writeWith(Publishers.map(contentPublisher, Buffer::new)); } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpResponse.java index 12426ba5c1..2a50a26edf 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpResponse.java @@ -15,20 +15,14 @@ */ package org.springframework.reactive.web.http.reactor; +import java.nio.ByteBuffer; + import org.reactivestreams.Publisher; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.reactive.web.http.ServerHttpResponse; -import org.springframework.util.Assert; -import reactor.Publishers; import reactor.io.buffer.Buffer; import reactor.io.net.http.HttpChannel; -import reactor.io.net.http.model.Status; import reactor.rx.Stream; import reactor.rx.Streams; -import java.nio.ByteBuffer; - /** * @author Stephane Maldini */ @@ -44,7 +38,7 @@ public class ReactorServerHttpResponse extends PublisherReactorServerHttpRespons } @Override - public Stream writeWith(Publisher contentPublisher) { - return Streams.wrap(super.writeWith(contentPublisher)); + public Stream addBody(Publisher contentPublisher) { + return Streams.wrap(super.addBody(contentPublisher)); } } From 45a88974bc9545eff6c7de07d054a2836dcbdfca Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Thu, 29 Oct 2015 16:28:22 +0100 Subject: [PATCH 070/498] Changed addBody() to setBody() --- .../org/springframework/http/ReactiveHttpOutputMessage.java | 4 ++-- .../method/annotation/ResponseBodyResultHandler.java | 5 +++-- .../org/springframework/reactive/web/http/HttpHandler.java | 2 +- .../web/http/reactor/PublisherReactorServerHttpResponse.java | 2 +- .../reactive/web/http/reactor/ReactorServerHttpResponse.java | 4 ++-- .../reactive/web/http/rxnetty/RxNettyServerHttpResponse.java | 2 +- .../reactive/web/http/servlet/ServletServerHttpResponse.java | 2 +- .../handler/SimpleUrlHandlerMappingIntegrationTests.java | 4 ++-- .../org/springframework/reactive/web/http/EchoHandler.java | 2 +- .../org/springframework/reactive/web/http/RandomHandler.java | 2 +- .../org/springframework/reactive/web/http/XmlHandler.java | 2 +- 11 files changed, 16 insertions(+), 15 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java b/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java index bbd5022a88..13e2b6d8e7 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java @@ -22,7 +22,7 @@ import org.reactivestreams.Publisher; /** * Represents a "reactive" HTTP output message, consisting of {@linkplain #getHeaders() headers} - * and the capability to add a {@linkplain #addBody(Publisher) body}. + * and the capability to add a {@linkplain #setBody(Publisher) body}. * *

    Typically implemented by an HTTP request on the client-side, or a response on the server-side. * @@ -36,6 +36,6 @@ public interface ReactiveHttpOutputMessage extends HttpMessage { * @param body the body to add * @return a publisher that indicates completion */ - Publisher addBody(Publisher body); + Publisher setBody(Publisher body); } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java index 221b0cbd61..2fa6eb650a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java @@ -140,10 +140,11 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered outputStream = postProcessor.encode(outputStream, elementType, mediaType, hints.toArray()); } response.getHeaders().setContentType(mediaType); - return response.addBody(outputStream); + return response.setBody(outputStream); } return Publishers.error(new IllegalStateException( - "Return value type '" + returnType.getParameterType().getName() + "' with media type '" + mediaType + "' not supported" )); + "Return value type '" + returnType.getParameterType().getName() + + "' with media type '" + mediaType + "' not supported")); } private MediaType resolveMediaType(ReactiveServerHttpRequest request) { diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/HttpHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/HttpHandler.java index 87076451b6..ff86a9a722 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/HttpHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/HttpHandler.java @@ -31,7 +31,7 @@ import org.springframework.http.server.ReactiveServerHttpResponse; * @author Rossen Stoyanchev * @author Sebastien Deleuze * @see ReactiveServerHttpRequest#getBody() - * @see ReactiveServerHttpResponse#addBody(Publisher) + * @see ReactiveServerHttpResponse#setBody(Publisher) */ public interface HttpHandler { diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/PublisherReactorServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/PublisherReactorServerHttpResponse.java index 0e9827ad2f..dcc605fc3c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/PublisherReactorServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/PublisherReactorServerHttpResponse.java @@ -67,7 +67,7 @@ public class PublisherReactorServerHttpResponse implements ReactiveServerHttpRes } @Override - public Publisher addBody(Publisher contentPublisher) { + public Publisher setBody(Publisher contentPublisher) { applyHeaders(); return this.channel.writeWith(Publishers.map(contentPublisher, Buffer::new)); } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpResponse.java index 2a50a26edf..54bcac5eaa 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpResponse.java @@ -38,7 +38,7 @@ public class ReactorServerHttpResponse extends PublisherReactorServerHttpRespons } @Override - public Stream addBody(Publisher contentPublisher) { - return Streams.wrap(super.addBody(contentPublisher)); + public Stream setBody(Publisher contentPublisher) { + return Streams.wrap(super.setBody(contentPublisher)); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpResponse.java index 91989ec9c2..b19180dd89 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpResponse.java @@ -75,7 +75,7 @@ public class RxNettyServerHttpResponse implements ReactiveServerHttpResponse { } @Override - public Publisher addBody(Publisher contentPublisher) { + public Publisher setBody(Publisher contentPublisher) { applyHeaders(); Observable contentObservable = RxJava1Converter.from(contentPublisher).map(content -> new Buffer(content).asBytes()); return RxJava1Converter.from(this.response.writeBytes(contentObservable)); diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpResponse.java index 614942283a..e0bc7c78f7 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpResponse.java @@ -69,7 +69,7 @@ public class ServletServerHttpResponse implements ReactiveServerHttpResponse { } @Override - public Publisher addBody(final Publisher contentPublisher) { + public Publisher setBody(final Publisher contentPublisher) { applyHeaders(); return (s -> contentPublisher.subscribe(responseSubscriber)); } diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/handler/SimpleUrlHandlerMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/handler/SimpleUrlHandlerMappingIntegrationTests.java index 764a66ce56..70ca1c7b5c 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/handler/SimpleUrlHandlerMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/handler/SimpleUrlHandlerMappingIntegrationTests.java @@ -101,7 +101,7 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler @Override public Publisher handle(ReactiveServerHttpRequest request, ReactiveServerHttpResponse response) { - return response.addBody(Streams.just(Buffer.wrap("foo").byteBuffer())); + return response.setBody(Streams.just(Buffer.wrap("foo").byteBuffer())); } } @@ -109,7 +109,7 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler @Override public Publisher handle(ReactiveServerHttpRequest request, ReactiveServerHttpResponse response) { - return response.addBody(Streams.just(Buffer.wrap("bar").byteBuffer())); + return response.setBody(Streams.just(Buffer.wrap("bar").byteBuffer())); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/EchoHandler.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/EchoHandler.java index 62a29932fc..907191b95f 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/EchoHandler.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/EchoHandler.java @@ -28,6 +28,6 @@ public class EchoHandler implements HttpHandler { @Override public Publisher handle(ReactiveServerHttpRequest request, ReactiveServerHttpResponse response) { - return response.addBody(request.getBody()); + return response.setBody(request.getBody()); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/RandomHandler.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/RandomHandler.java index 592b510c38..79a87fd76e 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/RandomHandler.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/RandomHandler.java @@ -76,7 +76,7 @@ public class RandomHandler implements HttpHandler { }); response.getHeaders().setContentLength(RESPONSE_SIZE); - return response.addBody(Streams.just(ByteBuffer.wrap(randomBytes()))); + return response.setBody(Streams.just(ByteBuffer.wrap(randomBytes()))); } private byte[] randomBytes() { diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/XmlHandler.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/XmlHandler.java index fc18ec885f..490cbd4c1b 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/XmlHandler.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/XmlHandler.java @@ -75,7 +75,7 @@ public class XmlHandler implements HttpHandler { bos.close(); buffer.flip(); - return response.addBody(Streams.just(buffer.byteBuffer())); + return response.setBody(Streams.just(buffer.byteBuffer())); } catch (Exception ex) { logger.error(ex, ex); From e121f7ecd462c5fd58a37d13b8b7b8f187a2cb74 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Thu, 29 Oct 2015 16:44:13 +0100 Subject: [PATCH 071/498] Improved documentation on setBody() --- .../springframework/http/ReactiveHttpOutputMessage.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java b/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java index 13e2b6d8e7..674268f6cd 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java @@ -31,9 +31,11 @@ import org.reactivestreams.Publisher; public interface ReactiveHttpOutputMessage extends HttpMessage { /** - * Adds the given publisher of {@link ByteBuffer}s as a body. A HTTP/1.1 message has - * one body, but HTTP/1.2 supports multiple bodies. - * @param body the body to add + * Sets the body of this message to the given publisher of {@link ByteBuffer}s. The + * publisher will be used to write to the underlying HTTP layer with asynchronously, + * given pull demand by this layer. + * + * @param body the body to use * @return a publisher that indicates completion */ Publisher setBody(Publisher body); From 6006fa6ec2c811dca36ec49a884c6028eb9b81a7 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Fri, 30 Oct 2015 09:24:53 +0100 Subject: [PATCH 072/498] Optimized imports --- .../server/ReactiveServerHttpResponse.java | 3 +- .../codec/decoder/JacksonJsonDecoder.java | 9 ++--- .../reactive/codec/decoder/Jaxb2Decoder.java | 33 +++++++++++-------- .../codec/decoder/JsonObjectDecoder.java | 14 ++++---- .../reactive/codec/decoder/StringDecoder.java | 13 ++++---- .../codec/encoder/JacksonJsonEncoder.java | 11 ++++--- .../reactive/codec/encoder/Jaxb2Encoder.java | 23 ++++++------- .../codec/encoder/JsonObjectEncoder.java | 19 +++++------ .../reactive/codec/encoder/StringEncoder.java | 11 ++++--- .../io/ByteBufferPublisherInputStream.java | 11 ++++--- .../RequestBodyArgumentResolver.java | 11 ------- .../RequestMappingHandlerAdapter.java | 4 +-- .../annotation/ResponseBodyResultHandler.java | 8 ----- .../reactor/ReactorServerHttpRequest.java | 12 ------- .../web/http/rxnetty/RxNettyHttpServer.java | 1 - .../rxnetty/RxNettyServerHttpResponse.java | 4 --- .../servlet/AsyncContextSynchronizer.java | 1 - .../web/http/servlet/HttpHandlerServlet.java | 1 - .../http/servlet/RequestBodyPublisher.java | 2 -- 19 files changed, 81 insertions(+), 110 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/ReactiveServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/ReactiveServerHttpResponse.java index 514592df51..3563b7b07b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/ReactiveServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/ReactiveServerHttpResponse.java @@ -16,8 +16,9 @@ package org.springframework.http.server; -import org.springframework.http.HttpStatus; import org.reactivestreams.Publisher; + +import org.springframework.http.HttpStatus; import org.springframework.http.ReactiveHttpOutputMessage; /** diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoder.java index 0562653bdc..3b98fbfa5b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoder.java @@ -16,18 +16,19 @@ package org.springframework.reactive.codec.decoder; +import java.io.IOException; +import java.nio.ByteBuffer; + import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectReader; import org.reactivestreams.Publisher; +import reactor.Publishers; + import org.springframework.core.ResolvableType; import org.springframework.http.MediaType; import org.springframework.reactive.codec.CodecException; import org.springframework.reactive.codec.encoder.JacksonJsonEncoder; import org.springframework.reactive.io.ByteBufferInputStream; -import reactor.Publishers; - -import java.io.IOException; -import java.nio.ByteBuffer; /** * Decode from a bytes stream of JSON objects to a stream of {@code Object} (POJO). diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/Jaxb2Decoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/Jaxb2Decoder.java index bff1ef0334..c1c0dc1883 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/Jaxb2Decoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/Jaxb2Decoder.java @@ -16,27 +16,32 @@ package org.springframework.reactive.codec.decoder; +import java.nio.ByteBuffer; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBElement; +import javax.xml.bind.JAXBException; +import javax.xml.bind.UnmarshalException; +import javax.xml.bind.Unmarshaller; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.transform.Source; +import javax.xml.transform.sax.SAXSource; +import javax.xml.transform.stream.StreamSource; + import org.reactivestreams.Publisher; -import org.springframework.core.ResolvableType; -import org.springframework.http.MediaType; -import org.springframework.reactive.codec.CodecException; -import org.springframework.reactive.codec.encoder.Jaxb2Encoder; -import org.springframework.reactive.io.ByteBufferPublisherInputStream; -import org.springframework.util.Assert; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.XMLReader; import org.xml.sax.helpers.XMLReaderFactory; import reactor.Publishers; -import javax.xml.bind.*; -import javax.xml.bind.annotation.XmlRootElement; -import javax.xml.transform.Source; -import javax.xml.transform.sax.SAXSource; -import javax.xml.transform.stream.StreamSource; -import java.nio.ByteBuffer; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; +import org.springframework.core.ResolvableType; +import org.springframework.http.MediaType; +import org.springframework.reactive.codec.CodecException; +import org.springframework.reactive.codec.encoder.Jaxb2Encoder; +import org.springframework.reactive.io.ByteBufferPublisherInputStream; +import org.springframework.util.Assert; /** * Decode from a bytes stream of XML elements to a stream of {@code Object} (POJO). diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JsonObjectDecoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JsonObjectDecoder.java index 9831b4035f..e3346d9793 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JsonObjectDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JsonObjectDecoder.java @@ -16,20 +16,20 @@ package org.springframework.reactive.codec.decoder; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; import org.reactivestreams.Publisher; -import org.springframework.core.ResolvableType; -import org.springframework.http.MediaType; -import org.springframework.reactive.codec.encoder.JsonObjectEncoder; - import reactor.Publishers; import reactor.fn.Function; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; +import org.springframework.core.ResolvableType; +import org.springframework.http.MediaType; +import org.springframework.reactive.codec.encoder.JsonObjectEncoder; /** * Decode an arbitrary split byte stream representing JSON objects to a byte stream diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/StringDecoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/StringDecoder.java index 91742a84b7..152cb6cf34 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/StringDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/StringDecoder.java @@ -16,17 +16,18 @@ package org.springframework.reactive.codec.decoder; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + import org.reactivestreams.Publisher; +import reactor.Publishers; +import reactor.io.buffer.Buffer; + import org.springframework.core.ResolvableType; import org.springframework.http.MediaType; import org.springframework.reactive.codec.encoder.StringEncoder; import org.springframework.reactive.codec.support.HintUtils; -import reactor.Publishers; -import reactor.io.buffer.Buffer; - -import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; /** * Decode from a bytes stream to a String stream. diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoder.java index efdb652be0..f24f7d76d1 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoder.java @@ -16,18 +16,19 @@ package org.springframework.reactive.codec.encoder; +import java.io.IOException; +import java.nio.ByteBuffer; + import com.fasterxml.jackson.databind.ObjectMapper; import org.reactivestreams.Publisher; +import reactor.Publishers; +import reactor.io.buffer.Buffer; + import org.springframework.core.ResolvableType; import org.springframework.http.MediaType; import org.springframework.reactive.codec.CodecException; import org.springframework.reactive.codec.decoder.JacksonJsonDecoder; import org.springframework.reactive.io.BufferOutputStream; -import reactor.Publishers; -import reactor.io.buffer.Buffer; - -import java.io.IOException; -import java.nio.ByteBuffer; /** * Encode from an {@code Object} stream to a byte stream of JSON objects. diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/Jaxb2Encoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/Jaxb2Encoder.java index 3a2887e21f..6533e5fe90 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/Jaxb2Encoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/Jaxb2Encoder.java @@ -16,7 +16,19 @@ package org.springframework.reactive.codec.encoder; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.MarshalException; +import javax.xml.bind.Marshaller; + import org.reactivestreams.Publisher; +import reactor.Publishers; +import reactor.io.buffer.Buffer; + import org.springframework.core.ResolvableType; import org.springframework.http.MediaType; import org.springframework.reactive.codec.CodecException; @@ -24,17 +36,6 @@ import org.springframework.reactive.codec.decoder.Jaxb2Decoder; import org.springframework.reactive.io.BufferOutputStream; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; -import reactor.Publishers; -import reactor.io.buffer.Buffer; - -import javax.xml.bind.JAXBContext; -import javax.xml.bind.JAXBException; -import javax.xml.bind.MarshalException; -import javax.xml.bind.Marshaller; -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; /** * Encode from an {@code Object} stream to a byte stream of XML elements. diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JsonObjectEncoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JsonObjectEncoder.java index d39e530f73..e6a070642a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JsonObjectEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JsonObjectEncoder.java @@ -16,23 +16,22 @@ package org.springframework.reactive.codec.encoder; -import org.reactivestreams.Publisher; -import org.reactivestreams.Subscriber; -import org.springframework.core.ResolvableType; -import org.springframework.http.MediaType; -import org.springframework.reactive.codec.decoder.JsonObjectDecoder; - -import reactor.core.subscriber.SubscriberBarrier; - import java.nio.ByteBuffer; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; import java.util.concurrent.atomic.AtomicLongFieldUpdater; -import static reactor.Publishers.*; - +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import reactor.core.subscriber.SubscriberBarrier; import reactor.core.support.BackpressureUtils; import reactor.io.buffer.Buffer; +import org.springframework.core.ResolvableType; +import org.springframework.http.MediaType; +import org.springframework.reactive.codec.decoder.JsonObjectDecoder; + +import static reactor.Publishers.lift; + /** * Encode a byte stream of individual JSON element to a byte stream representing a single * JSON array when if it contains more than one element. diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/StringEncoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/StringEncoder.java index ac267de1c1..f7bc6699fc 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/StringEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/StringEncoder.java @@ -16,16 +16,17 @@ package org.springframework.reactive.codec.encoder; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + import org.reactivestreams.Publisher; +import reactor.Publishers; + import org.springframework.core.ResolvableType; import org.springframework.http.MediaType; import org.springframework.reactive.codec.decoder.StringDecoder; import org.springframework.reactive.codec.support.HintUtils; -import reactor.Publishers; - -import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; /** * Encode from a String stream to a bytes stream. diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteBufferPublisherInputStream.java b/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteBufferPublisherInputStream.java index 647e688712..a4eace4e86 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteBufferPublisherInputStream.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteBufferPublisherInputStream.java @@ -16,16 +16,17 @@ package org.springframework.reactive.io; -import org.reactivestreams.Publisher; -import org.reactivestreams.Subscription; -import org.springframework.util.Assert; -import reactor.Publishers; - import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; import java.util.concurrent.BlockingQueue; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscription; +import reactor.Publishers; + +import org.springframework.util.Assert; + /** * {@code InputStream} implementation based on a byte array {@link Publisher}. * diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java index 0dea573a22..e14be9ee21 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java @@ -21,19 +21,8 @@ import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; import org.reactivestreams.Publisher; -import reactor.Publishers; -import reactor.core.publisher.convert.CompletableFutureConverter; -import reactor.core.publisher.convert.RxJava1Converter; -import reactor.core.publisher.convert.RxJava1SingleConverter; -import reactor.rx.Promise; -import reactor.rx.Stream; -import reactor.rx.Streams; -import rx.Observable; -import rx.Single; import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java index a24859915b..3ffa44d83c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java @@ -22,10 +22,10 @@ import java.util.Arrays; import java.util.List; import org.springframework.beans.factory.InitializingBean; -import org.springframework.reactive.codec.decoder.ByteBufferDecoder; -import org.springframework.reactive.codec.decoder.ByteToMessageDecoder; import org.springframework.http.server.ReactiveServerHttpRequest; import org.springframework.http.server.ReactiveServerHttpResponse; +import org.springframework.reactive.codec.decoder.ByteBufferDecoder; +import org.springframework.reactive.codec.decoder.ByteToMessageDecoder; import org.springframework.reactive.codec.decoder.JacksonJsonDecoder; import org.springframework.reactive.codec.decoder.JsonObjectDecoder; import org.springframework.reactive.codec.decoder.StringDecoder; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java index 2fa6eb650a..0bfd9cdd47 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java @@ -16,22 +16,14 @@ package org.springframework.reactive.web.dispatch.method.annotation; -import java.lang.reflect.Type; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.concurrent.CompletableFuture; import org.reactivestreams.Publisher; import reactor.Publishers; -import reactor.core.publisher.convert.CompletableFutureConverter; -import reactor.core.publisher.convert.RxJava1Converter; -import reactor.core.publisher.convert.RxJava1SingleConverter; -import reactor.rx.Promise; -import rx.Observable; -import rx.Single; import org.springframework.core.MethodParameter; import org.springframework.core.Ordered; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpRequest.java index b79763bd33..b0a7512d92 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpRequest.java @@ -21,18 +21,6 @@ import reactor.io.buffer.Buffer; import reactor.io.net.http.HttpChannel; import reactor.rx.Stream; import reactor.rx.Streams; -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.ByteBuffer; - -import org.reactivestreams.Publisher; -import reactor.io.buffer.Buffer; -import reactor.io.net.http.HttpChannel; - -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.server.ReactiveServerHttpRequest; -import org.springframework.util.Assert; /** * @author Stephane Maldini diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyHttpServer.java index d07cc78c84..f1526d1a3b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyHttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyHttpServer.java @@ -20,7 +20,6 @@ import io.netty.buffer.ByteBuf; import org.springframework.beans.factory.InitializingBean; import org.springframework.reactive.web.http.HttpServer; import org.springframework.reactive.web.http.HttpServerSupport; -import org.springframework.reactive.web.http.rxnetty.RequestHandlerAdapter; import org.springframework.util.Assert; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpResponse.java index b19180dd89..241d76fa59 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpResponse.java @@ -21,10 +21,6 @@ import java.nio.ByteBuffer; import io.netty.handler.codec.http.HttpResponseStatus; import io.reactivex.netty.protocol.http.server.HttpServerResponse; import org.reactivestreams.Publisher; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.util.Assert; - import reactor.Publishers; import reactor.core.publisher.convert.RxJava1Converter; import reactor.io.buffer.Buffer; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/AsyncContextSynchronizer.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/AsyncContextSynchronizer.java index 98811b67a2..65240b963c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/AsyncContextSynchronizer.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/AsyncContextSynchronizer.java @@ -18,7 +18,6 @@ package org.springframework.reactive.web.http.servlet; import java.io.IOException; import java.util.concurrent.atomic.AtomicInteger; - import javax.servlet.AsyncContext; import javax.servlet.ServletInputStream; import javax.servlet.ServletOutputStream; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/HttpHandlerServlet.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/HttpHandlerServlet.java index 9bcddbfd6f..5a6b7c2155 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/HttpHandlerServlet.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/HttpHandlerServlet.java @@ -17,7 +17,6 @@ package org.springframework.reactive.web.http.servlet; import java.io.IOException; - import javax.servlet.AsyncContext; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/RequestBodyPublisher.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/RequestBodyPublisher.java index c685882a09..12368fc183 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/RequestBodyPublisher.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/RequestBodyPublisher.java @@ -18,9 +18,7 @@ package org.springframework.reactive.web.http.servlet; import java.io.IOException; import java.nio.ByteBuffer; -import java.nio.charset.Charset; import java.util.Arrays; - import javax.servlet.ReadListener; import javax.servlet.ServletInputStream; From bd472af37e656bb2ec84b8ec3a67486ee79f3506 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Fri, 30 Oct 2015 09:26:34 +0100 Subject: [PATCH 073/498] Updated Copyright notices --- .../convert/support/ReactiveStreamsToReactorConverter.java | 4 ++-- .../convert/support/ReactiveStreamsToRxJava1Converter.java | 4 ++-- .../springframework/reactive/web/dispatch/HandlerResult.java | 1 + .../dispatch/method/annotation/DefaultConversionService.java | 4 ++-- .../org/springframework/reactive/web/http/HttpServer.java | 1 + .../springframework/reactive/web/http/HttpServerSupport.java | 1 + .../web/http/reactor/PublisherReactorServerHttpRequest.java | 4 ++-- .../web/http/reactor/PublisherReactorServerHttpResponse.java | 4 ++-- .../reactive/web/http/reactor/ReactorHttpServer.java | 4 ++-- .../reactive/web/http/reactor/ReactorServerHttpRequest.java | 1 + .../reactive/web/http/reactor/ReactorServerHttpResponse.java | 1 + .../reactive/web/http/reactor/RequestHandlerAdapter.java | 4 ++-- .../reactive/web/http/rxnetty/RequestHandlerAdapter.java | 1 + .../reactive/web/http/rxnetty/RxNettyHttpServer.java | 1 + .../reactive/web/http/servlet/JettyHttpServer.java | 1 + .../reactive/web/http/servlet/TomcatHttpServer.java | 1 + .../method/annotation/RequestMappingIntegrationTests.java | 1 + 17 files changed, 24 insertions(+), 14 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToReactorConverter.java b/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToReactorConverter.java index 52d894048c..411e0ab42a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToReactorConverter.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToReactorConverter.java @@ -1,11 +1,11 @@ /* - * Copyright (c) 2011-2015 Pivotal Software Inc, All Rights Reserved. + * Copyright 2002-2015 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 + * 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, diff --git a/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToRxJava1Converter.java b/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToRxJava1Converter.java index b4b2043dcb..b82d4742c8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToRxJava1Converter.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToRxJava1Converter.java @@ -1,11 +1,11 @@ /* - * Copyright (c) 2011-2015 Pivotal Software Inc, All Rights Reserved. + * Copyright 2002-2015 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 + * 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, diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerResult.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerResult.java index 2b3c912071..156f2f5c96 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerResult.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerResult.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.reactive.web.dispatch; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/DefaultConversionService.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/DefaultConversionService.java index 327bd8d8f8..f38d00a063 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/DefaultConversionService.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/DefaultConversionService.java @@ -1,11 +1,11 @@ /* - * Copyright (c) 2011-2015 Pivotal Software Inc, All Rights Reserved. + * Copyright 2002-2015 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 + * 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, diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/HttpServer.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/HttpServer.java index 7d0e436377..6006e97422 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/HttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/HttpServer.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.reactive.web.http; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/HttpServerSupport.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/HttpServerSupport.java index ae5b1a820c..f7aa9aae8b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/HttpServerSupport.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/HttpServerSupport.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.reactive.web.http; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/PublisherReactorServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/PublisherReactorServerHttpRequest.java index 28f7a0e5ba..81c761230b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/PublisherReactorServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/PublisherReactorServerHttpRequest.java @@ -1,11 +1,11 @@ /* - * Copyright (c) 2011-2015 Pivotal Software Inc, All Rights Reserved. + * Copyright 2002-2015 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 + * 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, diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/PublisherReactorServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/PublisherReactorServerHttpResponse.java index dcc605fc3c..d6b25ba454 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/PublisherReactorServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/PublisherReactorServerHttpResponse.java @@ -1,11 +1,11 @@ /* - * Copyright (c) 2011-2015 Pivotal Software Inc, All Rights Reserved. + * Copyright 2002-2015 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 + * 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, diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorHttpServer.java index 9cbd3976e9..5528fea3a8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorHttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorHttpServer.java @@ -1,11 +1,11 @@ /* - * Copyright (c) 2011-2015 Pivotal Software Inc, All Rights Reserved. + * Copyright 2002-2015 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 + * 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, diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpRequest.java index b0a7512d92..7dff1f2b7c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpRequest.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.reactive.web.http.reactor; import java.nio.ByteBuffer; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpResponse.java index 54bcac5eaa..831e0f2478 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpResponse.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.reactive.web.http.reactor; import java.nio.ByteBuffer; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/RequestHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/RequestHandlerAdapter.java index ecf4f560ba..2e6d120794 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/RequestHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/RequestHandlerAdapter.java @@ -1,11 +1,11 @@ /* - * Copyright (c) 2011-2015 Pivotal Software Inc, All Rights Reserved. + * Copyright 2002-2015 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 + * 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, diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RequestHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RequestHandlerAdapter.java index 4149dce5ea..1a8fea2b8f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RequestHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RequestHandlerAdapter.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.reactive.web.http.rxnetty; import io.netty.buffer.ByteBuf; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyHttpServer.java index f1526d1a3b..21dc109a2a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyHttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyHttpServer.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.reactive.web.http.rxnetty; import io.netty.buffer.ByteBuf; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/JettyHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/JettyHttpServer.java index ddfd5d01ab..4acad5dfe7 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/JettyHttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/JettyHttpServer.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.reactive.web.http.servlet; import org.eclipse.jetty.server.Server; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/TomcatHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/TomcatHttpServer.java index 16e37d0e85..472a74afc3 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/TomcatHttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/TomcatHttpServer.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.reactive.web.http.servlet; import java.io.File; diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingIntegrationTests.java index 6aa048e259..e46dc4ff46 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingIntegrationTests.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.reactive.web.dispatch.method.annotation; From 5f2e958faeea3f5d8a73b4e4bb85a57b697960dc Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Fri, 30 Oct 2015 09:38:29 +0100 Subject: [PATCH 074/498] Removed IOException declaration --- .../http/client/ReactiveClientHttpResponse.java | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/ReactiveClientHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/client/ReactiveClientHttpResponse.java index b5f619bdc9..5d120fb26d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/ReactiveClientHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/ReactiveClientHttpResponse.java @@ -17,7 +17,6 @@ package org.springframework.http.client; import java.io.Closeable; -import java.io.IOException; import org.springframework.http.HttpStatus; import org.springframework.http.ReactiveHttpInputMessage; @@ -32,23 +31,20 @@ public interface ReactiveClientHttpResponse extends ReactiveHttpInputMessage, Cl /** * Return the HTTP status code of the response. * @return the HTTP status as an HttpStatus enum value - * @throws IOException in case of I/O errors */ - HttpStatus getStatusCode() throws IOException; + HttpStatus getStatusCode(); /** * Return the HTTP status code of the response as integer * @return the HTTP status as an integer - * @throws IOException in case of I/O errors */ - int getRawStatusCode() throws IOException; + int getRawStatusCode(); /** * Return the HTTP status text of the response. * @return the HTTP status text - * @throws IOException in case of I/O errors */ - String getStatusText() throws IOException; + String getStatusText(); /** * Close this response, freeing any resources created. From b2bf18dd3d08060b93aaae53f4f2ca724330acf0 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Fri, 30 Oct 2015 09:39:46 +0100 Subject: [PATCH 075/498] Removed Closeable implementation --- .../http/client/ReactiveClientHttpResponse.java | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/ReactiveClientHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/client/ReactiveClientHttpResponse.java index 5d120fb26d..6c4fab27f5 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/ReactiveClientHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/ReactiveClientHttpResponse.java @@ -26,7 +26,7 @@ import org.springframework.http.ReactiveHttpInputMessage; * * @author Arjen Poutsma */ -public interface ReactiveClientHttpResponse extends ReactiveHttpInputMessage, Closeable { +public interface ReactiveClientHttpResponse extends ReactiveHttpInputMessage { /** * Return the HTTP status code of the response. @@ -46,10 +46,4 @@ public interface ReactiveClientHttpResponse extends ReactiveHttpInputMessage, Cl */ String getStatusText(); - /** - * Close this response, freeing any resources created. - */ - @Override - void close(); - } From a257a4a37dd8ea52188694d29115f55dcf2d6388 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Fri, 30 Oct 2015 11:02:40 +0100 Subject: [PATCH 076/498] Polishing --- .../http/client/ReactiveClientHttpResponse.java | 4 +--- .../http/server/ReactiveServerHttpResponse.java | 5 ++--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/ReactiveClientHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/client/ReactiveClientHttpResponse.java index 6c4fab27f5..ae4b7fa0de 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/ReactiveClientHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/ReactiveClientHttpResponse.java @@ -16,8 +16,6 @@ package org.springframework.http.client; -import java.io.Closeable; - import org.springframework.http.HttpStatus; import org.springframework.http.ReactiveHttpInputMessage; @@ -30,7 +28,7 @@ public interface ReactiveClientHttpResponse extends ReactiveHttpInputMessage { /** * Return the HTTP status code of the response. - * @return the HTTP status as an HttpStatus enum value + * @return the HTTP status as an {@link HttpStatus} enum value */ HttpStatus getStatusCode(); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/ReactiveServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/ReactiveServerHttpResponse.java index 3563b7b07b..6ca45f9264 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/ReactiveServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/ReactiveServerHttpResponse.java @@ -26,12 +26,11 @@ import org.springframework.http.ReactiveHttpOutputMessage; * * @author Arjen Poutsma */ -public interface ReactiveServerHttpResponse - extends ReactiveHttpOutputMessage { +public interface ReactiveServerHttpResponse extends ReactiveHttpOutputMessage { /** * Set the HTTP status code of the response. - * @param status the HTTP status as an HttpStatus enum value + * @param status the HTTP status as an {@link HttpStatus} enum value */ void setStatusCode(HttpStatus status); From 01ae961820768bfe9c1f948a74a37104dcdef992 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Fri, 30 Oct 2015 11:03:49 +0100 Subject: [PATCH 077/498] Make DefaultConversionService usable by the upcoming client --- .../support}/DefaultConversionService.java | 4 ++-- .../method/annotation/RequestMappingHandlerAdapter.java | 1 + .../dispatch/method/annotation/ResponseBodyResultHandler.java | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) rename spring-web-reactive/src/main/java/org/springframework/reactive/{web/dispatch/method/annotation => convert/support}/DefaultConversionService.java (92%) diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/DefaultConversionService.java b/spring-web-reactive/src/main/java/org/springframework/reactive/convert/support/DefaultConversionService.java similarity index 92% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/DefaultConversionService.java rename to spring-web-reactive/src/main/java/org/springframework/reactive/convert/support/DefaultConversionService.java index f38d00a063..a3e345f1f1 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/DefaultConversionService.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/convert/support/DefaultConversionService.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.web.dispatch.method.annotation; +package org.springframework.reactive.convert.support; import reactor.core.publisher.convert.DependencyUtils; @@ -28,7 +28,7 @@ import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converte * TODO temporary class designed to be replaced by org.springframework.core.convert.support.DefaultConversionService when it will contain Reactive Streams converter * @author Sebastien Deleuze */ -class DefaultConversionService extends GenericConversionService { +public class DefaultConversionService extends GenericConversionService { public DefaultConversionService() { addDefaultConverters(this); diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java index 3ffa44d83c..ad2bfef86e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java @@ -29,6 +29,7 @@ import org.springframework.reactive.codec.decoder.ByteToMessageDecoder; import org.springframework.reactive.codec.decoder.JacksonJsonDecoder; import org.springframework.reactive.codec.decoder.JsonObjectDecoder; import org.springframework.reactive.codec.decoder.StringDecoder; +import org.springframework.reactive.convert.support.DefaultConversionService; import org.springframework.reactive.web.dispatch.HandlerAdapter; import org.springframework.reactive.web.dispatch.HandlerResult; import org.springframework.reactive.web.dispatch.method.HandlerMethodArgumentResolver; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java index 0bfd9cdd47..43b189e299 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java @@ -35,6 +35,7 @@ import org.springframework.http.MediaType; import org.springframework.http.server.ReactiveServerHttpRequest; import org.springframework.http.server.ReactiveServerHttpResponse; import org.springframework.reactive.codec.encoder.MessageToByteEncoder; +import org.springframework.reactive.convert.support.DefaultConversionService; import org.springframework.reactive.web.dispatch.HandlerResult; import org.springframework.reactive.web.dispatch.HandlerResultHandler; import org.springframework.web.bind.annotation.ResponseBody; From 6b73993a389147bf5010d73c68eacccf11ca9cb5 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 30 Oct 2015 15:02:54 -0400 Subject: [PATCH 078/498] HandlerAdapter is asynchronous again --- .../web/dispatch/DispatcherHandler.java | 19 +++++++-------- .../reactive/web/dispatch/HandlerAdapter.java | 5 +++- .../dispatch/handler/HttpHandlerAdapter.java | 7 ++++-- .../RequestMappingHandlerAdapter.java | 24 ++++++++++++------- ...mpleUrlHandlerMappingIntegrationTests.java | 4 ++++ .../RequestMappingHandlerMappingTests.java | 4 ++-- 6 files changed, 40 insertions(+), 23 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/DispatcherHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/DispatcherHandler.java index 89c372946e..c134df5171 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/DispatcherHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/DispatcherHandler.java @@ -108,21 +108,21 @@ public class DispatcherHandler implements HttpHandler, ApplicationContextAware { } HandlerAdapter handlerAdapter = getHandlerAdapter(handler); + if (handlerAdapter == null) { + return Publishers.error(new IllegalStateException("No HandlerAdapter for " + handler)); + } - try { - HandlerResult result = handlerAdapter.handle(request, response, handler); + Publisher resultPublisher = handlerAdapter.handle(request, response, handler); + + return Publishers.concatMap(resultPublisher, result -> { for (HandlerResultHandler resultHandler : resultHandlers) { if (resultHandler.supports(result)) { return resultHandler.handleResult(request, response, result); } } return Publishers.error(new IllegalStateException( - "No HandlerResultHandler for " + result.getValue())); - } - catch(Exception ex) { - return Publishers.error(ex); - } - + "No HandlerResultHandler for " + result.getValue())); + }); } protected Object getHandler(ReactiveServerHttpRequest request) { @@ -142,8 +142,7 @@ public class DispatcherHandler implements HttpHandler, ApplicationContextAware { return handlerAdapter; } } - // more specific exception - throw new IllegalStateException("No HandlerAdapter for " + handler); + return null; } } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerAdapter.java index 91d9420412..131a79b826 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerAdapter.java @@ -16,6 +16,8 @@ package org.springframework.reactive.web.dispatch; +import org.reactivestreams.Publisher; + import org.springframework.http.server.ReactiveServerHttpRequest; import org.springframework.http.server.ReactiveServerHttpResponse; @@ -53,6 +55,7 @@ public interface HandlerAdapter { * @throws Exception in case of errors * @return An {@link HandlerResult} instance */ - HandlerResult handle(ReactiveServerHttpRequest request, ReactiveServerHttpResponse response, Object handler) throws Exception; + Publisher handle(ReactiveServerHttpRequest request, + ReactiveServerHttpResponse response, Object handler); } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/handler/HttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/handler/HttpHandlerAdapter.java index 67c563355a..2204cd85fe 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/handler/HttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/handler/HttpHandlerAdapter.java @@ -17,6 +17,7 @@ package org.springframework.reactive.web.dispatch.handler; import org.reactivestreams.Publisher; +import reactor.Publishers; import org.springframework.http.server.ReactiveServerHttpRequest; import org.springframework.http.server.ReactiveServerHttpResponse; @@ -44,10 +45,12 @@ public class HttpHandlerAdapter implements HandlerAdapter { } @Override - public HandlerResult handle(ReactiveServerHttpRequest request, ReactiveServerHttpResponse response, Object handler) { + public Publisher handle(ReactiveServerHttpRequest request, + ReactiveServerHttpResponse response, Object handler) { + HttpHandler httpHandler = (HttpHandler)handler; Publisher completion = httpHandler.handle(request, response); - return new HandlerResult(httpHandler, completion); + return Publishers.just(new HandlerResult(httpHandler, completion)); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java index ad2bfef86e..e4b1f98579 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java @@ -21,6 +21,9 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import org.reactivestreams.Publisher; +import reactor.Publishers; + import org.springframework.beans.factory.InitializingBean; import org.springframework.http.server.ReactiveServerHttpRequest; import org.springframework.http.server.ReactiveServerHttpResponse; @@ -70,15 +73,20 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Initializin } @Override - public HandlerResult handle(ReactiveServerHttpRequest request, ReactiveServerHttpResponse response, - Object handler) throws Exception { + public Publisher handle(ReactiveServerHttpRequest request, + ReactiveServerHttpResponse response, Object handler) { - final InvocableHandlerMethod invocable = new InvocableHandlerMethod((HandlerMethod) handler); - invocable.setHandlerMethodArgumentResolvers(this.argumentResolvers); + InvocableHandlerMethod handlerMethod = new InvocableHandlerMethod((HandlerMethod) handler); + handlerMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers); - Object result = invocable.invokeForRequest(request); - - return new HandlerResult(invocable, result); + try { + Object result = handlerMethod.invokeForRequest(request); + return Publishers.just(new HandlerResult(handlerMethod, result)); + } + catch (Exception e) { + // TODO: remove throws declaration from InvocableHandlerMethod + return Publishers.error(e); + } } -} +} \ No newline at end of file diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/handler/SimpleUrlHandlerMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/handler/SimpleUrlHandlerMappingIntegrationTests.java index 70ca1c7b5c..34353f8bd4 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/handler/SimpleUrlHandlerMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/handler/SimpleUrlHandlerMappingIntegrationTests.java @@ -26,6 +26,7 @@ import org.reactivestreams.Publisher; import reactor.io.buffer.Buffer; import reactor.rx.Streams; +import org.springframework.http.HttpStatus; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; import org.springframework.http.server.ReactiveServerHttpRequest; @@ -38,6 +39,7 @@ import org.springframework.web.client.RestTemplate; import org.springframework.web.context.support.StaticWebApplicationContext; import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; /** @@ -71,6 +73,7 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler RequestEntity request = RequestEntity.get(url).build(); ResponseEntity response = restTemplate.exchange(request, byte[].class); + assertEquals(HttpStatus.OK, response.getStatusCode()); assertArrayEquals("foo".getBytes(UTF_8), response.getBody()); } @@ -83,6 +86,7 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler RequestEntity request = RequestEntity.get(url).build(); ResponseEntity response = restTemplate.exchange(request, byte[].class); + assertEquals(HttpStatus.OK, response.getStatusCode()); assertArrayEquals("bar".getBytes(UTF_8), response.getBody()); } diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerMappingTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerMappingTests.java index 0d53aac9bf..9ff435f2b6 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerMappingTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerMappingTests.java @@ -52,14 +52,14 @@ public class RequestMappingHandlerMappingTests { } @Test - public void path() throws NoSuchMethodException { + public void path() throws Exception { ReactiveServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, "boo"); HandlerMethod handler = (HandlerMethod) this.mapping.getHandler(request); assertEquals(TestController.class.getMethod("boo"), handler.getMethod()); } @Test - public void method() throws NoSuchMethodException { + public void method() throws Exception { ReactiveServerHttpRequest request = new MockServerHttpRequest(HttpMethod.POST, "foo"); HandlerMethod handler = (HandlerMethod) this.mapping.getHandler(request); assertEquals(TestController.class.getMethod("postFoo"), handler.getMethod()); From 0989c8b3c24b94b6dc3eeb8e60f68b80be32fb14 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 30 Oct 2015 16:04:49 -0400 Subject: [PATCH 079/498] HandlerMethodArgumentResolver is now asynchronous --- .../method/HandlerMethodArgumentResolver.java | 10 +- .../method/InvocableHandlerMethod.java | 103 +++++++++++++----- .../RequestBodyArgumentResolver.java | 13 +-- .../RequestMappingHandlerAdapter.java | 10 +- .../RequestParamArgumentResolver.java | 10 +- .../RequestMappingIntegrationTests.java | 12 ++ 6 files changed, 110 insertions(+), 48 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/HandlerMethodArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/HandlerMethodArgumentResolver.java index f09fa42830..a7b5b16f95 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/HandlerMethodArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/HandlerMethodArgumentResolver.java @@ -16,6 +16,8 @@ package org.springframework.reactive.web.dispatch.method; +import org.reactivestreams.Publisher; + import org.springframework.core.MethodParameter; import org.springframework.http.server.ReactiveServerHttpRequest; @@ -27,6 +29,12 @@ public interface HandlerMethodArgumentResolver { boolean supportsParameter(MethodParameter parameter); - Object resolveArgument(MethodParameter parameter, ReactiveServerHttpRequest request); + /** + * The returned Publisher must produce a single value. As Reactive Streams + * does not allow publishing null values, if the value may be {@code null} + * use {@link java.util.Optional#ofNullable(Object)} to wrap it. + */ + Publisher resolveArgument(MethodParameter parameter, ReactiveServerHttpRequest request); + } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/InvocableHandlerMethod.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/InvocableHandlerMethod.java index 7507677180..c409425f7b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/InvocableHandlerMethod.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/InvocableHandlerMethod.java @@ -19,8 +19,14 @@ package org.springframework.reactive.web.dispatch.method; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; -import java.util.Arrays; +import java.util.Collections; import java.util.List; +import java.util.Optional; + +import org.reactivestreams.Publisher; +import reactor.Publishers; +import reactor.fn.tuple.Tuple; +import reactor.rx.Streams; import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.GenericTypeResolver; @@ -32,9 +38,6 @@ import org.springframework.web.method.HandlerMethod; /** - * 90% overlap with the existing one in spring-web except for the different - * HandlerMethodArgumentResolver contract. - * * @author Rossen Stoyanchev */ public class InvocableHandlerMethod extends HandlerMethod { @@ -55,53 +58,74 @@ public class InvocableHandlerMethod extends HandlerMethod { } - public Object invokeForRequest(ReactiveServerHttpRequest request, Object... providedArgs) throws Exception { - Object[] args = getMethodArgumentValues(request, providedArgs); - if (logger.isTraceEnabled()) { - logger.trace("Invoking [" + getBeanType().getSimpleName() + "." + - getMethod().getName() + "] method with arguments " + Arrays.asList(args)); - } - Object returnValue = doInvoke(args); - if (logger.isTraceEnabled()) { - logger.trace("Method [" + getMethod().getName() + "] returned [" + returnValue + "]"); - } - return returnValue; + public Publisher invokeForRequest(ReactiveServerHttpRequest request, + Object... providedArgs) { + + List> argPublishers = getMethodArguments(request, providedArgs); + + Publisher argValues = (!argPublishers.isEmpty() ? + Streams.zip(argPublishers, this::unwrapOptionalArgValues) : Publishers.just(new Object[0])); + + return Publishers.map(argValues, args -> { + if (logger.isTraceEnabled()) { + logger.trace("Invoking [" + getBeanType().getSimpleName() + "." + + getMethod().getName() + "] method with arguments " + + Collections.singletonList(argPublishers)); + } + Object returnValue = null; + try { + returnValue = doInvoke(args); + if (logger.isTraceEnabled()) { + logger.trace("Method [" + getMethod().getName() + "] returned [" + returnValue + "]"); + } + } + catch (Exception ex) { + // TODO: how to best handle error inside map? (also wrapping hides original ex) + throw new IllegalStateException(ex); + } + return returnValue; + }); } - private Object[] getMethodArgumentValues(ReactiveServerHttpRequest request, Object... providedArgs) throws Exception { + private List> getMethodArguments(ReactiveServerHttpRequest request, + Object... providedArgs) { + MethodParameter[] parameters = getMethodParameters(); - Object[] args = new Object[parameters.length]; + List> valuePublishers = new ArrayList<>(parameters.length); for (int i = 0; i < parameters.length; i++) { MethodParameter parameter = parameters[i]; parameter.initParameterNameDiscovery(this.parameterNameDiscoverer); GenericTypeResolver.resolveParameterType(parameter, getBean().getClass()); - args[i] = resolveProvidedArgument(parameter, providedArgs); - if (args[i] != null) { + Object value = resolveProvidedArgument(parameter, providedArgs); + if (value != null) { + valuePublishers.add(Publishers.just(value)); continue; } + boolean resolved = false; for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) { if (resolver.supportsParameter(parameter)) { try { - args[i] = resolver.resolveArgument(parameter, request); + valuePublishers.add(resolver.resolveArgument(parameter, request)); + resolved = true; break; } catch (Exception ex) { - if (logger.isDebugEnabled()) { - logger.debug(getArgumentResolutionErrorMessage("Error resolving argument", i), ex); - } - throw ex; + String msg = buildArgErrorMessage("Error resolving argument", i); + valuePublishers.add(Publishers.error(new IllegalStateException(msg, ex))); + break; } } } - if (args[i] == null) { - String msg = getArgumentResolutionErrorMessage("No suitable resolver for argument", i); - throw new IllegalStateException(msg); + if (!resolved) { + String msg = buildArgErrorMessage("No suitable resolver for argument", i); + valuePublishers.add(Publishers.error(new IllegalStateException(msg))); + break; } } - return args; + return valuePublishers; } - private String getArgumentResolutionErrorMessage(String message, int index) { + private String buildArgErrorMessage(String message, int index) { MethodParameter param = getMethodParameters()[index]; message += " [" + index + "] [type=" + param.getParameterType().getName() + "]"; return getDetailedErrorMessage(message); @@ -125,6 +149,27 @@ public class InvocableHandlerMethod extends HandlerMethod { return null; } + private void unwrapOptionalArgValues(Object[] args) { + for (int i = 0; i < args.length; i++) { + if (args[i] instanceof Optional) { + Optional optional = (Optional) args[i]; + args[i] = optional.isPresent() ? optional.get() : null; + } + } + } + + private Object[] unwrapOptionalArgValues(Tuple tuple) { + Object[] args = new Object[tuple.size()]; + for (int i = 0; i < tuple.size(); i++) { + args[i] = tuple.get(i); + if (args[i] instanceof Optional) { + Optional optional = (Optional) args[i]; + args[i] = optional.isPresent() ? optional.get() : null; + } + } + return args; + } + protected Object doInvoke(Object... args) throws Exception { ReflectionUtils.makeAccessible(getBridgedMethod()); try { diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java index e14be9ee21..8a0119c569 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java @@ -23,6 +23,7 @@ import java.util.Collections; import java.util.List; import org.reactivestreams.Publisher; +import reactor.Publishers; import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; @@ -66,9 +67,7 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve } @Override - @SuppressWarnings("unchecked") - public Object resolveArgument(MethodParameter parameter, ReactiveServerHttpRequest request) { - + public Publisher resolveArgument(MethodParameter parameter, ReactiveServerHttpRequest request) { MediaType mediaType = resolveMediaType(request); ResolvableType type = ResolvableType.forMethodParameter(parameter); List hints = new ArrayList<>(); @@ -85,12 +84,10 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve } elementStream = deserializer.decode(inputStream, elementType, mediaType, hints.toArray()); } - if (conversionService.canConvert(Publisher.class, type.getRawClass())) { - return conversionService.convert(elementStream, type.getRawClass()); - } - else { - return elementStream; + if (this.conversionService.canConvert(Publisher.class, type.getRawClass())) { + return Publishers.just(this.conversionService.convert(elementStream, type.getRawClass())); } + return Publishers.map(elementStream, element -> element); } private MediaType resolveMediaType(ReactiveServerHttpRequest request) { diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java index e4b1f98579..2b89f9f7ed 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java @@ -79,14 +79,8 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Initializin InvocableHandlerMethod handlerMethod = new InvocableHandlerMethod((HandlerMethod) handler); handlerMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers); - try { - Object result = handlerMethod.invokeForRequest(request); - return Publishers.just(new HandlerResult(handlerMethod, result)); - } - catch (Exception e) { - // TODO: remove throws declaration from InvocableHandlerMethod - return Publishers.error(e); - } + Publisher resultPublisher = handlerMethod.invokeForRequest(request); + return Publishers.map(resultPublisher, result -> new HandlerResult(handlerMethod, result)); } } \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestParamArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestParamArgumentResolver.java index ab7fa92d85..124f840b13 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestParamArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestParamArgumentResolver.java @@ -17,6 +17,11 @@ package org.springframework.reactive.web.dispatch.method.annotation; +import java.util.Optional; + +import org.reactivestreams.Publisher; +import reactor.Publishers; + import org.springframework.core.MethodParameter; import org.springframework.http.server.ReactiveServerHttpRequest; import org.springframework.reactive.web.dispatch.method.HandlerMethodArgumentResolver; @@ -40,11 +45,12 @@ public class RequestParamArgumentResolver implements HandlerMethodArgumentResolv @Override - public Object resolveArgument(MethodParameter param, ReactiveServerHttpRequest request) { + public Publisher resolveArgument(MethodParameter param, ReactiveServerHttpRequest request) { RequestParam annotation = param.getParameterAnnotation(RequestParam.class); String name = (annotation.value().length() != 0 ? annotation.value() : param.getParameterName()); UriComponents uriComponents = UriComponentsBuilder.fromUri(request.getURI()).build(); - return uriComponents.getQueryParams().getFirst(name); + String value = uriComponents.getQueryParams().getFirst(name); + return Publishers.just(Optional.ofNullable(value)); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingIntegrationTests.java index e46dc4ff46..0b3544740c 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingIntegrationTests.java @@ -180,6 +180,11 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati capitalizeCollection("http://localhost:" + port + "/stream-capitalize"); } + @Test + public void personCapitalize() throws Exception { + capitalizePojo("http://localhost:" + port + "/person-capitalize"); + } + @Test public void completableFutureCapitalize() throws Exception { capitalizePojo("http://localhost:" + port + "/completable-future-capitalize"); @@ -376,6 +381,13 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati }); } + @RequestMapping("/person-capitalize") + @ResponseBody + public Person personCapitalize(@RequestBody Person person) { + person.setName(person.getName().toUpperCase()); + return person; + } + @RequestMapping("/completable-future-capitalize") @ResponseBody public CompletableFuture completableFutureCapitalize(@RequestBody CompletableFuture personFuture) { From c0dff3d2bb2277429f4e387d18deb75b0797a375 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 30 Oct 2015 17:22:19 -0400 Subject: [PATCH 080/498] Comply with Spring Framework code style https://github.com/spring-projects/spring-framework/wiki/Spring-Framework-Code-Style --- ...veStreamsToCompletableFutureConverter.java | 22 +-- .../ReactiveStreamsToReactorConverter.java | 36 ++-- .../ReactiveStreamsToRxJava1Converter.java | 38 ++-- .../http/ReactiveHttpInputMessage.java | 8 +- .../http/ReactiveHttpOutputMessage.java | 14 +- .../codec/decoder/ByteBufferDecoder.java | 4 +- .../codec/decoder/ByteToMessageDecoder.java | 3 +- .../codec/decoder/JacksonJsonDecoder.java | 11 +- .../reactive/codec/decoder/Jaxb2Decoder.java | 24 +-- .../codec/decoder/JsonObjectDecoder.java | 164 +++++++++--------- .../reactive/codec/decoder/StringDecoder.java | 4 +- .../codec/encoder/ByteBufferEncoder.java | 7 +- .../codec/encoder/JacksonJsonEncoder.java | 4 +- .../reactive/codec/encoder/Jaxb2Encoder.java | 33 ++-- .../codec/encoder/JsonObjectEncoder.java | 43 +++-- .../codec/encoder/MessageToByteEncoder.java | 3 +- .../reactive/codec/encoder/StringEncoder.java | 4 +- .../io/ByteBufferPublisherInputStream.java | 3 +- .../web/dispatch/DispatcherHandler.java | 13 +- .../reactive/web/dispatch/HandlerAdapter.java | 1 - .../web/dispatch/HandlerResultHandler.java | 3 +- .../dispatch/SimpleHandlerResultHandler.java | 13 +- .../method/InvocableHandlerMethod.java | 6 +- .../RequestBodyArgumentResolver.java | 40 +++-- .../RequestMappingHandlerAdapter.java | 13 +- .../RequestMappingHandlerMapping.java | 24 ++- .../annotation/ResponseBodyResultHandler.java | 53 +++--- .../rxnetty/RxNettyServerHttpResponse.java | 7 +- .../web/http/servlet/HttpHandlerServlet.java | 12 +- .../servlet/ServletServerHttpRequest.java | 8 +- .../servlet/ServletServerHttpResponse.java | 36 ++-- .../RequestMappingIntegrationTests.java | 65 ++++--- .../ResponseBodyResultHandlerTests.java | 14 +- 33 files changed, 420 insertions(+), 313 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToCompletableFutureConverter.java b/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToCompletableFutureConverter.java index 046ab43a74..dcb17c6de2 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToCompletableFutureConverter.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToCompletableFutureConverter.java @@ -32,20 +32,22 @@ public class ReactiveStreamsToCompletableFutureConverter implements GenericConve @Override public Set getConvertibleTypes() { - Set convertibleTypes = new LinkedHashSet<>(); - convertibleTypes.add(new GenericConverter.ConvertiblePair(Publisher.class, CompletableFuture.class)); - convertibleTypes.add(new GenericConverter.ConvertiblePair(CompletableFuture.class, Publisher.class)); - return convertibleTypes; + Set pairs = new LinkedHashSet<>(); + pairs.add(new GenericConverter.ConvertiblePair(Publisher.class, CompletableFuture.class)); + pairs.add(new GenericConverter.ConvertiblePair(CompletableFuture.class, Publisher.class)); + return pairs; } @Override public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { - if (source != null) { - if (CompletableFuture.class.isAssignableFrom(source.getClass())) { - return reactor.core.publisher.convert.CompletableFutureConverter.from((CompletableFuture)source); - } else if (CompletableFuture.class.isAssignableFrom(targetType.getResolvableType().getRawClass())) { - return reactor.core.publisher.convert.CompletableFutureConverter.fromSingle((Publisher)source); - } + if (source == null) { + return null; + } + else if (CompletableFuture.class.isAssignableFrom(source.getClass())) { + return reactor.core.publisher.convert.CompletableFutureConverter.from((CompletableFuture) source); + } + else if (CompletableFuture.class.isAssignableFrom(targetType.getResolvableType().getRawClass())) { + return reactor.core.publisher.convert.CompletableFutureConverter.fromSingle((Publisher) source); } return null; } diff --git a/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToReactorConverter.java b/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToReactorConverter.java index 411e0ab42a..4a59deb976 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToReactorConverter.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToReactorConverter.java @@ -35,26 +35,30 @@ public final class ReactiveStreamsToReactorConverter implements GenericConverter @Override public Set getConvertibleTypes() { - Set convertibleTypes = new LinkedHashSet<>(); - convertibleTypes.add(new GenericConverter.ConvertiblePair(Publisher.class, Stream.class)); - convertibleTypes.add(new GenericConverter.ConvertiblePair(Stream.class, Publisher.class)); - convertibleTypes.add(new GenericConverter.ConvertiblePair(Publisher.class, Promise.class)); - convertibleTypes.add(new GenericConverter.ConvertiblePair(Promise.class, Publisher.class)); - return convertibleTypes; + Set pairs = new LinkedHashSet<>(); + pairs.add(new GenericConverter.ConvertiblePair(Publisher.class, Stream.class)); + pairs.add(new GenericConverter.ConvertiblePair(Stream.class, Publisher.class)); + pairs.add(new GenericConverter.ConvertiblePair(Publisher.class, Promise.class)); + pairs.add(new GenericConverter.ConvertiblePair(Promise.class, Publisher.class)); + return pairs; } @Override public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { - if (source != null) { - if (Stream.class.isAssignableFrom(source.getClass())) { - return source; - } else if (Stream.class.isAssignableFrom(targetType.getResolvableType().getRawClass())) { - return Streams.wrap((Publisher)source); - } else if (Promise.class.isAssignableFrom(source.getClass())) { - return ((Promise)source); - } else if (Promise.class.isAssignableFrom(targetType.getResolvableType().getRawClass())) { - return Streams.wrap((Publisher)source).next(); - } + if (source == null) { + return null; + } + if (Stream.class.isAssignableFrom(source.getClass())) { + return source; + } + else if (Stream.class.isAssignableFrom(targetType.getResolvableType().getRawClass())) { + return Streams.wrap((Publisher)source); + } + else if (Promise.class.isAssignableFrom(source.getClass())) { + return source; + } + else if (Promise.class.isAssignableFrom(targetType.getResolvableType().getRawClass())) { + return Streams.wrap((Publisher)source).next(); } return null; } diff --git a/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToRxJava1Converter.java b/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToRxJava1Converter.java index b82d4742c8..546d49f560 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToRxJava1Converter.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToRxJava1Converter.java @@ -36,28 +36,30 @@ public final class ReactiveStreamsToRxJava1Converter implements GenericConverter @Override public Set getConvertibleTypes() { - Set convertibleTypes = new LinkedHashSet<>(); - convertibleTypes.add(new GenericConverter.ConvertiblePair(Publisher.class, Observable.class)); - convertibleTypes.add(new GenericConverter.ConvertiblePair(Observable.class, Publisher.class)); - convertibleTypes.add(new GenericConverter.ConvertiblePair(Publisher.class, Single.class)); - convertibleTypes.add(new GenericConverter.ConvertiblePair(Single.class, Publisher.class)); - return convertibleTypes; + Set pairs = new LinkedHashSet<>(); + pairs.add(new GenericConverter.ConvertiblePair(Publisher.class, Observable.class)); + pairs.add(new GenericConverter.ConvertiblePair(Observable.class, Publisher.class)); + pairs.add(new GenericConverter.ConvertiblePair(Publisher.class, Single.class)); + pairs.add(new GenericConverter.ConvertiblePair(Single.class, Publisher.class)); + return pairs; } @Override public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { - if (source != null) { - if (Observable.class.isAssignableFrom(source.getClass())) { - return RxJava1Converter.from((Observable) source); - } - else if (Observable.class.isAssignableFrom(targetType.getResolvableType().getRawClass())) { - return RxJava1Converter.from((Publisher)source); - } - else if (Single.class.isAssignableFrom(source.getClass())) { - return reactor.core.publisher.convert.RxJava1SingleConverter.from((Single) source); - } else if (Single.class.isAssignableFrom(targetType.getResolvableType().getRawClass())) { - return reactor.core.publisher.convert.RxJava1SingleConverter.from((Publisher)source); - } + if (source == null) { + return null; + } + if (Observable.class.isAssignableFrom(source.getClass())) { + return RxJava1Converter.from((Observable) source); + } + else if (Observable.class.isAssignableFrom(targetType.getResolvableType().getRawClass())) { + return RxJava1Converter.from((Publisher) source); + } + else if (Single.class.isAssignableFrom(source.getClass())) { + return reactor.core.publisher.convert.RxJava1SingleConverter.from((Single) source); + } + else if (Single.class.isAssignableFrom(targetType.getResolvableType().getRawClass())) { + return reactor.core.publisher.convert.RxJava1SingleConverter.from((Publisher) source); } return null; } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpInputMessage.java b/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpInputMessage.java index 8cd4535b6b..7870571b4a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpInputMessage.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpInputMessage.java @@ -21,10 +21,12 @@ import java.nio.ByteBuffer; import org.reactivestreams.Publisher; /** - * Represents a "reactive" HTTP input message, consisting of {@linkplain #getHeaders() headers} - * and a readable {@linkplain #getBody() streaming body }. + * Represents a "reactive" HTTP input message, consisting of + * {@linkplain #getHeaders() headers} and a readable + * {@linkplain #getBody() streaming body }. * - *

    Typically implemented by an HTTP request on the server-side, or a response on the client-side. + *

    Typically implemented by an HTTP request on the server-side, or a response + * on the client-side. * * @author Arjen Poutsma */ diff --git a/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java b/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java index 674268f6cd..33df2dcdee 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java @@ -21,19 +21,21 @@ import java.nio.ByteBuffer; import org.reactivestreams.Publisher; /** - * Represents a "reactive" HTTP output message, consisting of {@linkplain #getHeaders() headers} - * and the capability to add a {@linkplain #setBody(Publisher) body}. + * Represents a "reactive" HTTP output message, consisting of + * {@linkplain #getHeaders() headers} and the capability to add a + * {@linkplain #setBody(Publisher) body}. * - *

    Typically implemented by an HTTP request on the client-side, or a response on the server-side. + *

    Typically implemented by an HTTP request on the client-side, or a response + * on the server-side. * * @author Arjen Poutsma */ public interface ReactiveHttpOutputMessage extends HttpMessage { /** - * Sets the body of this message to the given publisher of {@link ByteBuffer}s. The - * publisher will be used to write to the underlying HTTP layer with asynchronously, - * given pull demand by this layer. + * Sets the body of this message to the given publisher of {@link ByteBuffer}s. + * The publisher will be used to write to the underlying HTTP layer with + * asynchronously, given pull demand by this layer. * * @param body the body to use * @return a publisher that indicates completion diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/ByteBufferDecoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/ByteBufferDecoder.java index b740dab95b..33bef171d0 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/ByteBufferDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/ByteBufferDecoder.java @@ -34,7 +34,9 @@ public class ByteBufferDecoder implements ByteToMessageDecoder { } @Override - public Publisher decode(Publisher inputStream, ResolvableType type, MediaType mediaType, Object... hints) { + public Publisher decode(Publisher inputStream, ResolvableType type, + MediaType mediaType, Object... hints) { + return inputStream; } } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/ByteToMessageDecoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/ByteToMessageDecoder.java index 3be8b83dae..6c1fed1995 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/ByteToMessageDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/ByteToMessageDecoder.java @@ -49,6 +49,7 @@ public interface ByteToMessageDecoder { * @param hints Additional information about how to do decode, optional. * @return the decoded message stream */ - Publisher decode(Publisher inputStream, ResolvableType type, MediaType mediaType, Object... hints); + Publisher decode(Publisher inputStream, ResolvableType type, + MediaType mediaType, Object... hints); } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoder.java index 3b98fbfa5b..00a3241461 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoder.java @@ -40,6 +40,7 @@ public class JacksonJsonDecoder implements ByteToMessageDecoder { private final ObjectMapper mapper; + public JacksonJsonDecoder() { this(new ObjectMapper()); } @@ -48,18 +49,22 @@ public class JacksonJsonDecoder implements ByteToMessageDecoder { this.mapper = mapper; } + @Override public boolean canDecode(ResolvableType type, MediaType mediaType, Object... hints) { return mediaType.isCompatibleWith(MediaType.APPLICATION_JSON); } @Override - public Publisher decode(Publisher inputStream, ResolvableType type, MediaType mediaType, Object... hints) { - ObjectReader reader = mapper.readerFor(type.getRawClass()); + public Publisher decode(Publisher inputStream, ResolvableType type, + MediaType mediaType, Object... hints) { + + ObjectReader reader = this.mapper.readerFor(type.getRawClass()); return Publishers.map(inputStream, chunk -> { try { return reader.readValue(new ByteBufferInputStream(chunk)); - } catch (IOException e) { + } + catch (IOException e) { throw new CodecException("Error while reading the data", e); } }); diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/Jaxb2Decoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/Jaxb2Decoder.java index c1c0dc1883..204483481d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/Jaxb2Decoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/Jaxb2Decoder.java @@ -51,15 +51,19 @@ import org.springframework.util.Assert; */ public class Jaxb2Decoder implements ByteToMessageDecoder { - private final ConcurrentMap, JAXBContext> jaxbContexts = new ConcurrentHashMap, JAXBContext>(64); + private final ConcurrentMap, JAXBContext> jaxbContexts = new ConcurrentHashMap<>(64); + @Override public boolean canDecode(ResolvableType type, MediaType mediaType, Object... hints) { - return mediaType.isCompatibleWith(MediaType.APPLICATION_XML) || mediaType.isCompatibleWith(MediaType.TEXT_XML); + return (mediaType.isCompatibleWith(MediaType.APPLICATION_XML) || + mediaType.isCompatibleWith(MediaType.TEXT_XML)); } @Override - public Publisher decode(Publisher inputStream, ResolvableType type, MediaType mediaType, Object... hints) { + public Publisher decode(Publisher inputStream, ResolvableType type, + MediaType mediaType, Object... hints) { + Class outputClass = type.getRawClass(); try { Source source = processSource(new StreamSource(new ByteBufferPublisherInputStream(inputStream))); @@ -77,7 +81,8 @@ public class Jaxb2Decoder implements ByteToMessageDecoder { new CodecException("Could not unmarshal to [" + outputClass + "]: " + ex.getMessage(), ex)); } catch (JAXBException ex) { - return Publishers.error(new CodecException("Could not instantiate JAXBContext: " + ex.getMessage(), ex)); + return Publishers.error(new CodecException("Could not instantiate JAXBContext: " + + ex.getMessage(), ex)); } } @@ -101,12 +106,11 @@ public class Jaxb2Decoder implements ByteToMessageDecoder { protected final Unmarshaller createUnmarshaller(Class clazz) throws JAXBException { try { JAXBContext jaxbContext = getJaxbContext(clazz); - Unmarshaller unmarshaller = jaxbContext.createUnmarshaller(); - return unmarshaller; + return jaxbContext.createUnmarshaller(); } catch (JAXBException ex) { - throw new CodecException( - "Could not create Unmarshaller for class [" + clazz + "]: " + ex.getMessage(), ex); + throw new CodecException("Could not create Unmarshaller for class " + + "[" + clazz + "]: " + ex.getMessage(), ex); } } @@ -119,8 +123,8 @@ public class Jaxb2Decoder implements ByteToMessageDecoder { this.jaxbContexts.putIfAbsent(clazz, jaxbContext); } catch (JAXBException ex) { - throw new CodecException( - "Could not instantiate JAXBContext for class [" + clazz + "]: " + ex.getMessage(), ex); + throw new CodecException("Could not instantiate JAXBContext for class " + + "[" + clazz + "]: " + ex.getMessage(), ex); } } return jaxbContext; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JsonObjectDecoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JsonObjectDecoder.java index e3346d9793..7a2b0bc4c2 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JsonObjectDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JsonObjectDecoder.java @@ -32,13 +32,15 @@ import org.springframework.http.MediaType; import org.springframework.reactive.codec.encoder.JsonObjectEncoder; /** - * Decode an arbitrary split byte stream representing JSON objects to a byte stream - * where each chunk is a well-formed JSON object. + * Decode an arbitrary split byte stream representing JSON objects to a byte + * stream where each chunk is a well-formed JSON object. * - * This class does not do any real parsing or validation. A sequence of bytes is considered a JSON object/array - * if it contains a matching number of opening and closing braces/brackets. + * This class does not do any real parsing or validation. A sequence of byte + * is considered a JSON object/array if it contains a matching number of opening + * and closing braces/brackets. * - * Based on Netty {@code JsonObjectDecoder} + * Based on + * Netty {@code JsonObjectDecoder} * * @author Sebastien Deleuze * @see JsonObjectEncoder @@ -46,13 +48,19 @@ import org.springframework.reactive.codec.encoder.JsonObjectEncoder; public class JsonObjectDecoder implements ByteToMessageDecoder { private static final int ST_CORRUPTED = -1; + private static final int ST_INIT = 0; + private static final int ST_DECODING_NORMAL = 1; + private static final int ST_DECODING_ARRAY_STREAM = 2; + private final int maxObjectLength; + private final boolean streamArrayElements; + public JsonObjectDecoder() { // 1 MB this(1024 * 1024); @@ -66,14 +74,15 @@ public class JsonObjectDecoder implements ByteToMessageDecoder { this(1024 * 1024, streamArrayElements); } + /** - * @param maxObjectLength maximum number of bytes a JSON object/array may use (including braces and all). - * Objects exceeding this length are dropped and an {@link IllegalStateException} - * is thrown. - * @param streamArrayElements if set to true and the "top level" JSON object is an array, each of its entries - * is passed through the pipeline individually and immediately after it was fully - * received, allowing for arrays with "infinitely" many elements. - * + * @param maxObjectLength maximum number of bytes a JSON object/array may + * use (including braces and all). Objects exceeding this length are dropped + * and an {@link IllegalStateException} is thrown. + * @param streamArrayElements if set to true and the "top level" JSON object + * is an array, each of its entries is passed through the pipeline individually + * and immediately after it was fully received, allowing for arrays with + * "infinitely" many elements. */ public JsonObjectDecoder(int maxObjectLength, boolean streamArrayElements) { if (maxObjectLength < 1) { @@ -90,91 +99,89 @@ public class JsonObjectDecoder implements ByteToMessageDecoder { } @Override - public Publisher decode(Publisher inputStream, ResolvableType type, MediaType mediaType, Object... hints) { + public Publisher decode(Publisher inputStream, ResolvableType type, + MediaType mediaType, Object... hints) { return Publishers.flatMap(inputStream, new Function>() { int openBraces; - int idx; + int index; int state; boolean insideString; - ByteBuf in; - Integer wrtIdx; + ByteBuf input; + Integer writerIndex; @Override public Publisher apply(ByteBuffer b) { List chunks = new ArrayList<>(); - - if (in == null) { - in = Unpooled.copiedBuffer(b); - wrtIdx = in.writerIndex(); + if (this.input == null) { + this.input = Unpooled.copiedBuffer(b); + this.writerIndex = this.input.writerIndex(); } else { - in = Unpooled.copiedBuffer(in, Unpooled.copiedBuffer(b)); - wrtIdx = in.writerIndex(); + this.input = Unpooled.copiedBuffer(this.input, Unpooled.copiedBuffer(b)); + this.writerIndex = this.input.writerIndex(); } - if (state == ST_CORRUPTED) { - in.skipBytes(in.readableBytes()); + if (this.state == ST_CORRUPTED) { + this.input.skipBytes(this.input.readableBytes()); return Publishers.error(new IllegalStateException("Corrupted stream")); } - - if (wrtIdx > maxObjectLength) { + if (this.writerIndex > maxObjectLength) { // buffer size exceeded maxObjectLength; discarding the complete buffer. - in.skipBytes(in.readableBytes()); + this.input.skipBytes(this.input.readableBytes()); reset(); - return Publishers.error(new IllegalStateException( - "object length exceeds " + maxObjectLength + ": " + - wrtIdx + - " bytes discarded")); + return Publishers.error(new IllegalStateException("object length exceeds " + + maxObjectLength + ": " + this.writerIndex + " bytes discarded")); } - - for (/* use current idx */; idx < wrtIdx; idx++) { - byte c = in.getByte(idx); - if (state == ST_DECODING_NORMAL) { - decodeByte(c, in, idx); + for (/* use current index */; this.index < this.writerIndex; this.index++) { + byte c = this.input.getByte(this.index); + if (this.state == ST_DECODING_NORMAL) { + decodeByte(c, this.input, this.index); // All opening braces/brackets have been closed. That's enough to conclude // that the JSON object/array is complete. - if (openBraces == 0) { - ByteBuf json = extractObject(in, in.readerIndex(), - idx + 1 - in.readerIndex()); + if (this.openBraces == 0) { + ByteBuf json = extractObject(this.input, this.input.readerIndex(), + this.index + 1 - this.input.readerIndex()); if (json != null) { chunks.add(json.nioBuffer()); } // The JSON object/array was extracted => discard the bytes from // the input buffer. - in.readerIndex(idx + 1); + this.input.readerIndex(this.index + 1); // Reset the object state to get ready for the next JSON object/text // coming along the byte stream. reset(); } } - else if (state == ST_DECODING_ARRAY_STREAM) { - decodeByte(c, in, idx); + else if (this.state == ST_DECODING_ARRAY_STREAM) { + decodeByte(c, this.input, this.index); - if (!insideString && (openBraces == 1 && c == ',' || - openBraces == 0 && c == ']')) { + if (!this.insideString && (this.openBraces == 1 && c == ',' || + this.openBraces == 0 && c == ']')) { // skip leading spaces. No range check is needed and the loop will terminate - // because the byte at position idx is not a whitespace. - for (int i = in.readerIndex(); Character.isWhitespace(in.getByte(i)); i++) { - in.skipBytes(1); + // because the byte at position index is not a whitespace. + for (int i = this.input.readerIndex(); Character.isWhitespace(this.input.getByte(i)); i++) { + this.input.skipBytes(1); } // skip trailing spaces. - int idxNoSpaces = idx - 1; - while (idxNoSpaces >= in.readerIndex() && - Character.isWhitespace(in.getByte(idxNoSpaces))) { + int idxNoSpaces = this.index - 1; + while (idxNoSpaces >= this.input.readerIndex() && + Character.isWhitespace(this.input.getByte(idxNoSpaces))) { + idxNoSpaces--; } - ByteBuf json = extractObject(in, in.readerIndex(), - idxNoSpaces + 1 - in.readerIndex()); + ByteBuf json = extractObject(this.input, this.input.readerIndex(), + idxNoSpaces + 1 - this.input.readerIndex()); + if (json != null) { chunks.add(json.nioBuffer()); } - in.readerIndex(idx + 1); + this.input.readerIndex(this.index + 1); if (c == ']') { reset(); @@ -185,74 +192,73 @@ public class JsonObjectDecoder implements ByteToMessageDecoder { else if (c == '{' || c == '[') { initDecoding(c, streamArrayElements); - if (state == ST_DECODING_ARRAY_STREAM) { + if (this.state == ST_DECODING_ARRAY_STREAM) { // Discard the array bracket - in.skipBytes(1); + this.input.skipBytes(1); } // Discard leading spaces in front of a JSON object/array. } else if (Character.isWhitespace(c)) { - in.skipBytes(1); + this.input.skipBytes(1); } else { - state = ST_CORRUPTED; + this.state = ST_CORRUPTED; return Publishers.error(new IllegalStateException( - "invalid JSON received at byte position " + idx + - ": " + ByteBufUtil.hexDump(in))); + "invalid JSON received at byte position " + this.index + ": " + + ByteBufUtil.hexDump(this.input))); } } - if (in.readableBytes() == 0) { - idx = 0; + if (this.input.readableBytes() == 0) { + this.index = 0; } return Publishers.from(chunks); } /** - * Override this method if you want to filter the json objects/arrays that get passed through the pipeline. + * Override this method if you want to filter the json objects/arrays that + * get passed through the pipeline. */ @SuppressWarnings("UnusedParameters") protected ByteBuf extractObject(ByteBuf buffer, int index, int length) { return buffer.slice(index, length).retain(); } - private void decodeByte(byte c, ByteBuf in, int idx) { - if ((c == '{' || c == '[') && !insideString) { - openBraces++; + private void decodeByte(byte c, ByteBuf input, int index) { + if ((c == '{' || c == '[') && !this.insideString) { + this.openBraces++; } - else if ((c == '}' || c == ']') && !insideString) { - openBraces--; + else if ((c == '}' || c == ']') && !this.insideString) { + this.openBraces--; } else if (c == '"') { // start of a new JSON string. It's necessary to detect strings as they may // also contain braces/brackets and that could lead to incorrect results. - if (!insideString) { - insideString = true; + if (!this.insideString) { + this.insideString = true; // If the double quote wasn't escaped then this is the end of a string. } - else if (in.getByte(idx - 1) != '\\') { - insideString = false; + else if (input.getByte(index - 1) != '\\') { + this.insideString = false; } } } private void initDecoding(byte openingBrace, boolean streamArrayElements) { - openBraces = 1; + this.openBraces = 1; if (openingBrace == '[' && streamArrayElements) { - state = ST_DECODING_ARRAY_STREAM; + this.state = ST_DECODING_ARRAY_STREAM; } else { - state = ST_DECODING_NORMAL; + this.state = ST_DECODING_NORMAL; } } private void reset() { - insideString = false; - state = ST_INIT; - openBraces = 0; + this.insideString = false; + this.state = ST_INIT; + this.openBraces = 0; } - }); } - } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/StringDecoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/StringDecoder.java index 152cb6cf34..b5f44ef73d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/StringDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/StringDecoder.java @@ -47,7 +47,9 @@ public class StringDecoder implements ByteToMessageDecoder { } @Override - public Publisher decode(Publisher inputStream, ResolvableType type, MediaType mediaType, Object... hints) { + public Publisher decode(Publisher inputStream, ResolvableType type, + MediaType mediaType, Object... hints) { + Charset charset = HintUtils.getHintByClass(Charset.class, hints, DEFAULT_CHARSET); return Publishers.map(inputStream, chunk -> new String(new Buffer(chunk).asBytes(), charset)); } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/ByteBufferEncoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/ByteBufferEncoder.java index 9b829f3d09..321f43357d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/ByteBufferEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/ByteBufferEncoder.java @@ -34,8 +34,11 @@ public class ByteBufferEncoder implements MessageToByteEncoder { } @Override - public Publisher encode(Publisher messageStream, ResolvableType type, MediaType mediaType, Object... hints) { - return (Publisher)messageStream; + @SuppressWarnings("unchecked") + public Publisher encode(Publisher messageStream, + ResolvableType type, MediaType mediaType, Object... hints) { + + return (Publisher) messageStream; } } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoder.java index f24f7d76d1..f5e639b752 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoder.java @@ -54,7 +54,9 @@ public class JacksonJsonEncoder implements MessageToByteEncoder { } @Override - public Publisher encode(Publisher messageStream, ResolvableType type, MediaType mediaType, Object... hints) { + public Publisher encode(Publisher messageStream, + ResolvableType type, MediaType mediaType, Object... hints) { + return Publishers.map(messageStream, value -> { Buffer buffer = new Buffer(); BufferOutputStream outputStream = new BufferOutputStream(buffer); diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/Jaxb2Encoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/Jaxb2Encoder.java index 6533e5fe90..ea3063d5f7 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/Jaxb2Encoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/Jaxb2Encoder.java @@ -45,15 +45,19 @@ import org.springframework.util.ClassUtils; */ public class Jaxb2Encoder implements MessageToByteEncoder { - private final ConcurrentMap, JAXBContext> jaxbContexts = new ConcurrentHashMap, JAXBContext>(64); + private final ConcurrentMap, JAXBContext> jaxbContexts = new ConcurrentHashMap<>(64); + @Override public boolean canEncode(ResolvableType type, MediaType mediaType, Object... hints) { - return mediaType.isCompatibleWith(MediaType.APPLICATION_XML) || mediaType.isCompatibleWith(MediaType.TEXT_XML); + return (mediaType.isCompatibleWith(MediaType.APPLICATION_XML) || + mediaType.isCompatibleWith(MediaType.TEXT_XML)); } @Override - public Publisher encode(Publisher messageStream, ResolvableType type, MediaType mediaType, Object... hints) { + public Publisher encode(Publisher messageStream, ResolvableType type, + MediaType mediaType, Object... hints) { + return Publishers.map(messageStream, value -> { try { Buffer buffer = new Buffer(); @@ -64,12 +68,12 @@ public class Jaxb2Encoder implements MessageToByteEncoder { marshaller.marshal(value, outputStream); buffer.flip(); return buffer.byteBuffer(); - } catch (MarshalException ex) { - throw new CodecException( - "Could not marshal [" + value + "]: " + ex.getMessage(), ex); - } catch (JAXBException ex) { - throw new CodecException( - "Could not instantiate JAXBContext: " + ex.getMessage(), ex); + } + catch (MarshalException ex) { + throw new CodecException("Could not marshal [" + value + "]: " + ex.getMessage(), ex); + } + catch (JAXBException ex) { + throw new CodecException("Could not instantiate JAXBContext: " + ex.getMessage(), ex); } }); } @@ -77,12 +81,11 @@ public class Jaxb2Encoder implements MessageToByteEncoder { protected final Marshaller createMarshaller(Class clazz) { try { JAXBContext jaxbContext = getJaxbContext(clazz); - Marshaller marshaller = jaxbContext.createMarshaller(); - return marshaller; + return jaxbContext.createMarshaller(); } catch (JAXBException ex) { - throw new CodecException( - "Could not create Marshaller for class [" + clazz + "]: " + ex.getMessage(), ex); + throw new CodecException("Could not create Marshaller for class " + + "[" + clazz + "]: " + ex.getMessage(), ex); } } @@ -95,8 +98,8 @@ public class Jaxb2Encoder implements MessageToByteEncoder { this.jaxbContexts.putIfAbsent(clazz, jaxbContext); } catch (JAXBException ex) { - throw new CodecException( - "Could not instantiate JAXBContext for class [" + clazz + "]: " + ex.getMessage(), ex); + throw new CodecException("Could not instantiate JAXBContext for class " + + "[" + clazz + "]: " + ex.getMessage(), ex); } } return jaxbContext; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JsonObjectEncoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JsonObjectEncoder.java index e6a070642a..103a8e4b74 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JsonObjectEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JsonObjectEncoder.java @@ -33,8 +33,8 @@ import org.springframework.reactive.codec.decoder.JsonObjectDecoder; import static reactor.Publishers.lift; /** - * Encode a byte stream of individual JSON element to a byte stream representing a single - * JSON array when if it contains more than one element. + * Encode a byte stream of individual JSON element to a byte stream representing + * a single JSON array when if it contains more than one element. * * @author Sebastien Deleuze * @author Stephane Maldini @@ -52,27 +52,36 @@ public class JsonObjectEncoder implements MessageToByteEncoder { @Override public Publisher encode(Publisher messageStream, ResolvableType type, MediaType mediaType, Object... hints) { + + //noinspection Convert2MethodRef return lift(messageStream, bbs -> new JsonEncoderBarrier(bbs)); } + private static class JsonEncoderBarrier extends SubscriberBarrier { - private volatile long requested; @SuppressWarnings("rawtypes") static final AtomicLongFieldUpdater REQUESTED = AtomicLongFieldUpdater.newUpdater(JsonEncoderBarrier.class, "requested"); - private volatile int terminated; static final AtomicIntegerFieldUpdater TERMINATED = AtomicIntegerFieldUpdater.newUpdater(JsonEncoderBarrier.class, "terminated"); - ByteBuffer prev = null; - long count = 0; + + private ByteBuffer prev = null; + + private long count = 0; + + private volatile long requested; + + private volatile int terminated; + public JsonEncoderBarrier(Subscriber subscriber) { super(subscriber); } + @Override protected void doRequest(long n) { BackpressureUtils.getAndAdd(REQUESTED, this, n); @@ -86,17 +95,17 @@ public class JsonObjectEncoder implements MessageToByteEncoder { @Override protected void doNext(ByteBuffer next) { - count++; - if (count == 1) { - prev = next; + this.count++; + if (this.count == 1) { + this.prev = next; super.doRequest(1); return; } - ByteBuffer tmp = prev; - prev = next; + ByteBuffer tmp = this.prev; + this.prev = next; Buffer buffer = new Buffer(); - if (count == 2) { + if (this.count == 2) { buffer.append("["); } buffer.append(tmp); @@ -104,25 +113,25 @@ public class JsonObjectEncoder implements MessageToByteEncoder { buffer.flip(); BackpressureUtils.getAndSub(REQUESTED, this, 1L); - subscriber.onNext(buffer.byteBuffer()); + downstream().onNext(buffer.byteBuffer()); } protected void drainLast(){ if(BackpressureUtils.getAndSub(REQUESTED, this, 1L) > 0) { Buffer buffer = new Buffer(); - buffer.append(prev); - if (count > 1) { + buffer.append(this.prev); + if (this.count > 1) { buffer.append("]"); } buffer.flip(); - subscriber.onNext(buffer.byteBuffer()); + downstream().onNext(buffer.byteBuffer()); super.doComplete(); } } @Override protected void doComplete() { - if(TERMINATED.compareAndSet(this, 0, 1)){ + if(TERMINATED.compareAndSet(this, 0, 1)) { drainLast(); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/MessageToByteEncoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/MessageToByteEncoder.java index 0dea2db8bb..3075702cda 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/MessageToByteEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/MessageToByteEncoder.java @@ -51,6 +51,7 @@ public interface MessageToByteEncoder { * @param hints Additional information about how to encode, optional. * @return the encoded bytes stream */ - Publisher encode(Publisher messageStream, ResolvableType type, MediaType mediaType, Object... hints); + Publisher encode(Publisher messageStream, ResolvableType type, + MediaType mediaType, Object... hints); } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/StringEncoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/StringEncoder.java index f7bc6699fc..708745e75f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/StringEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/StringEncoder.java @@ -46,7 +46,9 @@ public class StringEncoder implements MessageToByteEncoder { } @Override - public Publisher encode(Publisher elementStream, ResolvableType type, MediaType mediaType, Object... hints) { + public Publisher encode(Publisher elementStream, + ResolvableType type, MediaType mediaType, Object... hints) { + final Charset charset = HintUtils.getHintByClass(Charset.class, hints, DEFAULT_CHARSET); return Publishers.map(elementStream, s -> ByteBuffer.wrap(s.getBytes(charset))); } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteBufferPublisherInputStream.java b/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteBufferPublisherInputStream.java index a4eace4e86..9f93f3aa62 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteBufferPublisherInputStream.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteBufferPublisherInputStream.java @@ -130,7 +130,8 @@ public class ByteBufferPublisherInputStream extends InputStream { if (this.currentStream != null && this.currentStream.available() > 0) { return this.currentStream; } else { - // take() blocks until next or complete() then return null, but that's OK since this is a *blocking* InputStream + // take() blocks until next or complete() then return null, + // but that's OK since this is a *blocking* InputStream ByteBuffer signal = this.queue.take(); if(signal == null){ this.completed = true; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/DispatcherHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/DispatcherHandler.java index c134df5171..51c9945e81 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/DispatcherHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/DispatcherHandler.java @@ -72,20 +72,20 @@ public class DispatcherHandler implements HttpHandler, ApplicationContextAware { protected void initStrategies(ApplicationContext context) { - Map mappingBeans = - BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false); + Map mappingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors( + context, HandlerMapping.class, true, false); this.handlerMappings = new ArrayList<>(mappingBeans.values()); AnnotationAwareOrderComparator.sort(this.handlerMappings); - Map adapterBeans = - BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerAdapter.class, true, false); + Map adapterBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors( + context, HandlerAdapter.class, true, false); this.handlerAdapters = new ArrayList<>(adapterBeans.values()); AnnotationAwareOrderComparator.sort(this.handlerAdapters); - Map beans = - BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerResultHandler.class, true, false); + Map beans = BeanFactoryUtils.beansOfTypeIncludingAncestors( + context, HandlerResultHandler.class, true, false); this.resultHandlers = new ArrayList<>(beans.values()); AnnotationAwareOrderComparator.sort(this.resultHandlers); @@ -94,7 +94,6 @@ public class DispatcherHandler implements HttpHandler, ApplicationContextAware { @Override public Publisher handle(ReactiveServerHttpRequest request, ReactiveServerHttpResponse response) { - if (logger.isDebugEnabled()) { logger.debug("Processing " + request.getMethod() + " request for [" + request.getURI() + "]"); } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerAdapter.java index 131a79b826..f9788ffb15 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerAdapter.java @@ -52,7 +52,6 @@ public interface HandlerAdapter { * @param handler handler to use. This object must have previously been passed * to the {@code supports} method of this interface, which must have * returned {@code true}. - * @throws Exception in case of errors * @return An {@link HandlerResult} instance */ Publisher handle(ReactiveServerHttpRequest request, diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerResultHandler.java index aee3d6eab5..3f3b593dca 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerResultHandler.java @@ -48,6 +48,7 @@ public interface HandlerResultHandler { * when the handling is complete (success or error) including the flush of the data on the * network. */ - Publisher handleResult(ReactiveServerHttpRequest request, ReactiveServerHttpResponse response, HandlerResult result); + Publisher handleResult(ReactiveServerHttpRequest request, ReactiveServerHttpResponse response, + HandlerResult result); } \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/SimpleHandlerResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/SimpleHandlerResultHandler.java index 0fe01b02be..e124bef083 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/SimpleHandlerResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/SimpleHandlerResultHandler.java @@ -34,6 +34,11 @@ public class SimpleHandlerResultHandler implements Ordered, HandlerResultHandler private int order = Ordered.LOWEST_PRECEDENCE; + + public void setOrder(int order) { + this.order = order; + } + @Override public int getOrder() { return this.order; @@ -46,8 +51,10 @@ public class SimpleHandlerResultHandler implements Ordered, HandlerResultHandler } @Override - public Publisher handleResult(ReactiveServerHttpRequest request, ReactiveServerHttpResponse response, HandlerResult result) { - Publisher handleComplete = Publishers.completable((Publisher)result.getValue()); - return Publishers.concat(Publishers.from(Arrays.asList(handleComplete, response.writeHeaders()))); + public Publisher handleResult(ReactiveServerHttpRequest request, + ReactiveServerHttpResponse response, HandlerResult result) { + + Publisher completion = Publishers.completable((Publisher)result.getValue()); + return Publishers.concat(Publishers.from(Arrays.asList(completion, response.writeHeaders()))); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/InvocableHandlerMethod.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/InvocableHandlerMethod.java index c409425f7b..29dc84358e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/InvocableHandlerMethod.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/InvocableHandlerMethod.java @@ -64,7 +64,8 @@ public class InvocableHandlerMethod extends HandlerMethod { List> argPublishers = getMethodArguments(request, providedArgs); Publisher argValues = (!argPublishers.isEmpty() ? - Streams.zip(argPublishers, this::unwrapOptionalArgValues) : Publishers.just(new Object[0])); + Streams.zip(argPublishers, this::unwrapOptionalArgValues) : + Publishers.just(new Object[0])); return Publishers.map(argValues, args -> { if (logger.isTraceEnabled()) { @@ -76,7 +77,8 @@ public class InvocableHandlerMethod extends HandlerMethod { try { returnValue = doInvoke(args); if (logger.isTraceEnabled()) { - logger.trace("Method [" + getMethod().getName() + "] returned [" + returnValue + "]"); + logger.trace("Method [" + getMethod().getName() + "] returned " + + "[" + returnValue + "]"); } } catch (Exception ex) { diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java index 8a0119c569..d9b5359f90 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java @@ -43,24 +43,29 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve private static final Charset UTF_8 = Charset.forName("UTF-8"); + private final List> deserializers; + private final List> preProcessors; + private final ConversionService conversionService; - public RequestBodyArgumentResolver(List> deserializers, + public RequestBodyArgumentResolver(List> decoders, ConversionService conversionService) { - this(deserializers, conversionService, Collections.EMPTY_LIST); + + this(decoders, conversionService, Collections.EMPTY_LIST); } - public RequestBodyArgumentResolver(List> deserializers, - ConversionService conversionService, - List> preProcessors) { - this.deserializers = deserializers; - this.conversionService = conversionService; + public RequestBodyArgumentResolver(List> decoders, + ConversionService service, List> preProcessors) { + + this.deserializers = decoders; + this.conversionService = service; this.preProcessors = preProcessors; } + @Override public boolean supportsParameter(MethodParameter parameter) { return parameter.hasParameterAnnotation(RequestBody.class); @@ -75,14 +80,15 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve Publisher inputStream = request.getBody(); Publisher elementStream = inputStream; ResolvableType elementType = type.hasGenerics() ? type.getGeneric(0) : type; - ByteToMessageDecoder deserializer = resolveDeserializers(request, elementType, mediaType, hints.toArray()); - if (deserializer != null) { - List> preProcessors = - resolvePreProcessors(request, elementType, mediaType,hints.toArray()); + ByteToMessageDecoder decoder = resolveDecoder(request, elementType, mediaType, hints.toArray()); + if (decoder != null) { + List> preProcessors = resolvePreProcessors( + request, elementType, mediaType,hints.toArray()); + for (ByteToMessageDecoder preProcessor : preProcessors) { inputStream = preProcessor.decode(inputStream, elementType, mediaType, hints.toArray()); } - elementStream = deserializer.decode(inputStream, elementType, mediaType, hints.toArray()); + elementStream = decoder.decode(inputStream, elementType, mediaType, hints.toArray()); } if (this.conversionService.canConvert(Publisher.class, type.getRawClass())) { return Publishers.just(this.conversionService.convert(elementStream, type.getRawClass())); @@ -97,7 +103,9 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve return ( mediaTypes.size() > 0 ? mediaTypes.get(0) : MediaType.TEXT_PLAIN); } - private ByteToMessageDecoder resolveDeserializers(ReactiveServerHttpRequest request, ResolvableType type, MediaType mediaType, Object[] hints) { + private ByteToMessageDecoder resolveDecoder(ReactiveServerHttpRequest request, + ResolvableType type, MediaType mediaType, Object[] hints) { + for (ByteToMessageDecoder deserializer : this.deserializers) { if (deserializer.canDecode(type, mediaType, hints)) { return deserializer; @@ -106,7 +114,10 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve return null; } - private List> resolvePreProcessors(ReactiveServerHttpRequest request, ResolvableType type, MediaType mediaType, Object[] hints) { + private List> resolvePreProcessors( + ReactiveServerHttpRequest request, ResolvableType type, MediaType mediaType, + Object[] hints) { + List> preProcessors = new ArrayList<>(); for (ByteToMessageDecoder preProcessor : this.preProcessors) { if (preProcessor.canDecode(type, mediaType, hints)) { @@ -115,4 +126,5 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve } return preProcessors; } + } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java index 2b89f9f7ed..75f20e7dbb 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java @@ -19,6 +19,7 @@ package org.springframework.reactive.web.dispatch.method.annotation; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import org.reactivestreams.Publisher; @@ -57,12 +58,16 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Initializin @Override public void afterPropertiesSet() throws Exception { if (this.argumentResolvers == null) { + + List> decoders = Arrays.asList(new ByteBufferDecoder(), + new StringDecoder(), new JacksonJsonDecoder()); + + List> preProcessors = Collections.singletonList( + new JsonObjectDecoder()); + this.argumentResolvers = new ArrayList<>(); this.argumentResolvers.add(new RequestParamArgumentResolver()); - List> deserializers = Arrays.asList(new ByteBufferDecoder(), - new StringDecoder(), new JacksonJsonDecoder()); - List> preProcessors = Arrays.asList(new JsonObjectDecoder()); - this.argumentResolvers.add(new RequestBodyArgumentResolver(deserializers, + this.argumentResolvers.add(new RequestBodyArgumentResolver(decoders, new DefaultConversionService(), preProcessors)); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerMapping.java index 3dd42b3831..c67acd65cf 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerMapping.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerMapping.java @@ -94,13 +94,12 @@ public class RequestMappingHandlerMapping implements HandlerMapping, @Override public Object getHandler(ReactiveServerHttpRequest request) { - String path = request.getURI().getPath(); - HttpMethod method = request.getMethod(); for (Map.Entry entry : this.methodMap.entrySet()) { RequestMappingInfo info = entry.getKey(); - if (path.equals(info.getPath()) && (info.getMethods().isEmpty() || info.getMethods().contains(RequestMethod.valueOf(method.name())))) { + if (info.matchesRequest(request)) { if (logger.isDebugEnabled()) { - logger.debug("Mapped " + method + " " + path + " to [" + entry.getValue() + "]"); + logger.debug("Mapped " + request.getMethod() + " " + + request.getURI().getPath() + " to [" + entry.getValue() + "]"); } return entry.getValue(); } @@ -120,6 +119,11 @@ public class RequestMappingHandlerMapping implements HandlerMapping, this(path, asList(methods)); } + private static List asList(RequestMethod... requestMethods) { + return (requestMethods != null ? + Arrays.asList(requestMethods) : Collections.emptyList()); + } + public RequestMappingInfo(String path, Collection methods) { this.path = path; this.methods = new TreeSet<>(methods); @@ -127,20 +131,22 @@ public class RequestMappingHandlerMapping implements HandlerMapping, public String getPath() { - return path; + return this.path; } public Set getMethods() { - return methods; + return this.methods; } - private static List asList(RequestMethod... requestMethods) { - return (requestMethods != null ? Arrays.asList(requestMethods) : Collections.emptyList()); + public boolean matchesRequest(ReactiveServerHttpRequest request) { + String httpMethod = request.getMethod().name(); + return request.getURI().getPath().equals(getPath()) && + (getMethods().isEmpty() || getMethods().contains(RequestMethod.valueOf(httpMethod))); } @Override public int compareTo(Object o) { - RequestMappingInfo other = (RequestMappingInfo)o; + RequestMappingInfo other = (RequestMappingInfo) o; if (!this.path.equals(other.getPath())) { return -1; } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java index 43b189e299..aa2a14bb0a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java @@ -16,6 +16,7 @@ package org.springframework.reactive.web.dispatch.method.annotation; +import java.lang.reflect.Method; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.util.ArrayList; @@ -61,21 +62,26 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered private int order = 0; - public ResponseBodyResultHandler(List> serializers) { - this(serializers, Collections.EMPTY_LIST); + public ResponseBodyResultHandler(List> encoders) { + this(encoders, Collections.EMPTY_LIST); } - public ResponseBodyResultHandler(List> serializers, List> postProcessors) { - this(serializers, postProcessors, new DefaultConversionService()); + public ResponseBodyResultHandler(List> encoders, + List> postProcessors) { + + this(encoders, postProcessors, new DefaultConversionService()); } - public ResponseBodyResultHandler(List> serializers, List> - postProcessors, ConversionService conversionService) { - this.serializers = serializers; + public ResponseBodyResultHandler(List> encoders, + List> postProcessors, + ConversionService conversionService) { + + this.serializers = encoders; this.postProcessors = postProcessors; this.conversionService = conversionService; } + public void setOrder(int order) { this.order = order; } @@ -90,8 +96,8 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered public boolean supports(HandlerResult result) { Object handler = result.getHandler(); if (handler instanceof HandlerMethod) { - HandlerMethod handlerMethod = (HandlerMethod) handler; - return AnnotatedElementUtils.isAnnotated(handlerMethod.getMethod(), ResponseBody.class.getName()); + Method method = ((HandlerMethod) handler).getMethod(); + return AnnotatedElementUtils.isAnnotated(method, ResponseBody.class.getName()); } return false; } @@ -99,8 +105,7 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered @Override @SuppressWarnings("unchecked") public Publisher handleResult(ReactiveServerHttpRequest request, - ReactiveServerHttpResponse response, - HandlerResult result) { + ReactiveServerHttpResponse response, HandlerResult result) { Object value = result.getValue(); HandlerMethod handlerMethod = (HandlerMethod) result.getHandler(); @@ -125,19 +130,22 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered elementType = type; } - MessageToByteEncoder serializer = (MessageToByteEncoder) resolveSerializer(request, elementType, mediaType, hints.toArray()); - if (serializer != null) { - Publisher outputStream = serializer.encode(elementStream, type, mediaType, hints.toArray()); - List> postProcessors = resolvePostProcessors(request, elementType, mediaType, hints.toArray()); + MessageToByteEncoder encoder = (MessageToByteEncoder) resolveEncoder( + request, elementType, mediaType, hints.toArray()); + + if (encoder != null) { + Publisher outputStream = encoder.encode(elementStream, type, mediaType, hints.toArray()); + List> postProcessors = resolvePostProcessors(request, + elementType, mediaType, hints.toArray()); for (MessageToByteEncoder postProcessor : postProcessors) { outputStream = postProcessor.encode(outputStream, elementType, mediaType, hints.toArray()); } response.getHeaders().setContentType(mediaType); return response.setBody(outputStream); } - return Publishers.error(new IllegalStateException( - "Return value type '" + returnType.getParameterType().getName() + - "' with media type '" + mediaType + "' not supported")); + String returnTypeName = returnType.getParameterType().getName(); + return Publishers.error(new IllegalStateException("Return value type '" + returnTypeName + + "' with media type '" + mediaType + "' not supported")); } private MediaType resolveMediaType(ReactiveServerHttpRequest request) { @@ -147,7 +155,9 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered return ( mediaTypes.size() > 0 ? mediaTypes.get(0) : MediaType.TEXT_PLAIN); } - private MessageToByteEncoder resolveSerializer(ReactiveServerHttpRequest request, ResolvableType type, MediaType mediaType, Object[] hints) { + private MessageToByteEncoder resolveEncoder(ReactiveServerHttpRequest request, + ResolvableType type, MediaType mediaType, Object[] hints) { + for (MessageToByteEncoder codec : this.serializers) { if (codec.canEncode(type, mediaType, hints)) { return codec; @@ -156,7 +166,10 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered return null; } - private List> resolvePostProcessors(ReactiveServerHttpRequest request, ResolvableType type, MediaType mediaType, Object[] hints) { + private List> resolvePostProcessors( + ReactiveServerHttpRequest request, ResolvableType type, MediaType mediaType, + Object[] hints) { + List> postProcessors = new ArrayList<>(); for (MessageToByteEncoder postProcessor : this.postProcessors) { if (postProcessor.canEncode(type, mediaType, hints)) { diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpResponse.java index 241d76fa59..4233be995c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpResponse.java @@ -71,10 +71,11 @@ public class RxNettyServerHttpResponse implements ReactiveServerHttpResponse { } @Override - public Publisher setBody(Publisher contentPublisher) { + public Publisher setBody(Publisher publisher) { applyHeaders(); - Observable contentObservable = RxJava1Converter.from(contentPublisher).map(content -> new Buffer(content).asBytes()); - return RxJava1Converter.from(this.response.writeBytes(contentObservable)); + Observable observable = RxJava1Converter.from(publisher).map( + content -> new Buffer(content).asBytes()); + return RxJava1Converter.from(this.response.writeBytes(observable)); } private void applyHeaders() { diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/HttpHandlerServlet.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/HttpHandlerServlet.java index 5a6b7c2155..63563aba43 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/HttpHandlerServlet.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/HttpHandlerServlet.java @@ -57,17 +57,17 @@ public class HttpHandlerServlet extends HttpServlet { throws ServletException, IOException { AsyncContext context = request.startAsync(); - AsyncContextSynchronizer contextSynchronizer = new AsyncContextSynchronizer(context); + AsyncContextSynchronizer synchronizer = new AsyncContextSynchronizer(context); - RequestBodyPublisher requestPublisher = new RequestBodyPublisher(contextSynchronizer, BUFFER_SIZE); + RequestBodyPublisher requestPublisher = new RequestBodyPublisher(synchronizer, BUFFER_SIZE); request.getInputStream().setReadListener(requestPublisher); ServletServerHttpRequest httpRequest = new ServletServerHttpRequest(request, requestPublisher); - ResponseBodySubscriber responseSubscriber = new ResponseBodySubscriber(contextSynchronizer); + ResponseBodySubscriber responseSubscriber = new ResponseBodySubscriber(synchronizer); response.getOutputStream().setWriteListener(responseSubscriber); ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response, responseSubscriber); - HandlerResultSubscriber resultSubscriber = new HandlerResultSubscriber(contextSynchronizer, httpResponse); + HandlerResultSubscriber resultSubscriber = new HandlerResultSubscriber(synchronizer, httpResponse); this.handler.handle(httpRequest, httpResponse).subscribe(resultSubscriber); } @@ -79,7 +79,9 @@ public class HttpHandlerServlet extends HttpServlet { private final ServletServerHttpResponse response; - public HandlerResultSubscriber(AsyncContextSynchronizer synchronizer, ServletServerHttpResponse response) { + public HandlerResultSubscriber(AsyncContextSynchronizer synchronizer, + ServletServerHttpResponse response) { + this.synchronizer = synchronizer; this.response = response; } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpRequest.java index 80a1406351..34aca4ee29 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpRequest.java @@ -46,7 +46,9 @@ public class ServletServerHttpRequest implements ReactiveServerHttpRequest { private HttpHeaders headers; - public ServletServerHttpRequest(HttpServletRequest servletRequest, Publisher requestBodyPublisher) { + public ServletServerHttpRequest(HttpServletRequest servletRequest, + Publisher requestBodyPublisher) { + Assert.notNull(servletRequest, "HttpServletRequest must not be null"); this.servletRequest = servletRequest; this.requestBodyPublisher = requestBodyPublisher; @@ -74,8 +76,8 @@ public class ServletServerHttpRequest implements ReactiveServerHttpRequest { public HttpHeaders getHeaders() { if (this.headers == null) { this.headers = new HttpHeaders(); - for (Enumeration headerNames = this.servletRequest.getHeaderNames(); headerNames.hasMoreElements(); ) { - String headerName = (String) headerNames.nextElement(); + for (Enumeration names = this.servletRequest.getHeaderNames(); names.hasMoreElements(); ) { + String headerName = (String) names.nextElement(); for (Enumeration headerValues = this.servletRequest.getHeaders(headerName); headerValues.hasMoreElements(); ) { String headerValue = (String) headerValues.nextElement(); diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpResponse.java index e0bc7c78f7..3a9949eb5b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpResponse.java @@ -17,6 +17,7 @@ package org.springframework.reactive.web.http.servlet; import java.nio.ByteBuffer; +import java.nio.charset.Charset; import java.util.List; import java.util.Map; import javax.servlet.http.HttpServletResponse; @@ -26,6 +27,7 @@ import reactor.Publishers; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.server.ReactiveServerHttpResponse; import org.springframework.util.Assert; @@ -34,27 +36,29 @@ import org.springframework.util.Assert; */ public class ServletServerHttpResponse implements ReactiveServerHttpResponse { - private final HttpServletResponse servletResponse; + private final HttpServletResponse response; - private final ResponseBodySubscriber responseSubscriber; + private final ResponseBodySubscriber subscriber; private final HttpHeaders headers; private boolean headersWritten = false; - public ServletServerHttpResponse(HttpServletResponse servletResponse, ResponseBodySubscriber responseSubscriber) { - Assert.notNull(servletResponse, "'servletResponse' must not be null"); - Assert.notNull(responseSubscriber, "'responseSubscriber' must not be null"); - this.servletResponse = servletResponse; - this.responseSubscriber = responseSubscriber; + public ServletServerHttpResponse(HttpServletResponse response, + ResponseBodySubscriber subscriber) { + + Assert.notNull(response, "'response' must not be null"); + Assert.notNull(subscriber, "'subscriber' must not be null"); + this.response = response; + this.subscriber = subscriber; this.headers = new HttpHeaders(); } @Override public void setStatusCode(HttpStatus status) { - this.servletResponse.setStatus(status.value()); + this.response.setStatus(status.value()); } @Override @@ -71,7 +75,7 @@ public class ServletServerHttpResponse implements ReactiveServerHttpResponse { @Override public Publisher setBody(final Publisher contentPublisher) { applyHeaders(); - return (s -> contentPublisher.subscribe(responseSubscriber)); + return (s -> contentPublisher.subscribe(subscriber)); } private void applyHeaders() { @@ -79,16 +83,16 @@ public class ServletServerHttpResponse implements ReactiveServerHttpResponse { for (Map.Entry> entry : this.headers.entrySet()) { String headerName = entry.getKey(); for (String headerValue : entry.getValue()) { - this.servletResponse.addHeader(headerName, headerValue); + this.response.addHeader(headerName, headerValue); } } - // HttpServletResponse exposes some headers as properties: we should include those if not already present - if (this.servletResponse.getContentType() == null && this.headers.getContentType() != null) { - this.servletResponse.setContentType(this.headers.getContentType().toString()); + MediaType contentType = this.headers.getContentType(); + if (this.response.getContentType() == null && contentType != null) { + this.response.setContentType(contentType.toString()); } - if (this.servletResponse.getCharacterEncoding() == null && this.headers.getContentType() != null && - this.headers.getContentType().getCharSet() != null) { - this.servletResponse.setCharacterEncoding(this.headers.getContentType().getCharSet().name()); + Charset charset = (contentType != null ? contentType.getCharSet() : null); + if (this.response.getCharacterEncoding() == null && charset != null) { + this.response.setCharacterEncoding(charset.name()); } this.headersWritten = true; } diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingIntegrationTests.java index 0b3544740c..fc6133e00f 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingIntegrationTests.java @@ -21,6 +21,7 @@ import java.net.URI; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.concurrent.CompletableFuture; @@ -69,6 +70,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati private TestController controller; + @Override protected HttpHandler createHttpHandler() { @@ -76,9 +78,9 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati DefaultListableBeanFactory factory = wac.getDefaultListableBeanFactory(); wac.registerSingleton("handlerMapping", RequestMappingHandlerMapping.class); wac.registerSingleton("handlerAdapter", RequestMappingHandlerAdapter.class); - factory.registerSingleton("responseBodyResultHandler", - new ResponseBodyResultHandler(Arrays.asList(new ByteBufferEncoder(), new StringEncoder(), new JacksonJsonEncoder()), Arrays.asList - (new JsonObjectEncoder()))); + factory.registerSingleton("responseBodyResultHandler", new ResponseBodyResultHandler( + Arrays.asList(new ByteBufferEncoder(), new StringEncoder(),new JacksonJsonEncoder()), + Collections.singletonList(new JsonObjectEncoder()))); wac.registerSingleton("simpleResultHandler", SimpleHandlerResultHandler.class); this.controller = new TestController(); factory.registerSingleton("controller", this.controller); @@ -197,16 +199,16 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @Test public void promiseCapitalize() throws Exception { - capitalizePojo("http://localhost:" + port + "/promise-capitalize"); + capitalizePojo("http://localhost:" + this.port + "/promise-capitalize"); } @Test public void create() throws Exception { RestTemplate restTemplate = new RestTemplate(); - - URI url = new URI("http://localhost:" + port + "/create"); - List persons = Arrays.asList(new Person("Robert"), new Person("Marie")); - RequestEntity> request = RequestEntity.post(url).contentType(MediaType.APPLICATION_JSON).body(persons); + URI url = new URI("http://localhost:" + this.port + "/create"); + RequestEntity> request = RequestEntity.post(url) + .contentType(MediaType.APPLICATION_JSON) + .body(Arrays.asList(new Person("Robert"), new Person("Marie"))); ResponseEntity response = restTemplate.exchange(request, Void.class); assertEquals(HttpStatus.OK, response.getStatusCode()); @@ -216,9 +218,9 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati public void serializeAsPojo(String requestUrl) throws Exception { RestTemplate restTemplate = new RestTemplate(); - - URI url = new URI(requestUrl); - RequestEntity request = RequestEntity.get(url).accept(MediaType.APPLICATION_JSON).build(); + RequestEntity request = RequestEntity.get(new URI(requestUrl)) + .accept(MediaType.APPLICATION_JSON) + .build(); ResponseEntity response = restTemplate.exchange(request, Person.class); assertEquals(new Person("Robert"), response.getBody()); @@ -226,10 +228,9 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati public void postAsPojo(String requestUrl) throws Exception { RestTemplate restTemplate = new RestTemplate(); - - URI url = new URI(requestUrl); - RequestEntity request = RequestEntity.post(url).accept(MediaType.APPLICATION_JSON).body(new Person - ("Robert")); + RequestEntity request = RequestEntity.post(new URI(requestUrl)) + .accept(MediaType.APPLICATION_JSON) + .body(new Person("Robert")); ResponseEntity response = restTemplate.exchange(request, Person.class); assertEquals(new Person("Robert"), response.getBody()); @@ -237,10 +238,11 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati public void serializeAsCollection(String requestUrl) throws Exception { RestTemplate restTemplate = new RestTemplate(); - - URI url = new URI(requestUrl); - RequestEntity request = RequestEntity.get(url).accept(MediaType.APPLICATION_JSON).build(); - List results = restTemplate.exchange(request, new ParameterizedTypeReference>(){}).getBody(); + RequestEntity request = RequestEntity.get(new URI(requestUrl)) + .accept(MediaType.APPLICATION_JSON) + .build(); + List results = restTemplate.exchange(request, + new ParameterizedTypeReference>(){}).getBody(); assertEquals(2, results.size()); assertEquals(new Person("Robert"), results.get(0)); @@ -250,10 +252,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati public void capitalizePojo(String requestUrl) throws Exception { RestTemplate restTemplate = new RestTemplate(); - - URI url = new URI(requestUrl); - RequestEntity request = RequestEntity - .post(url) + RequestEntity request = RequestEntity.post(new URI(requestUrl)) .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON) .body(new Person("Robert")); @@ -265,15 +264,12 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati public void capitalizeCollection(String requestUrl) throws Exception { RestTemplate restTemplate = new RestTemplate(); - - URI url = new URI(requestUrl); - List persons = Arrays.asList(new Person("Robert"), new Person("Marie")); - RequestEntity> request = RequestEntity - .post(url) + RequestEntity> request = RequestEntity.post(new URI(requestUrl)) .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON) - .body(persons); - List results = restTemplate.exchange(request, new ParameterizedTypeReference>(){}).getBody(); + .body(Arrays.asList(new Person("Robert"), new Person("Marie"))); + List results = restTemplate.exchange(request, + new ParameterizedTypeReference>(){}).getBody(); assertEquals(2, results.size()); assertEquals("ROBERT", results.get(0).getName()); @@ -309,7 +305,8 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @ResponseBody public Publisher rawResponseBody() { JacksonJsonEncoder encoder = new JacksonJsonEncoder(); - return encoder.encode(Streams.just(new Person("Robert")), ResolvableType.forClass(Person.class), MediaType.APPLICATION_JSON); + return encoder.encode(Streams.just(new Person("Robert")), + ResolvableType.forClass(Person.class), MediaType.APPLICATION_JSON); } @RequestMapping("/raw-observable") @@ -390,7 +387,9 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @RequestMapping("/completable-future-capitalize") @ResponseBody - public CompletableFuture completableFutureCapitalize(@RequestBody CompletableFuture personFuture) { + public CompletableFuture completableFutureCapitalize( + @RequestBody CompletableFuture personFuture) { + return personFuture.thenApply(person -> { person.setName(person.getName().toUpperCase()); return person; @@ -417,7 +416,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @RequestMapping("/create") public Publisher create(@RequestBody Stream personStream) { - return personStream.toList().onSuccess(personList -> persons.addAll(personList)).after(); + return personStream.toList().onSuccess(persons::addAll).after(); } //TODO add mixed and T request mappings tests diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandlerTests.java index 4cdd620432..c37f6dc19f 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandlerTests.java @@ -32,22 +32,24 @@ import org.springframework.web.method.HandlerMethod; */ public class ResponseBodyResultHandlerTests { + @Test public void supports() throws NoSuchMethodException { ResponseBodyResultHandler resultHandler = new ResponseBodyResultHandler(Collections.emptyList()); TestController controller = new TestController(); - HandlerMethod notAnnotatedMethod = new HandlerMethod(controller, TestController.class.getMethod("notAnnotated")); - assertFalse(resultHandler.supports(new HandlerResult(notAnnotatedMethod, null))); + HandlerMethod hm = new HandlerMethod(controller,TestController.class.getMethod("notAnnotated")); + assertFalse(resultHandler.supports(new HandlerResult(hm, null))); - HandlerMethod publisherStringMethod = new HandlerMethod(controller, TestController.class.getMethod("publisherString")); - assertTrue(resultHandler.supports(new HandlerResult(publisherStringMethod, null))); + hm = new HandlerMethod(controller, TestController.class.getMethod("publisherString")); + assertTrue(resultHandler.supports(new HandlerResult(hm, null))); - HandlerMethod publisherVoidMethod = new HandlerMethod(controller, TestController.class.getMethod("publisherVoid")); - assertTrue(resultHandler.supports(new HandlerResult(publisherVoidMethod, null))); + hm = new HandlerMethod(controller, TestController.class.getMethod("publisherVoid")); + assertTrue(resultHandler.supports(new HandlerResult(hm, null))); } + @SuppressWarnings("unused") private static class TestController { public Publisher notAnnotated() { From a48c9b6305a9aab74168b5c5455f8b5ef9eb466b Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 30 Oct 2015 17:58:59 -0400 Subject: [PATCH 081/498] Replace DefaultConversionService in spring-reactive --- .../support/DefaultConversionService.java | 47 ------- .../RequestBodyArgumentResolver.java | 38 +++--- .../RequestMappingHandlerAdapter.java | 27 ++-- .../annotation/ResponseBodyResultHandler.java | 40 +++--- .../RequestMappingIntegrationTests.java | 123 ++++++++++++------ .../ResponseBodyResultHandlerTests.java | 16 ++- 6 files changed, 141 insertions(+), 150 deletions(-) delete mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/convert/support/DefaultConversionService.java diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/convert/support/DefaultConversionService.java b/spring-web-reactive/src/main/java/org/springframework/reactive/convert/support/DefaultConversionService.java deleted file mode 100644 index a3e345f1f1..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/convert/support/DefaultConversionService.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2002-2015 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.reactive.convert.support; - -import reactor.core.publisher.convert.DependencyUtils; - -import org.springframework.core.convert.converter.ConverterRegistry; -import org.springframework.core.convert.support.GenericConversionService; -import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter; -import org.springframework.core.convert.support.ReactiveStreamsToReactorConverter; -import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter; - -/** - * TODO temporary class designed to be replaced by org.springframework.core.convert.support.DefaultConversionService when it will contain Reactive Streams converter - * @author Sebastien Deleuze - */ -public class DefaultConversionService extends GenericConversionService { - - public DefaultConversionService() { - addDefaultConverters(this); - } - - public static void addDefaultConverters(ConverterRegistry converterRegistry) { - converterRegistry.addConverter(new ReactiveStreamsToCompletableFutureConverter()); - if (DependencyUtils.hasReactorStream()) { - converterRegistry.addConverter(new ReactiveStreamsToReactorConverter()); - } - if (DependencyUtils.hasRxJava1()) { - converterRegistry.addConverter(new ReactiveStreamsToRxJava1Converter()); - } - } - -} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java index d9b5359f90..60f8aa8055 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java @@ -19,6 +19,7 @@ package org.springframework.reactive.web.dispatch.method.annotation; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -32,7 +33,9 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.server.ReactiveServerHttpRequest; import org.springframework.reactive.codec.decoder.ByteToMessageDecoder; +import org.springframework.reactive.codec.decoder.JsonObjectDecoder; import org.springframework.reactive.web.dispatch.method.HandlerMethodArgumentResolver; +import org.springframework.util.Assert; import org.springframework.web.bind.annotation.RequestBody; /** @@ -44,25 +47,19 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve private static final Charset UTF_8 = Charset.forName("UTF-8"); - private final List> deserializers; - - private final List> preProcessors; + private final List> decoders; private final ConversionService conversionService; + // TODO: remove field + private final List> preProcessors = Arrays.asList(new JsonObjectDecoder()); - public RequestBodyArgumentResolver(List> decoders, - ConversionService conversionService) { - this(decoders, conversionService, Collections.EMPTY_LIST); - } - - public RequestBodyArgumentResolver(List> decoders, - ConversionService service, List> preProcessors) { - - this.deserializers = decoders; + public RequestBodyArgumentResolver(List> decoders, ConversionService service) { + Assert.notEmpty(decoders, "At least one decoder is required."); + Assert.notNull(service, "'conversionService' is required."); + this.decoders = decoders; this.conversionService = service; - this.preProcessors = preProcessors; } @@ -80,10 +77,10 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve Publisher inputStream = request.getBody(); Publisher elementStream = inputStream; ResolvableType elementType = type.hasGenerics() ? type.getGeneric(0) : type; - ByteToMessageDecoder decoder = resolveDecoder(request, elementType, mediaType, hints.toArray()); + ByteToMessageDecoder decoder = resolveDecoder(elementType, mediaType, hints.toArray()); if (decoder != null) { List> preProcessors = resolvePreProcessors( - request, elementType, mediaType,hints.toArray()); + elementType, mediaType,hints.toArray()); for (ByteToMessageDecoder preProcessor : preProcessors) { inputStream = preProcessor.decode(inputStream, elementType, mediaType, hints.toArray()); @@ -103,10 +100,8 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve return ( mediaTypes.size() > 0 ? mediaTypes.get(0) : MediaType.TEXT_PLAIN); } - private ByteToMessageDecoder resolveDecoder(ReactiveServerHttpRequest request, - ResolvableType type, MediaType mediaType, Object[] hints) { - - for (ByteToMessageDecoder deserializer : this.deserializers) { + private ByteToMessageDecoder resolveDecoder(ResolvableType type, MediaType mediaType, Object[] hints) { + for (ByteToMessageDecoder deserializer : this.decoders) { if (deserializer.canDecode(type, mediaType, hints)) { return deserializer; } @@ -114,9 +109,8 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve return null; } - private List> resolvePreProcessors( - ReactiveServerHttpRequest request, ResolvableType type, MediaType mediaType, - Object[] hints) { + private List> resolvePreProcessors(ResolvableType type, + MediaType mediaType, Object[] hints) { List> preProcessors = new ArrayList<>(); for (ByteToMessageDecoder preProcessor : this.preProcessors) { diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java index 75f20e7dbb..f604fa0b3c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java @@ -16,24 +16,21 @@ package org.springframework.reactive.web.dispatch.method.annotation; -import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; import org.reactivestreams.Publisher; import reactor.Publishers; import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.convert.ConversionService; import org.springframework.http.server.ReactiveServerHttpRequest; import org.springframework.http.server.ReactiveServerHttpResponse; import org.springframework.reactive.codec.decoder.ByteBufferDecoder; import org.springframework.reactive.codec.decoder.ByteToMessageDecoder; import org.springframework.reactive.codec.decoder.JacksonJsonDecoder; -import org.springframework.reactive.codec.decoder.JsonObjectDecoder; import org.springframework.reactive.codec.decoder.StringDecoder; -import org.springframework.reactive.convert.support.DefaultConversionService; import org.springframework.reactive.web.dispatch.HandlerAdapter; import org.springframework.reactive.web.dispatch.HandlerResult; import org.springframework.reactive.web.dispatch.method.HandlerMethodArgumentResolver; @@ -48,12 +45,26 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Initializin private List argumentResolvers; + private ConversionService conversionService; - public void setHandlerMethodArgumentResolvers(List resolvers) { + + public void setArgumentResolvers(List resolvers) { this.argumentResolvers.clear(); this.argumentResolvers.addAll(resolvers); } + public List getArgumentResolvers() { + return this.argumentResolvers; + } + + public void setConversionService(ConversionService conversionService) { + this.conversionService = conversionService; + } + + public ConversionService getConversionService() { + return this.conversionService; + } + @Override public void afterPropertiesSet() throws Exception { @@ -62,13 +73,9 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Initializin List> decoders = Arrays.asList(new ByteBufferDecoder(), new StringDecoder(), new JacksonJsonDecoder()); - List> preProcessors = Collections.singletonList( - new JsonObjectDecoder()); - this.argumentResolvers = new ArrayList<>(); this.argumentResolvers.add(new RequestParamArgumentResolver()); - this.argumentResolvers.add(new RequestBodyArgumentResolver(decoders, - new DefaultConversionService(), preProcessors)); + this.argumentResolvers.add(new RequestBodyArgumentResolver(decoders, this.conversionService)); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java index aa2a14bb0a..ec66870464 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java @@ -20,7 +20,7 @@ import java.lang.reflect.Method; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.util.ArrayList; -import java.util.Collections; +import java.util.Arrays; import java.util.List; import org.reactivestreams.Publisher; @@ -35,10 +35,11 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.server.ReactiveServerHttpRequest; import org.springframework.http.server.ReactiveServerHttpResponse; +import org.springframework.reactive.codec.encoder.JsonObjectEncoder; import org.springframework.reactive.codec.encoder.MessageToByteEncoder; -import org.springframework.reactive.convert.support.DefaultConversionService; import org.springframework.reactive.web.dispatch.HandlerResult; import org.springframework.reactive.web.dispatch.HandlerResultHandler; +import org.springframework.util.Assert; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.method.HandlerMethod; @@ -56,29 +57,21 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered private final List> serializers; - private final List> postProcessors; + private final ConversionService conversionService; private int order = 0; + // TODO: remove field + private final List> postProcessors = Arrays.asList(new JsonObjectEncoder()); - public ResponseBodyResultHandler(List> encoders) { - this(encoders, Collections.EMPTY_LIST); - } - public ResponseBodyResultHandler(List> encoders, - List> postProcessors) { - - this(encoders, postProcessors, new DefaultConversionService()); - } - - public ResponseBodyResultHandler(List> encoders, - List> postProcessors, - ConversionService conversionService) { + public ResponseBodyResultHandler(List> encoders, ConversionService service) { + Assert.notEmpty(encoders, "At least one encoder is required."); + Assert.notNull(service, "'conversionService' is required."); this.serializers = encoders; - this.postProcessors = postProcessors; - this.conversionService = conversionService; + this.conversionService = service; } @@ -131,11 +124,11 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered } MessageToByteEncoder encoder = (MessageToByteEncoder) resolveEncoder( - request, elementType, mediaType, hints.toArray()); + elementType, mediaType, hints.toArray()); if (encoder != null) { Publisher outputStream = encoder.encode(elementStream, type, mediaType, hints.toArray()); - List> postProcessors = resolvePostProcessors(request, + List> postProcessors = resolvePostProcessors( elementType, mediaType, hints.toArray()); for (MessageToByteEncoder postProcessor : postProcessors) { outputStream = postProcessor.encode(outputStream, elementType, mediaType, hints.toArray()); @@ -155,9 +148,7 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered return ( mediaTypes.size() > 0 ? mediaTypes.get(0) : MediaType.TEXT_PLAIN); } - private MessageToByteEncoder resolveEncoder(ReactiveServerHttpRequest request, - ResolvableType type, MediaType mediaType, Object[] hints) { - + private MessageToByteEncoder resolveEncoder(ResolvableType type, MediaType mediaType, Object[] hints) { for (MessageToByteEncoder codec : this.serializers) { if (codec.canEncode(type, mediaType, hints)) { return codec; @@ -166,9 +157,8 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered return null; } - private List> resolvePostProcessors( - ReactiveServerHttpRequest request, ResolvableType type, MediaType mediaType, - Object[] hints) { + private List> resolvePostProcessors(ResolvableType type, + MediaType mediaType, Object[] hints) { List> postProcessors = new ArrayList<>(); for (MessageToByteEncoder postProcessor : this.postProcessors) { diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingIntegrationTests.java index fc6133e00f..f8f14cd64e 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingIntegrationTests.java @@ -21,17 +21,11 @@ import java.net.URI; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.concurrent.CompletableFuture; -import static org.junit.Assert.assertEquals; - import org.junit.Test; import org.reactivestreams.Publisher; - -import org.springframework.beans.factory.support.DefaultListableBeanFactory; -import org.springframework.core.ResolvableType; import reactor.io.buffer.Buffer; import reactor.rx.Promise; import reactor.rx.Promises; @@ -40,14 +34,22 @@ import reactor.rx.Streams; import rx.Observable; import rx.Single; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.ResolvableType; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter; +import org.springframework.core.convert.support.ReactiveStreamsToReactorConverter; +import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; import org.springframework.reactive.codec.encoder.ByteBufferEncoder; import org.springframework.reactive.codec.encoder.JacksonJsonEncoder; -import org.springframework.reactive.codec.encoder.JsonObjectEncoder; import org.springframework.reactive.codec.encoder.StringEncoder; import org.springframework.reactive.web.dispatch.DispatcherHandler; import org.springframework.reactive.web.dispatch.SimpleHandlerResultHandler; @@ -59,7 +61,9 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.client.RestTemplate; -import org.springframework.web.context.support.StaticWebApplicationContext; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; + +import static org.junit.Assert.assertEquals; /** * @author Rossen Stoyanchev @@ -68,26 +72,17 @@ import org.springframework.web.context.support.StaticWebApplicationContext; */ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrationTests { - private TestController controller; + private AnnotationConfigWebApplicationContext wac; @Override protected HttpHandler createHttpHandler() { - - StaticWebApplicationContext wac = new StaticWebApplicationContext(); - DefaultListableBeanFactory factory = wac.getDefaultListableBeanFactory(); - wac.registerSingleton("handlerMapping", RequestMappingHandlerMapping.class); - wac.registerSingleton("handlerAdapter", RequestMappingHandlerAdapter.class); - factory.registerSingleton("responseBodyResultHandler", new ResponseBodyResultHandler( - Arrays.asList(new ByteBufferEncoder(), new StringEncoder(),new JacksonJsonEncoder()), - Collections.singletonList(new JsonObjectEncoder()))); - wac.registerSingleton("simpleResultHandler", SimpleHandlerResultHandler.class); - this.controller = new TestController(); - factory.registerSingleton("controller", this.controller); - wac.refresh(); + this.wac = new AnnotationConfigWebApplicationContext(); + this.wac.register(FrameworkConfig.class, ApplicationConfig.class); + this.wac.refresh(); DispatcherHandler dispatcherHandler = new DispatcherHandler(); - dispatcherHandler.setApplicationContext(wac); + dispatcherHandler.setApplicationContext(this.wac); return dispatcherHandler; } @@ -212,11 +207,11 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati ResponseEntity response = restTemplate.exchange(request, Void.class); assertEquals(HttpStatus.OK, response.getStatusCode()); - assertEquals(2, this.controller.persons.size()); + assertEquals(2, this.wac.getBean(TestController.class).persons.size()); } - public void serializeAsPojo(String requestUrl) throws Exception { + private void serializeAsPojo(String requestUrl) throws Exception { RestTemplate restTemplate = new RestTemplate(); RequestEntity request = RequestEntity.get(new URI(requestUrl)) .accept(MediaType.APPLICATION_JSON) @@ -226,17 +221,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati assertEquals(new Person("Robert"), response.getBody()); } - public void postAsPojo(String requestUrl) throws Exception { - RestTemplate restTemplate = new RestTemplate(); - RequestEntity request = RequestEntity.post(new URI(requestUrl)) - .accept(MediaType.APPLICATION_JSON) - .body(new Person("Robert")); - ResponseEntity response = restTemplate.exchange(request, Person.class); - - assertEquals(new Person("Robert"), response.getBody()); - } - - public void serializeAsCollection(String requestUrl) throws Exception { + private void serializeAsCollection(String requestUrl) throws Exception { RestTemplate restTemplate = new RestTemplate(); RequestEntity request = RequestEntity.get(new URI(requestUrl)) .accept(MediaType.APPLICATION_JSON) @@ -250,7 +235,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati } - public void capitalizePojo(String requestUrl) throws Exception { + private void capitalizePojo(String requestUrl) throws Exception { RestTemplate restTemplate = new RestTemplate(); RequestEntity request = RequestEntity.post(new URI(requestUrl)) .contentType(MediaType.APPLICATION_JSON) @@ -261,8 +246,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati assertEquals(new Person("ROBERT"), response.getBody()); } - - public void capitalizeCollection(String requestUrl) throws Exception { + private void capitalizeCollection(String requestUrl) throws Exception { RestTemplate restTemplate = new RestTemplate(); RequestEntity> request = RequestEntity.post(new URI(requestUrl)) .contentType(MediaType.APPLICATION_JSON) @@ -277,6 +261,57 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati } + @Configuration + @SuppressWarnings("unused") + static class FrameworkConfig { + + @Bean + public RequestMappingHandlerMapping handlerMapping() { + return new RequestMappingHandlerMapping(); + } + + @Bean + public RequestMappingHandlerAdapter handlerAdapter() { + RequestMappingHandlerAdapter handlerAdapter = new RequestMappingHandlerAdapter(); + handlerAdapter.setConversionService(conversionService()); + return handlerAdapter; + } + + @Bean + public ConversionService conversionService() { + // TODO: test failures with DefaultConversionService + GenericConversionService service = new GenericConversionService(); + service.addConverter(new ReactiveStreamsToCompletableFutureConverter()); + service.addConverter(new ReactiveStreamsToReactorConverter()); + service.addConverter(new ReactiveStreamsToRxJava1Converter()); + return service; + } + + @Bean + public ResponseBodyResultHandler responseBodyResultHandler() { + return new ResponseBodyResultHandler(Arrays.asList( + new ByteBufferEncoder(), new StringEncoder(),new JacksonJsonEncoder()), + conversionService()); + } + + @Bean + public SimpleHandlerResultHandler simpleHandlerResultHandler() { + return new SimpleHandlerResultHandler(); + } + + } + + @Configuration + @SuppressWarnings("unused") + static class ApplicationConfig { + + @Bean + public TestController testController() { + return new TestController(); + } + } + + @Controller @SuppressWarnings("unused") private static class TestController { @@ -427,6 +462,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati private String name; + @SuppressWarnings("unused") public Person() { } @@ -444,12 +480,19 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @Override public boolean equals(Object o) { - return name.equals(((Person)o).name); + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Person person = (Person) o; + return !(this.name != null ? !this.name.equals(person.name) : person.name != null); } @Override public int hashCode() { - return name.hashCode(); + return this.name != null ? this.name.hashCode() : 0; } } diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandlerTests.java index c37f6dc19f..ea97ee81a9 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandlerTests.java @@ -18,15 +18,18 @@ package org.springframework.reactive.web.dispatch.method.annotation; import java.util.Collections; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; import org.junit.Test; import org.reactivestreams.Publisher; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.reactive.codec.encoder.StringEncoder; import org.springframework.reactive.web.dispatch.HandlerResult; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.method.HandlerMethod; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + /** * @author Sebastien Deleuze */ @@ -35,17 +38,18 @@ public class ResponseBodyResultHandlerTests { @Test public void supports() throws NoSuchMethodException { - ResponseBodyResultHandler resultHandler = new ResponseBodyResultHandler(Collections.emptyList()); + ResponseBodyResultHandler handler = new ResponseBodyResultHandler(Collections.singletonList( + new StringEncoder()), new DefaultConversionService()); TestController controller = new TestController(); HandlerMethod hm = new HandlerMethod(controller,TestController.class.getMethod("notAnnotated")); - assertFalse(resultHandler.supports(new HandlerResult(hm, null))); + assertFalse(handler.supports(new HandlerResult(hm, null))); hm = new HandlerMethod(controller, TestController.class.getMethod("publisherString")); - assertTrue(resultHandler.supports(new HandlerResult(hm, null))); + assertTrue(handler.supports(new HandlerResult(hm, null))); hm = new HandlerMethod(controller, TestController.class.getMethod("publisherVoid")); - assertTrue(resultHandler.supports(new HandlerResult(hm, null))); + assertTrue(handler.supports(new HandlerResult(hm, null))); } From 4c84117155c4acc69e826f217ad1fe1336e32b27 Mon Sep 17 00:00:00 2001 From: Marek Hawrylczak Date: Sat, 7 Nov 2015 15:52:47 +0100 Subject: [PATCH 082/498] =?UTF-8?q?undertow=20support=20using=20non-blocki?= =?UTF-8?q?ng=20API=E2=80=99s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spring-web-reactive/build.gradle | 2 + .../http/undertow/RequestBodyPublisher.java | 240 ++++++++++++++++++ .../http/undertow/RequestHandlerAdapter.java | 79 ++++++ .../http/undertow/ResponseBodySubscriber.java | 200 +++++++++++++++ .../web/http/undertow/UndertowHttpServer.java | 70 +++++ .../undertow/UndertowServerHttpRequest.java | 86 +++++++ .../undertow/UndertowServerHttpResponse.java | 113 +++++++++ .../AbstractHttpHandlerIntegrationTests.java | 4 +- 8 files changed, 793 insertions(+), 1 deletion(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/RequestBodyPublisher.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/RequestHandlerAdapter.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/ResponseBodySubscriber.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/UndertowHttpServer.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/UndertowServerHttpRequest.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/UndertowServerHttpResponse.java diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index 1f416da579..2a951cf6ac 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -47,6 +47,8 @@ dependencies { optional 'org.apache.tomcat:tomcat-util:8.0.28' optional 'org.apache.tomcat.embed:tomcat-embed-core:8.0.28' + optional 'io.undertow:undertow-core:1.3.5.Final' + optional 'org.eclipse.jetty:jetty-server:9.3.5.v20151012' optional 'org.eclipse.jetty:jetty-servlet:9.3.5.v20151012' diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/RequestBodyPublisher.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/RequestBodyPublisher.java new file mode 100644 index 0000000000..c5d5bebacd --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/RequestBodyPublisher.java @@ -0,0 +1,240 @@ +/* + * Copyright 2002-2015 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.reactive.web.http.undertow; + +import static org.xnio.IoUtils.safeClose; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicLongFieldUpdater; + +import org.springframework.util.Assert; + +import io.undertow.connector.PooledByteBuffer; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.SameThreadExecutor; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import org.xnio.ChannelListener; +import org.xnio.channels.StreamSourceChannel; +import reactor.core.error.SpecificationExceptions; +import reactor.core.support.BackpressureUtils; + +/** + * @author Marek Hawrylczak + */ +class RequestBodyPublisher implements Publisher { + + private static final AtomicLongFieldUpdater DEMAND = + AtomicLongFieldUpdater.newUpdater(RequestBodySubscription.class, "demand"); + private final HttpServerExchange exchange; + private Subscriber subscriber; + + public RequestBodyPublisher(HttpServerExchange exchange) { + Assert.notNull(exchange, "'exchange' is required."); + this.exchange = exchange; + } + + @Override + public void subscribe(Subscriber s) { + if (s == null) { + throw SpecificationExceptions.spec_2_13_exception(); + } + if (subscriber != null) { + s.onError(new IllegalStateException("Only one subscriber allowed")); + } + + subscriber = s; + subscriber.onSubscribe(new RequestBodySubscription()); + } + + private class RequestBodySubscription + implements Subscription, Runnable, ChannelListener { + + volatile long demand; + private PooledByteBuffer pooledBuffer; + private StreamSourceChannel channel; + private boolean subscriptionClosed; + private boolean draining; + + @Override + public void cancel() { + subscriptionClosed = true; + close(); + } + + @Override + public void request(long n) { + BackpressureUtils.checkRequest(n, subscriber); + + if (subscriptionClosed) { + return; + } + + BackpressureUtils.getAndAdd(DEMAND, this, n); + scheduleNextMessage(); + } + + private void scheduleNextMessage() { + exchange.dispatch(exchange.isInIoThread() ? + SameThreadExecutor.INSTANCE : exchange.getIoThread(), this); + } + + private void doOnNext(ByteBuffer buffer) { + draining = false; + buffer.flip(); + subscriber.onNext(buffer); + } + + private void doOnComplete() { + subscriptionClosed = true; + try { + subscriber.onComplete(); + } + finally { + close(); + } + } + + private void doOnError(Throwable t) { + subscriptionClosed = true; + try { + subscriber.onError(t); + } + finally { + close(); + } + } + + private void close() { + if (pooledBuffer != null) { + safeClose(pooledBuffer); + pooledBuffer = null; + } + if (channel != null) { + safeClose(channel); + channel = null; + } + } + + @Override + public void run() { + if (subscriptionClosed || draining) { + return; + } + + if (0 == BackpressureUtils.getAndSub(DEMAND, this, 1)) { + return; + } + + draining = true; + + if (channel == null) { + channel = exchange.getRequestChannel(); + + if (channel == null) { + if (exchange.isRequestComplete()) { + return; + } + else { + throw new IllegalStateException( + "Another party already acquired the channel!"); + } + } + } + if (pooledBuffer == null) { + pooledBuffer = exchange.getConnection().getByteBufferPool().allocate(); + } + else { + pooledBuffer.getBuffer().clear(); + } + + try { + ByteBuffer buffer = pooledBuffer.getBuffer(); + int count; + do { + count = channel.read(buffer); + if (count == 0) { + channel.getReadSetter().set(this); + channel.resumeReads(); + } + else if (count == -1) { + if (buffer.position() > 0) { + doOnNext(buffer); + } + doOnComplete(); + } + else { + if (buffer.remaining() == 0) { + if (demand == 0) { + channel.suspendReads(); + } + doOnNext(buffer); + if (demand > 0) { + scheduleNextMessage(); + } + break; + } + } + } while (count > 0); + } + catch (IOException e) { + doOnError(e); + } + } + + @Override + public void handleEvent(StreamSourceChannel channel) { + if (subscriptionClosed) { + return; + } + + try { + ByteBuffer buffer = pooledBuffer.getBuffer(); + int count; + do { + count = channel.read(buffer); + if (count == 0) { + return; + } + else if (count == -1) { + if (buffer.position() > 0) { + doOnNext(buffer); + } + doOnComplete(); + } + else { + if (buffer.remaining() == 0) { + if (demand == 0) { + channel.suspendReads(); + } + doOnNext(buffer); + if (demand > 0) { + scheduleNextMessage(); + } + break; + } + } + } while (count > 0); + } + catch (IOException e) { + doOnError(e); + } + } + } +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/RequestHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/RequestHandlerAdapter.java new file mode 100644 index 0000000000..666b8ca7c5 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/RequestHandlerAdapter.java @@ -0,0 +1,79 @@ +/* + * Copyright 2002-2015 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.reactive.web.http.undertow; + +import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; + +import org.springframework.http.server.ReactiveServerHttpRequest; +import org.springframework.http.server.ReactiveServerHttpResponse; +import org.springframework.reactive.web.http.HttpHandler; +import org.springframework.util.Assert; + +import io.undertow.server.HttpServerExchange; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +/** + * @author Marek Hawrylczak + */ +class RequestHandlerAdapter implements io.undertow.server.HttpHandler { + + private final HttpHandler httpHandler; + + public RequestHandlerAdapter(HttpHandler httpHandler) { + Assert.notNull(httpHandler, "'httpHandler' is required."); + this.httpHandler = httpHandler; + } + + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception { + RequestBodyPublisher requestBodyPublisher = new RequestBodyPublisher(exchange); + ReactiveServerHttpRequest request = + new UndertowServerHttpRequest(exchange, requestBodyPublisher); + + ResponseBodySubscriber responseBodySubscriber = new ResponseBodySubscriber(exchange); + ReactiveServerHttpResponse response = + new UndertowServerHttpResponse(exchange, responseBodySubscriber); + + exchange.dispatch(); + httpHandler.handle(request, response).subscribe(new Subscriber() { + @Override + public void onSubscribe(Subscription subscription) { + subscription.request(Long.MAX_VALUE); + } + + @Override + public void onNext(Void aVoid) { + } + + @Override + public void onError(Throwable t) { + if (!exchange.isResponseStarted() && + exchange.getStatusCode() < INTERNAL_SERVER_ERROR.value()) { + + exchange.setStatusCode(INTERNAL_SERVER_ERROR.value()); + } + exchange.endExchange(); + } + + @Override + public void onComplete() { + exchange.endExchange(); + } + }); + } +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/ResponseBodySubscriber.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/ResponseBodySubscriber.java new file mode 100644 index 0000000000..06f7a76cea --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/ResponseBodySubscriber.java @@ -0,0 +1,200 @@ +/* + * Copyright 2002-2015 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.reactive.web.http.undertow; + +import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; +import static org.xnio.ChannelListeners.closingChannelExceptionHandler; +import static org.xnio.ChannelListeners.flushingChannelListener; +import static org.xnio.IoUtils.safeClose; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import io.undertow.connector.PooledByteBuffer; +import io.undertow.server.HttpServerExchange; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.reactivestreams.Subscription; +import org.xnio.ChannelListener; +import org.xnio.channels.StreamSinkChannel; +import reactor.core.subscriber.BaseSubscriber; + +/** + * @author Marek Hawrylczak + */ +class ResponseBodySubscriber extends BaseSubscriber + implements ChannelListener { + + private static final Log logger = LogFactory.getLog(ResponseBodySubscriber.class); + + private final HttpServerExchange exchange; + private final Queue buffers; + private final AtomicInteger writing = new AtomicInteger(); + private final AtomicBoolean closing = new AtomicBoolean(); + private StreamSinkChannel responseChannel; + private Subscription subscription; + + public ResponseBodySubscriber(HttpServerExchange exchange) { + this.exchange = exchange; + this.buffers = new ConcurrentLinkedQueue<>(); + } + + @Override + public void onSubscribe(Subscription s) { + super.onSubscribe(s); + subscription = s; + subscription.request(1); + } + + @Override + public void onNext(ByteBuffer buffer) { + super.onNext(buffer); + + if (responseChannel == null) { + responseChannel = exchange.getResponseChannel(); + } + + writing.incrementAndGet(); + try { + int c; + do { + c = responseChannel.write(buffer); + } while (buffer.hasRemaining() && c > 0); + if (buffer.hasRemaining()) { + writing.incrementAndGet(); + enqueue(buffer); + responseChannel.getWriteSetter().set(this); + responseChannel.resumeWrites(); + } + else { + this.subscription.request(1); + } + + } + catch (IOException ex) { + onError(ex); + } + finally { + writing.decrementAndGet(); + if (closing.get()) { + closeIfDone(); + } + } + } + + private void enqueue(ByteBuffer src) { + do { + PooledByteBuffer pooledBuffer = + exchange.getConnection().getByteBufferPool().allocate(); + + ByteBuffer dst = pooledBuffer.getBuffer(); + copy(dst, src); + dst.flip(); + buffers.add(pooledBuffer); + } while (src.remaining() > 0); + } + + private void copy(ByteBuffer dst, ByteBuffer src) { + int n = Math.min(dst.capacity(), src.remaining()); + for (int i = 0; i < n; i++) { + dst.put(src.get()); + } + } + + @Override + public void handleEvent(StreamSinkChannel channel) { + try { + int c; + do { + ByteBuffer buffer = buffers.peek().getBuffer(); + do { + c = channel.write(buffer); + } while (buffer.hasRemaining() && c > 0); + if (!buffer.hasRemaining()) { + safeClose(buffers.remove()); + } + } while (!buffers.isEmpty() && c > 0); + if (!buffers.isEmpty()) { + channel.resumeWrites(); + } + else { + writing.decrementAndGet(); + + if (closing.get()) { + closeIfDone(); + } + else { + subscription.request(1); + } + } + } + catch (IOException ex) { + onError(ex); + } + } + + @Override + public void onError(Throwable t) { + super.onError(t); + if (!exchange.isResponseStarted() && + exchange.getStatusCode() < INTERNAL_SERVER_ERROR.value()) { + + exchange.setStatusCode(INTERNAL_SERVER_ERROR.value()); + } + logger.error("ResponseBodySubscriber error", t); + } + + @Override + public void onComplete() { + super.onComplete(); + + if (responseChannel != null) { + closing.set(true); + closeIfDone(); + } + } + + private void closeIfDone() { + if (writing.get() == 0) { + if (closing.compareAndSet(true, false)) { + closeChannel(); + } + } + } + + private void closeChannel() { + try { + responseChannel.shutdownWrites(); + + if (!responseChannel.flush()) { + responseChannel.getWriteSetter().set( + flushingChannelListener( + o -> safeClose(responseChannel), + closingChannelExceptionHandler())); + responseChannel.resumeWrites(); + } + responseChannel = null; + } + catch (IOException ex) { + onError(ex); + } + } +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/UndertowHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/UndertowHttpServer.java new file mode 100644 index 0000000000..262ed92869 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/UndertowHttpServer.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2015 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.reactive.web.http.undertow; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.reactive.web.http.HttpServer; +import org.springframework.reactive.web.http.HttpServerSupport; +import org.springframework.util.Assert; + +import io.undertow.Undertow; +import io.undertow.server.HttpHandler; + +/** + * @author Marek Hawrylczak + */ +public class UndertowHttpServer extends HttpServerSupport + implements InitializingBean, HttpServer { + + private Undertow undertowServer; + + private boolean running; + + @Override + public void afterPropertiesSet() throws Exception { + Assert.notNull(getHttpHandler()); + + HttpHandler handler = new RequestHandlerAdapter(getHttpHandler()); + + undertowServer = Undertow.builder() + .addHttpListener(getPort() != -1 ? getPort() : 8080, "localhost") + .setHandler(handler) + .build(); + } + + @Override + public void start() { + if (!running) { + undertowServer.start(); + running = true; + } + + } + + @Override + public void stop() { + if (running) { + undertowServer.stop(); + running = false; + } + } + + @Override + public boolean isRunning() { + return running; + } +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/UndertowServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/UndertowServerHttpRequest.java new file mode 100644 index 0000000000..32b5fa2b99 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/UndertowServerHttpRequest.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002-2015 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.reactive.web.http.undertow; + +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.ByteBuffer; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.server.ReactiveServerHttpRequest; +import org.springframework.util.StringUtils; + +import io.undertow.server.HttpServerExchange; +import io.undertow.util.HeaderValues; +import org.reactivestreams.Publisher; + +/** + * @author Marek Hawrylczak + */ +class UndertowServerHttpRequest implements ReactiveServerHttpRequest { + + private final HttpServerExchange exchange; + + private final Publisher requestBodyPublisher; + + private HttpHeaders headers; + + public UndertowServerHttpRequest(HttpServerExchange exchange, + Publisher requestBodyPublisher) { + + this.exchange = exchange; + this.requestBodyPublisher = requestBodyPublisher; + } + + @Override + public Publisher getBody() { + return this.requestBodyPublisher; + } + + @Override + public HttpMethod getMethod() { + return HttpMethod.valueOf(exchange.getRequestMethod().toString()); + } + + @Override + public URI getURI() { + try { + StringBuilder uri = new StringBuilder(exchange.getRequestPath()); + if (StringUtils.hasLength(exchange.getQueryString())) { + uri.append('?').append(exchange.getQueryString()); + } + return new URI(uri.toString()); + } + catch (URISyntaxException ex) { + throw new IllegalStateException("Could not get URI: " + ex.getMessage(), ex); + } + } + + @Override + public HttpHeaders getHeaders() { + if (this.headers == null) { + this.headers = new HttpHeaders(); + for (HeaderValues headerValues : exchange.getRequestHeaders()) { + for (String value : headerValues) { + this.headers.add(headerValues.getHeaderName().toString(), value); + } + } + } + return this.headers; + } +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/UndertowServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/UndertowServerHttpResponse.java new file mode 100644 index 0000000000..3c68dc41e5 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/UndertowServerHttpResponse.java @@ -0,0 +1,113 @@ +/* + * Copyright 2002-2015 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.reactive.web.http.undertow; + +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Map; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.server.ReactiveServerHttpResponse; + +import io.undertow.server.HttpServerExchange; +import io.undertow.util.HttpString; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscription; +import reactor.rx.Streams; + +/** + * @author Marek Hawrylczak + */ +class UndertowServerHttpResponse implements ReactiveServerHttpResponse { + private final HttpServerExchange exchange; + private final HttpHeaders headers; + + private final ResponseBodySubscriber responseBodySubscriber; + + private boolean headersWritten = false; + + public UndertowServerHttpResponse(HttpServerExchange exchange, + ResponseBodySubscriber responseBodySubscriber) { + + this.exchange = exchange; + this.responseBodySubscriber = responseBodySubscriber; + this.headers = new HttpHeaders(); + } + + @Override + public void setStatusCode(HttpStatus status) { + exchange.setStatusCode(status.value()); + } + + @Override + public Publisher setBody(Publisher contentPublisher) { + applyHeaders(); + return s -> s.onSubscribe(new Subscription() { + @Override + public void request(long n) { + Streams.wrap(contentPublisher) + .finallyDo(byteBufferSignal -> { + if (byteBufferSignal.isOnComplete()) { + s.onComplete(); + } + else { + s.onError(byteBufferSignal.getThrowable()); + } + } + ).subscribe(responseBodySubscriber); + } + + @Override + public void cancel() { + } + }); + } + + @Override + public HttpHeaders getHeaders() { + return (this.headersWritten ? + HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers); + } + + @Override + public Publisher writeHeaders() { + applyHeaders(); + return s -> s.onSubscribe(new Subscription() { + @Override + public void request(long n) { + s.onComplete(); + } + + @Override + public void cancel() { + } + }); + } + + private void applyHeaders() { + if (!this.headersWritten) { + for (Map.Entry> entry : this.headers.entrySet()) { + String headerName = entry.getKey(); + exchange.getResponseHeaders() + .addAll(HttpString.tryFromString(headerName), entry.getValue()); + + } + this.headersWritten = true; + } + } +} diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/AbstractHttpHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/AbstractHttpHandlerIntegrationTests.java index e03b75180d..41136ac8b0 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/AbstractHttpHandlerIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/AbstractHttpHandlerIntegrationTests.java @@ -25,6 +25,7 @@ import org.springframework.reactive.web.http.reactor.ReactorHttpServer; import org.springframework.reactive.web.http.rxnetty.RxNettyHttpServer; import org.springframework.reactive.web.http.servlet.JettyHttpServer; import org.springframework.reactive.web.http.servlet.TomcatHttpServer; +import org.springframework.reactive.web.http.undertow.UndertowHttpServer; import org.springframework.util.SocketUtils; @@ -43,7 +44,8 @@ public abstract class AbstractHttpHandlerIntegrationTests { {new JettyHttpServer()}, {new RxNettyHttpServer()}, {new ReactorHttpServer()}, - {new TomcatHttpServer()} + {new TomcatHttpServer()}, + {new UndertowHttpServer()} }; } From 9126fd9dec35a95f6363937490630a7e9173089d Mon Sep 17 00:00:00 2001 From: Marek Hawrylczak Date: Sat, 7 Nov 2015 16:02:09 +0100 Subject: [PATCH 083/498] Indentation using tabs --- spring-web-reactive/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index 2a951cf6ac..fee4846c00 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -47,7 +47,7 @@ dependencies { optional 'org.apache.tomcat:tomcat-util:8.0.28' optional 'org.apache.tomcat.embed:tomcat-embed-core:8.0.28' - optional 'io.undertow:undertow-core:1.3.5.Final' + optional 'io.undertow:undertow-core:1.3.5.Final' optional 'org.eclipse.jetty:jetty-server:9.3.5.v20151012' optional 'org.eclipse.jetty:jetty-servlet:9.3.5.v20151012' From f1bec5f1e4c677807f9be42fb32df759990b2fc4 Mon Sep 17 00:00:00 2001 From: Marek Hawrylczak Date: Sat, 7 Nov 2015 16:37:40 +0100 Subject: [PATCH 084/498] referencing fields of a class using this --- .../http/undertow/RequestBodyPublisher.java | 66 +++++++++---------- .../http/undertow/RequestHandlerAdapter.java | 2 +- .../http/undertow/ResponseBodySubscriber.java | 66 +++++++++---------- .../web/http/undertow/UndertowHttpServer.java | 6 +- .../undertow/UndertowServerHttpRequest.java | 10 +-- .../undertow/UndertowServerHttpResponse.java | 4 +- 6 files changed, 77 insertions(+), 77 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/RequestBodyPublisher.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/RequestBodyPublisher.java index c5d5bebacd..ffa21597dd 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/RequestBodyPublisher.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/RequestBodyPublisher.java @@ -55,12 +55,12 @@ class RequestBodyPublisher implements Publisher { if (s == null) { throw SpecificationExceptions.spec_2_13_exception(); } - if (subscriber != null) { + if (this.subscriber != null) { s.onError(new IllegalStateException("Only one subscriber allowed")); } - subscriber = s; - subscriber.onSubscribe(new RequestBodySubscription()); + this.subscriber = s; + this.subscriber.onSubscribe(new RequestBodySubscription()); } private class RequestBodySubscription @@ -74,7 +74,7 @@ class RequestBodyPublisher implements Publisher { @Override public void cancel() { - subscriptionClosed = true; + this.subscriptionClosed = true; close(); } @@ -82,7 +82,7 @@ class RequestBodyPublisher implements Publisher { public void request(long n) { BackpressureUtils.checkRequest(n, subscriber); - if (subscriptionClosed) { + if (this.subscriptionClosed) { return; } @@ -96,13 +96,13 @@ class RequestBodyPublisher implements Publisher { } private void doOnNext(ByteBuffer buffer) { - draining = false; + this.draining = false; buffer.flip(); subscriber.onNext(buffer); } private void doOnComplete() { - subscriptionClosed = true; + this.subscriptionClosed = true; try { subscriber.onComplete(); } @@ -112,7 +112,7 @@ class RequestBodyPublisher implements Publisher { } private void doOnError(Throwable t) { - subscriptionClosed = true; + this.subscriptionClosed = true; try { subscriber.onError(t); } @@ -122,19 +122,19 @@ class RequestBodyPublisher implements Publisher { } private void close() { - if (pooledBuffer != null) { - safeClose(pooledBuffer); - pooledBuffer = null; + if (this.pooledBuffer != null) { + safeClose(this.pooledBuffer); + this.pooledBuffer = null; } - if (channel != null) { - safeClose(channel); - channel = null; + if (this.channel != null) { + safeClose(this.channel); + this.channel = null; } } @Override public void run() { - if (subscriptionClosed || draining) { + if (this.subscriptionClosed || this.draining) { return; } @@ -142,12 +142,12 @@ class RequestBodyPublisher implements Publisher { return; } - draining = true; + this.draining = true; - if (channel == null) { - channel = exchange.getRequestChannel(); + if (this.channel == null) { + this.channel = exchange.getRequestChannel(); - if (channel == null) { + if (this.channel == null) { if (exchange.isRequestComplete()) { return; } @@ -157,21 +157,21 @@ class RequestBodyPublisher implements Publisher { } } } - if (pooledBuffer == null) { - pooledBuffer = exchange.getConnection().getByteBufferPool().allocate(); + if (this.pooledBuffer == null) { + this.pooledBuffer = exchange.getConnection().getByteBufferPool().allocate(); } else { - pooledBuffer.getBuffer().clear(); + this.pooledBuffer.getBuffer().clear(); } try { - ByteBuffer buffer = pooledBuffer.getBuffer(); + ByteBuffer buffer = this.pooledBuffer.getBuffer(); int count; do { - count = channel.read(buffer); + count = this.channel.read(buffer); if (count == 0) { - channel.getReadSetter().set(this); - channel.resumeReads(); + this.channel.getReadSetter().set(this); + this.channel.resumeReads(); } else if (count == -1) { if (buffer.position() > 0) { @@ -181,11 +181,11 @@ class RequestBodyPublisher implements Publisher { } else { if (buffer.remaining() == 0) { - if (demand == 0) { - channel.suspendReads(); + if (this.demand == 0) { + this.channel.suspendReads(); } doOnNext(buffer); - if (demand > 0) { + if (this.demand > 0) { scheduleNextMessage(); } break; @@ -200,12 +200,12 @@ class RequestBodyPublisher implements Publisher { @Override public void handleEvent(StreamSourceChannel channel) { - if (subscriptionClosed) { + if (this.subscriptionClosed) { return; } try { - ByteBuffer buffer = pooledBuffer.getBuffer(); + ByteBuffer buffer = this.pooledBuffer.getBuffer(); int count; do { count = channel.read(buffer); @@ -220,11 +220,11 @@ class RequestBodyPublisher implements Publisher { } else { if (buffer.remaining() == 0) { - if (demand == 0) { + if (this.demand == 0) { channel.suspendReads(); } doOnNext(buffer); - if (demand > 0) { + if (this.demand > 0) { scheduleNextMessage(); } break; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/RequestHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/RequestHandlerAdapter.java index 666b8ca7c5..b87f126818 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/RequestHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/RequestHandlerAdapter.java @@ -50,7 +50,7 @@ class RequestHandlerAdapter implements io.undertow.server.HttpHandler { new UndertowServerHttpResponse(exchange, responseBodySubscriber); exchange.dispatch(); - httpHandler.handle(request, response).subscribe(new Subscriber() { + this.httpHandler.handle(request, response).subscribe(new Subscriber() { @Override public void onSubscribe(Subscription subscription) { subscription.request(Long.MAX_VALUE); diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/ResponseBodySubscriber.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/ResponseBodySubscriber.java index 06f7a76cea..ae42aa1a2d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/ResponseBodySubscriber.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/ResponseBodySubscriber.java @@ -60,29 +60,29 @@ class ResponseBodySubscriber extends BaseSubscriber @Override public void onSubscribe(Subscription s) { super.onSubscribe(s); - subscription = s; - subscription.request(1); + this.subscription = s; + this.subscription.request(1); } @Override public void onNext(ByteBuffer buffer) { super.onNext(buffer); - if (responseChannel == null) { - responseChannel = exchange.getResponseChannel(); + if (this.responseChannel == null) { + this.responseChannel = this.exchange.getResponseChannel(); } - writing.incrementAndGet(); + this.writing.incrementAndGet(); try { int c; do { - c = responseChannel.write(buffer); + c = this.responseChannel.write(buffer); } while (buffer.hasRemaining() && c > 0); if (buffer.hasRemaining()) { - writing.incrementAndGet(); + this.writing.incrementAndGet(); enqueue(buffer); - responseChannel.getWriteSetter().set(this); - responseChannel.resumeWrites(); + this.responseChannel.getWriteSetter().set(this); + this.responseChannel.resumeWrites(); } else { this.subscription.request(1); @@ -93,8 +93,8 @@ class ResponseBodySubscriber extends BaseSubscriber onError(ex); } finally { - writing.decrementAndGet(); - if (closing.get()) { + this.writing.decrementAndGet(); + if (this.closing.get()) { closeIfDone(); } } @@ -103,12 +103,12 @@ class ResponseBodySubscriber extends BaseSubscriber private void enqueue(ByteBuffer src) { do { PooledByteBuffer pooledBuffer = - exchange.getConnection().getByteBufferPool().allocate(); + this.exchange.getConnection().getByteBufferPool().allocate(); ByteBuffer dst = pooledBuffer.getBuffer(); copy(dst, src); dst.flip(); - buffers.add(pooledBuffer); + this.buffers.add(pooledBuffer); } while (src.remaining() > 0); } @@ -124,25 +124,25 @@ class ResponseBodySubscriber extends BaseSubscriber try { int c; do { - ByteBuffer buffer = buffers.peek().getBuffer(); + ByteBuffer buffer = this.buffers.peek().getBuffer(); do { c = channel.write(buffer); } while (buffer.hasRemaining() && c > 0); if (!buffer.hasRemaining()) { - safeClose(buffers.remove()); + safeClose(this.buffers.remove()); } - } while (!buffers.isEmpty() && c > 0); - if (!buffers.isEmpty()) { + } while (!this.buffers.isEmpty() && c > 0); + if (!this.buffers.isEmpty()) { channel.resumeWrites(); } else { - writing.decrementAndGet(); + this.writing.decrementAndGet(); - if (closing.get()) { + if (this.closing.get()) { closeIfDone(); } else { - subscription.request(1); + this.subscription.request(1); } } } @@ -154,10 +154,10 @@ class ResponseBodySubscriber extends BaseSubscriber @Override public void onError(Throwable t) { super.onError(t); - if (!exchange.isResponseStarted() && - exchange.getStatusCode() < INTERNAL_SERVER_ERROR.value()) { + if (!this.exchange.isResponseStarted() && + this.exchange.getStatusCode() < INTERNAL_SERVER_ERROR.value()) { - exchange.setStatusCode(INTERNAL_SERVER_ERROR.value()); + this.exchange.setStatusCode(INTERNAL_SERVER_ERROR.value()); } logger.error("ResponseBodySubscriber error", t); } @@ -166,15 +166,15 @@ class ResponseBodySubscriber extends BaseSubscriber public void onComplete() { super.onComplete(); - if (responseChannel != null) { - closing.set(true); + if (this.responseChannel != null) { + this.closing.set(true); closeIfDone(); } } private void closeIfDone() { - if (writing.get() == 0) { - if (closing.compareAndSet(true, false)) { + if (this.writing.get() == 0) { + if (this.closing.compareAndSet(true, false)) { closeChannel(); } } @@ -182,16 +182,16 @@ class ResponseBodySubscriber extends BaseSubscriber private void closeChannel() { try { - responseChannel.shutdownWrites(); + this.responseChannel.shutdownWrites(); - if (!responseChannel.flush()) { - responseChannel.getWriteSetter().set( + if (!this.responseChannel.flush()) { + this.responseChannel.getWriteSetter().set( flushingChannelListener( - o -> safeClose(responseChannel), + o -> safeClose(this.responseChannel), closingChannelExceptionHandler())); - responseChannel.resumeWrites(); + this.responseChannel.resumeWrites(); } - responseChannel = null; + this.responseChannel = null; } catch (IOException ex) { onError(ex); diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/UndertowHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/UndertowHttpServer.java index 262ed92869..6a9a747c18 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/UndertowHttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/UndertowHttpServer.java @@ -40,7 +40,7 @@ public class UndertowHttpServer extends HttpServerSupport HttpHandler handler = new RequestHandlerAdapter(getHttpHandler()); - undertowServer = Undertow.builder() + this.undertowServer = Undertow.builder() .addHttpListener(getPort() != -1 ? getPort() : 8080, "localhost") .setHandler(handler) .build(); @@ -49,7 +49,7 @@ public class UndertowHttpServer extends HttpServerSupport @Override public void start() { if (!running) { - undertowServer.start(); + this.undertowServer.start(); running = true; } @@ -58,7 +58,7 @@ public class UndertowHttpServer extends HttpServerSupport @Override public void stop() { if (running) { - undertowServer.stop(); + this.undertowServer.stop(); running = false; } } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/UndertowServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/UndertowServerHttpRequest.java index 32b5fa2b99..4e83006676 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/UndertowServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/UndertowServerHttpRequest.java @@ -54,15 +54,15 @@ class UndertowServerHttpRequest implements ReactiveServerHttpRequest { @Override public HttpMethod getMethod() { - return HttpMethod.valueOf(exchange.getRequestMethod().toString()); + return HttpMethod.valueOf(this.exchange.getRequestMethod().toString()); } @Override public URI getURI() { try { - StringBuilder uri = new StringBuilder(exchange.getRequestPath()); - if (StringUtils.hasLength(exchange.getQueryString())) { - uri.append('?').append(exchange.getQueryString()); + StringBuilder uri = new StringBuilder(this.exchange.getRequestPath()); + if (StringUtils.hasLength(this.exchange.getQueryString())) { + uri.append('?').append(this.exchange.getQueryString()); } return new URI(uri.toString()); } @@ -75,7 +75,7 @@ class UndertowServerHttpRequest implements ReactiveServerHttpRequest { public HttpHeaders getHeaders() { if (this.headers == null) { this.headers = new HttpHeaders(); - for (HeaderValues headerValues : exchange.getRequestHeaders()) { + for (HeaderValues headerValues : this.exchange.getRequestHeaders()) { for (String value : headerValues) { this.headers.add(headerValues.getHeaderName().toString(), value); } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/UndertowServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/UndertowServerHttpResponse.java index 3c68dc41e5..b3bc0fefe9 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/UndertowServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/UndertowServerHttpResponse.java @@ -51,7 +51,7 @@ class UndertowServerHttpResponse implements ReactiveServerHttpResponse { @Override public void setStatusCode(HttpStatus status) { - exchange.setStatusCode(status.value()); + this.exchange.setStatusCode(status.value()); } @Override @@ -103,7 +103,7 @@ class UndertowServerHttpResponse implements ReactiveServerHttpResponse { if (!this.headersWritten) { for (Map.Entry> entry : this.headers.entrySet()) { String headerName = entry.getKey(); - exchange.getResponseHeaders() + this.exchange.getResponseHeaders() .addAll(HttpString.tryFromString(headerName), entry.getValue()); } From 141d75791d029ec72c27cf72c04c9b7625854e2c Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 12 Nov 2015 11:52:06 -0500 Subject: [PATCH 085/498] Polish --- .../http/undertow/RequestBodyPublisher.java | 90 ++++++++++--------- .../http/undertow/ResponseBodySubscriber.java | 62 +++++++------ ...r.java => UndertowHttpHandlerAdapter.java} | 48 ++++++---- .../web/http/undertow/UndertowHttpServer.java | 32 ++++--- .../undertow/UndertowServerHttpRequest.java | 35 ++++---- .../undertow/UndertowServerHttpResponse.java | 53 ++++------- 6 files changed, 162 insertions(+), 158 deletions(-) rename spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/{RequestHandlerAdapter.java => UndertowHttpHandlerAdapter.java} (56%) diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/RequestBodyPublisher.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/RequestBodyPublisher.java index ffa21597dd..db0a0e89e7 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/RequestBodyPublisher.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/RequestBodyPublisher.java @@ -42,83 +42,66 @@ class RequestBodyPublisher implements Publisher { private static final AtomicLongFieldUpdater DEMAND = AtomicLongFieldUpdater.newUpdater(RequestBodySubscription.class, "demand"); + + private final HttpServerExchange exchange; + private Subscriber subscriber; + public RequestBodyPublisher(HttpServerExchange exchange) { Assert.notNull(exchange, "'exchange' is required."); this.exchange = exchange; } + @Override - public void subscribe(Subscriber s) { - if (s == null) { + public void subscribe(Subscriber subscriber) { + if (subscriber == null) { throw SpecificationExceptions.spec_2_13_exception(); } if (this.subscriber != null) { - s.onError(new IllegalStateException("Only one subscriber allowed")); + subscriber.onError(new IllegalStateException("Only one subscriber allowed")); } - this.subscriber = s; + this.subscriber = subscriber; this.subscriber.onSubscribe(new RequestBodySubscription()); } - private class RequestBodySubscription - implements Subscription, Runnable, ChannelListener { + + private class RequestBodySubscription implements Subscription, Runnable, + ChannelListener { volatile long demand; + private PooledByteBuffer pooledBuffer; + private StreamSourceChannel channel; + private boolean subscriptionClosed; + private boolean draining; - @Override - public void cancel() { - this.subscriptionClosed = true; - close(); - } @Override public void request(long n) { BackpressureUtils.checkRequest(n, subscriber); - if (this.subscriptionClosed) { return; } - BackpressureUtils.getAndAdd(DEMAND, this, n); scheduleNextMessage(); } private void scheduleNextMessage() { - exchange.dispatch(exchange.isInIoThread() ? - SameThreadExecutor.INSTANCE : exchange.getIoThread(), this); + exchange.dispatch(exchange.isInIoThread() ? SameThreadExecutor.INSTANCE : + exchange.getIoThread(), this); } - private void doOnNext(ByteBuffer buffer) { - this.draining = false; - buffer.flip(); - subscriber.onNext(buffer); - } - - private void doOnComplete() { + @Override + public void cancel() { this.subscriptionClosed = true; - try { - subscriber.onComplete(); - } - finally { - close(); - } - } - - private void doOnError(Throwable t) { - this.subscriptionClosed = true; - try { - subscriber.onError(t); - } - finally { - close(); - } + close(); } private void close() { @@ -137,7 +120,6 @@ class RequestBodyPublisher implements Publisher { if (this.subscriptionClosed || this.draining) { return; } - if (0 == BackpressureUtils.getAndSub(DEMAND, this, 1)) { return; } @@ -152,8 +134,7 @@ class RequestBodyPublisher implements Publisher { return; } else { - throw new IllegalStateException( - "Another party already acquired the channel!"); + throw new IllegalStateException("Failed to acquire channel!"); } } } @@ -198,6 +179,32 @@ class RequestBodyPublisher implements Publisher { } } + private void doOnNext(ByteBuffer buffer) { + this.draining = false; + buffer.flip(); + subscriber.onNext(buffer); + } + + private void doOnComplete() { + this.subscriptionClosed = true; + try { + subscriber.onComplete(); + } + finally { + close(); + } + } + + private void doOnError(Throwable t) { + this.subscriptionClosed = true; + try { + subscriber.onError(t); + } + finally { + close(); + } + } + @Override public void handleEvent(StreamSourceChannel channel) { if (this.subscriptionClosed) { @@ -237,4 +244,5 @@ class RequestBodyPublisher implements Publisher { } } } + } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/ResponseBodySubscriber.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/ResponseBodySubscriber.java index ae42aa1a2d..b78a687fa6 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/ResponseBodySubscriber.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/ResponseBodySubscriber.java @@ -16,11 +16,6 @@ package org.springframework.reactive.web.http.undertow; -import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; -import static org.xnio.ChannelListeners.closingChannelExceptionHandler; -import static org.xnio.ChannelListeners.flushingChannelListener; -import static org.xnio.IoUtils.safeClose; - import java.io.IOException; import java.nio.ByteBuffer; import java.util.Queue; @@ -37,30 +32,43 @@ import org.xnio.ChannelListener; import org.xnio.channels.StreamSinkChannel; import reactor.core.subscriber.BaseSubscriber; +import static org.xnio.ChannelListeners.closingChannelExceptionHandler; +import static org.xnio.ChannelListeners.flushingChannelListener; +import static org.xnio.IoUtils.safeClose; + /** * @author Marek Hawrylczak + * @author Rossen Stoyanchev */ class ResponseBodySubscriber extends BaseSubscriber implements ChannelListener { private static final Log logger = LogFactory.getLog(ResponseBodySubscriber.class); + private final HttpServerExchange exchange; - private final Queue buffers; - private final AtomicInteger writing = new AtomicInteger(); - private final AtomicBoolean closing = new AtomicBoolean(); - private StreamSinkChannel responseChannel; + private Subscription subscription; + private final Queue buffers; + + private final AtomicInteger writing = new AtomicInteger(); + + private final AtomicBoolean closing = new AtomicBoolean(); + + private StreamSinkChannel responseChannel; + + public ResponseBodySubscriber(HttpServerExchange exchange) { this.exchange = exchange; this.buffers = new ConcurrentLinkedQueue<>(); } + @Override - public void onSubscribe(Subscription s) { - super.onSubscribe(s); - this.subscription = s; + public void onSubscribe(Subscription subscription) { + super.onSubscribe(subscription); + this.subscription = subscription; this.subscription.request(1); } @@ -78,6 +86,7 @@ class ResponseBodySubscriber extends BaseSubscriber do { c = this.responseChannel.write(buffer); } while (buffer.hasRemaining() && c > 0); + if (buffer.hasRemaining()) { this.writing.incrementAndGet(); enqueue(buffer); @@ -102,13 +111,11 @@ class ResponseBodySubscriber extends BaseSubscriber private void enqueue(ByteBuffer src) { do { - PooledByteBuffer pooledBuffer = - this.exchange.getConnection().getByteBufferPool().allocate(); - - ByteBuffer dst = pooledBuffer.getBuffer(); + PooledByteBuffer buffer = this.exchange.getConnection().getByteBufferPool().allocate(); + ByteBuffer dst = buffer.getBuffer(); copy(dst, src); dst.flip(); - this.buffers.add(pooledBuffer); + this.buffers.add(buffer); } while (src.remaining() > 0); } @@ -128,10 +135,12 @@ class ResponseBodySubscriber extends BaseSubscriber do { c = channel.write(buffer); } while (buffer.hasRemaining() && c > 0); + if (!buffer.hasRemaining()) { safeClose(this.buffers.remove()); } } while (!this.buffers.isEmpty() && c > 0); + if (!this.buffers.isEmpty()) { channel.resumeWrites(); } @@ -152,20 +161,17 @@ class ResponseBodySubscriber extends BaseSubscriber } @Override - public void onError(Throwable t) { - super.onError(t); - if (!this.exchange.isResponseStarted() && - this.exchange.getStatusCode() < INTERNAL_SERVER_ERROR.value()) { - - this.exchange.setStatusCode(INTERNAL_SERVER_ERROR.value()); + public void onError(Throwable ex) { + super.onError(ex); + logger.error("ResponseBodySubscriber error", ex); + if (!this.exchange.isResponseStarted() && this.exchange.getStatusCode() < 500) { + this.exchange.setStatusCode(500); } - logger.error("ResponseBodySubscriber error", t); } @Override public void onComplete() { super.onComplete(); - if (this.responseChannel != null) { this.closing.set(true); closeIfDone(); @@ -185,10 +191,8 @@ class ResponseBodySubscriber extends BaseSubscriber this.responseChannel.shutdownWrites(); if (!this.responseChannel.flush()) { - this.responseChannel.getWriteSetter().set( - flushingChannelListener( - o -> safeClose(this.responseChannel), - closingChannelExceptionHandler())); + this.responseChannel.getWriteSetter().set(flushingChannelListener( + o -> safeClose(this.responseChannel), closingChannelExceptionHandler())); this.responseChannel.resumeWrites(); } this.responseChannel = null; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/RequestHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/UndertowHttpHandlerAdapter.java similarity index 56% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/RequestHandlerAdapter.java rename to spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/UndertowHttpHandlerAdapter.java index b87f126818..92ad9ff3f0 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/RequestHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/UndertowHttpHandlerAdapter.java @@ -16,41 +16,48 @@ package org.springframework.reactive.web.http.undertow; -import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; - import org.springframework.http.server.ReactiveServerHttpRequest; import org.springframework.http.server.ReactiveServerHttpResponse; import org.springframework.reactive.web.http.HttpHandler; import org.springframework.util.Assert; import io.undertow.server.HttpServerExchange; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; + /** * @author Marek Hawrylczak + * @author Rossen Stoyanchev */ -class RequestHandlerAdapter implements io.undertow.server.HttpHandler { +class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandler { - private final HttpHandler httpHandler; + private static Log logger = LogFactory.getLog(UndertowHttpHandlerAdapter.class); - public RequestHandlerAdapter(HttpHandler httpHandler) { - Assert.notNull(httpHandler, "'httpHandler' is required."); - this.httpHandler = httpHandler; + + private final HttpHandler delegate; + + + public UndertowHttpHandlerAdapter(HttpHandler delegate) { + Assert.notNull(delegate, "'delegate' is required."); + this.delegate = delegate; } + @Override public void handleRequest(HttpServerExchange exchange) throws Exception { - RequestBodyPublisher requestBodyPublisher = new RequestBodyPublisher(exchange); - ReactiveServerHttpRequest request = - new UndertowServerHttpRequest(exchange, requestBodyPublisher); + RequestBodyPublisher requestPublisher = new RequestBodyPublisher(exchange); + ReactiveServerHttpRequest request = new UndertowServerHttpRequest(exchange, requestPublisher); - ResponseBodySubscriber responseBodySubscriber = new ResponseBodySubscriber(exchange); - ReactiveServerHttpResponse response = - new UndertowServerHttpResponse(exchange, responseBodySubscriber); + ResponseBodySubscriber responseSubscriber = new ResponseBodySubscriber(exchange); + ReactiveServerHttpResponse response = new UndertowServerHttpResponse(exchange, responseSubscriber); exchange.dispatch(); - this.httpHandler.handle(request, response).subscribe(new Subscriber() { + + this.delegate.handle(request, response).subscribe(new Subscriber() { + @Override public void onSubscribe(Subscription subscription) { subscription.request(Long.MAX_VALUE); @@ -58,14 +65,16 @@ class RequestHandlerAdapter implements io.undertow.server.HttpHandler { @Override public void onNext(Void aVoid) { + // no op } @Override - public void onError(Throwable t) { - if (!exchange.isResponseStarted() && - exchange.getStatusCode() < INTERNAL_SERVER_ERROR.value()) { - - exchange.setStatusCode(INTERNAL_SERVER_ERROR.value()); + public void onError(Throwable ex) { + if (exchange.isResponseStarted() || exchange.getStatusCode() > 500) { + logger.error("Error from request handling. Completing the request.", ex); + } + else { + exchange.setStatusCode(500); } exchange.endExchange(); } @@ -76,4 +85,5 @@ class RequestHandlerAdapter implements io.undertow.server.HttpHandler { } }); } + } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/UndertowHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/UndertowHttpServer.java index 6a9a747c18..b55fa2201f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/UndertowHttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/UndertowHttpServer.java @@ -27,44 +27,42 @@ import io.undertow.server.HttpHandler; /** * @author Marek Hawrylczak */ -public class UndertowHttpServer extends HttpServerSupport - implements InitializingBean, HttpServer { +public class UndertowHttpServer extends HttpServerSupport implements InitializingBean, HttpServer { - private Undertow undertowServer; + private Undertow server; private boolean running; + @Override public void afterPropertiesSet() throws Exception { Assert.notNull(getHttpHandler()); - - HttpHandler handler = new RequestHandlerAdapter(getHttpHandler()); - - this.undertowServer = Undertow.builder() - .addHttpListener(getPort() != -1 ? getPort() : 8080, "localhost") - .setHandler(handler) - .build(); + HttpHandler handler = new UndertowHttpHandlerAdapter(getHttpHandler()); + int port = (getPort() != -1 ? getPort() : 8080); + this.server = Undertow.builder().addHttpListener(port, "localhost") + .setHandler(handler).build(); } @Override public void start() { - if (!running) { - this.undertowServer.start(); - running = true; + if (!this.running) { + this.server.start(); + this.running = true; } } @Override public void stop() { - if (running) { - this.undertowServer.stop(); - running = false; + if (this.running) { + this.server.stop(); + this.running = false; } } @Override public boolean isRunning() { - return running; + return this.running; } + } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/UndertowServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/UndertowServerHttpRequest.java index 4e83006676..6f856595f3 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/UndertowServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/UndertowServerHttpRequest.java @@ -20,37 +20,32 @@ import java.net.URI; import java.net.URISyntaxException; import java.nio.ByteBuffer; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.server.ReactiveServerHttpRequest; -import org.springframework.util.StringUtils; - import io.undertow.server.HttpServerExchange; import io.undertow.util.HeaderValues; import org.reactivestreams.Publisher; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.server.ReactiveServerHttpRequest; + /** * @author Marek Hawrylczak + * @author Rossen Stoyanchev */ class UndertowServerHttpRequest implements ReactiveServerHttpRequest { private final HttpServerExchange exchange; - private final Publisher requestBodyPublisher; + private final Publisher body; private HttpHeaders headers; - public UndertowServerHttpRequest(HttpServerExchange exchange, - Publisher requestBodyPublisher) { + public UndertowServerHttpRequest(HttpServerExchange exchange, Publisher body) { this.exchange = exchange; - this.requestBodyPublisher = requestBodyPublisher; + this.body = body; } - @Override - public Publisher getBody() { - return this.requestBodyPublisher; - } @Override public HttpMethod getMethod() { @@ -60,11 +55,9 @@ class UndertowServerHttpRequest implements ReactiveServerHttpRequest { @Override public URI getURI() { try { - StringBuilder uri = new StringBuilder(this.exchange.getRequestPath()); - if (StringUtils.hasLength(this.exchange.getQueryString())) { - uri.append('?').append(this.exchange.getQueryString()); - } - return new URI(uri.toString()); + return new URI(this.exchange.getRequestScheme(), null, this.exchange.getHostName(), + this.exchange.getHostPort(), this.exchange.getRequestURI(), + this.exchange.getQueryString(), null); } catch (URISyntaxException ex) { throw new IllegalStateException("Could not get URI: " + ex.getMessage(), ex); @@ -83,4 +76,10 @@ class UndertowServerHttpRequest implements ReactiveServerHttpRequest { } return this.headers; } + + @Override + public Publisher getBody() { + return this.body; + } + } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/UndertowServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/UndertowServerHttpResponse.java index b3bc0fefe9..5efe890de1 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/UndertowServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/UndertowServerHttpResponse.java @@ -23,65 +23,50 @@ import java.util.Map; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.server.ReactiveServerHttpResponse; +import org.springframework.util.Assert; import io.undertow.server.HttpServerExchange; import io.undertow.util.HttpString; import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; -import reactor.rx.Streams; /** * @author Marek Hawrylczak + * @author Rossen Stoyanchev */ class UndertowServerHttpResponse implements ReactiveServerHttpResponse { - private final HttpServerExchange exchange; - private final HttpHeaders headers; - private final ResponseBodySubscriber responseBodySubscriber; + private final HttpServerExchange exchange; + + private final ResponseBodySubscriber bodySubscriber; + + private final HttpHeaders headers = new HttpHeaders(); private boolean headersWritten = false; - public UndertowServerHttpResponse(HttpServerExchange exchange, - ResponseBodySubscriber responseBodySubscriber) { + public UndertowServerHttpResponse(HttpServerExchange exchange, ResponseBodySubscriber body) { this.exchange = exchange; - this.responseBodySubscriber = responseBodySubscriber; - this.headers = new HttpHeaders(); + this.bodySubscriber = body; } + @Override public void setStatusCode(HttpStatus status) { + Assert.notNull(status); this.exchange.setStatusCode(status.value()); } - @Override - public Publisher setBody(Publisher contentPublisher) { - applyHeaders(); - return s -> s.onSubscribe(new Subscription() { - @Override - public void request(long n) { - Streams.wrap(contentPublisher) - .finallyDo(byteBufferSignal -> { - if (byteBufferSignal.isOnComplete()) { - s.onComplete(); - } - else { - s.onError(byteBufferSignal.getThrowable()); - } - } - ).subscribe(responseBodySubscriber); - } - @Override - public void cancel() { - } - }); + @Override + public Publisher setBody(Publisher bodyPublisher) { + applyHeaders(); + return (subscriber -> bodyPublisher.subscribe(bodySubscriber)); } @Override public HttpHeaders getHeaders() { - return (this.headersWritten ? - HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers); + return (this.headersWritten ? HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers); } @Override @@ -102,12 +87,12 @@ class UndertowServerHttpResponse implements ReactiveServerHttpResponse { private void applyHeaders() { if (!this.headersWritten) { for (Map.Entry> entry : this.headers.entrySet()) { - String headerName = entry.getKey(); - this.exchange.getResponseHeaders() - .addAll(HttpString.tryFromString(headerName), entry.getValue()); + HttpString headerName = HttpString.tryFromString(entry.getKey()); + this.exchange.getResponseHeaders().addAll(headerName, entry.getValue()); } this.headersWritten = true; } } + } From bdc5b38cb12d26605a968845338bef5bad22816d Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Wed, 4 Nov 2015 17:48:53 +0100 Subject: [PATCH 086/498] Refactor codecs This commit introduces the following changes: - MessageToByteEncoder/Decoder renamed to Encoder/Decoder - JsonObjectEncoder/Decoder are now used directly in JacksonJsonEncoder/Decoder - Codec uses MimeType instead of MediaType since they are not specific to HTTP - Default MimeType are now managed thanks to Encoder/Decoder#getSupportedMimeTypes() - AbstractEncoder/Decoder takes care of generic MimeType related behavior --- .../codec/decoder/AbstractDecoder.java | 52 ++++++++++++++ .../codec/decoder/ByteBufferDecoder.java | 18 +++-- ...ByteToMessageDecoder.java => Decoder.java} | 40 ++++++----- .../codec/decoder/JacksonJsonDecoder.java | 30 ++++---- .../reactive/codec/decoder/Jaxb2Decoder.java | 13 ++-- .../codec/decoder/JsonObjectDecoder.java | 16 ++--- .../reactive/codec/decoder/StringDecoder.java | 22 ++++-- .../codec/encoder/AbstractEncoder.java | 52 ++++++++++++++ .../codec/encoder/ByteBufferEncoder.java | 26 ++++--- .../reactive/codec/encoder/Encoder.java | 61 ++++++++++++++++ .../codec/encoder/JacksonJsonEncoder.java | 30 ++++---- .../reactive/codec/encoder/Jaxb2Encoder.java | 11 ++- .../codec/encoder/JsonObjectEncoder.java | 14 ++-- .../codec/encoder/MessageToByteEncoder.java | 57 --------------- .../reactive/codec/encoder/StringEncoder.java | 23 +++++-- .../reactive/codec/support/HintUtils.java | 8 +-- .../RequestBodyArgumentResolver.java | 51 +++----------- .../RequestMappingHandlerAdapter.java | 7 +- .../annotation/ResponseBodyResultHandler.java | 69 +++++++------------ ...coder.java => ByteBufferEncoderTests.java} | 2 +- .../RequestMappingIntegrationTests.java | 4 +- 21 files changed, 351 insertions(+), 255 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/AbstractDecoder.java rename spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/{ByteToMessageDecoder.java => Decoder.java} (51%) create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/AbstractEncoder.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/Encoder.java delete mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/MessageToByteEncoder.java rename spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/{ByteBufferDecoderEncoder.java => ByteBufferEncoderTests.java} (98%) diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/AbstractDecoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/AbstractDecoder.java new file mode 100644 index 0000000000..af899e079d --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/AbstractDecoder.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2015 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.reactive.codec.decoder; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.springframework.core.ResolvableType; +import org.springframework.util.MimeType; + +/** + * @author Sebastien Deleuze + */ +public abstract class AbstractDecoder implements Decoder { + + private List supportedMimeTypes = Collections.emptyList(); + + public AbstractDecoder(MimeType... supportedMimeTypes) { + this.supportedMimeTypes = Arrays.asList(supportedMimeTypes); + } + + @Override + public boolean canDecode(ResolvableType type, MimeType mimeType, Object... hints) { + for (MimeType supportedMimeType : this.supportedMimeTypes) { + if (supportedMimeType.isCompatibleWith(mimeType)) { + return true; + } + } + return false; + } + + @Override + public List getSupportedMimeTypes() { + return this.supportedMimeTypes; + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/ByteBufferDecoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/ByteBufferDecoder.java index 33bef171d0..a06063ae48 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/ByteBufferDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/ByteBufferDecoder.java @@ -21,22 +21,28 @@ import java.nio.ByteBuffer; import org.reactivestreams.Publisher; import org.springframework.core.ResolvableType; -import org.springframework.http.MediaType; +import org.springframework.util.MimeType; +import org.springframework.util.MimeTypeUtils; /** * @author Sebastien Deleuze */ -public class ByteBufferDecoder implements ByteToMessageDecoder { +public class ByteBufferDecoder extends AbstractDecoder { + + public ByteBufferDecoder() { + super(MimeTypeUtils.ALL); + } @Override - public boolean canDecode(ResolvableType type, MediaType mediaType, Object... hints) { - return ByteBuffer.class.isAssignableFrom(type.getRawClass()); + public boolean canDecode(ResolvableType type, MimeType mimeType, Object... hints) { + return super.canDecode(type, mimeType, hints) + && ByteBuffer.class.isAssignableFrom(type.getRawClass()); } @Override public Publisher decode(Publisher inputStream, ResolvableType type, - MediaType mediaType, Object... hints) { + MimeType mimeType, Object... hints) { return inputStream; } -} +} \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/ByteToMessageDecoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/Decoder.java similarity index 51% rename from spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/ByteToMessageDecoder.java rename to spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/Decoder.java index 6c1fed1995..2a7416501e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/ByteToMessageDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/Decoder.java @@ -17,39 +17,45 @@ package org.springframework.reactive.codec.decoder; import java.nio.ByteBuffer; +import java.util.List; import org.reactivestreams.Publisher; import org.springframework.core.ResolvableType; -import org.springframework.http.MediaType; -import org.springframework.reactive.codec.encoder.MessageToByteEncoder; +import org.springframework.reactive.codec.encoder.Encoder; +import org.springframework.util.MimeType; /** - * Decode from a bytes stream to a message stream. + * Decode from a bytes stream to a stream of {@code T}. * * @author Sebastien Deleuze - * @see MessageToByteEncoder + * @see Encoder */ -public interface ByteToMessageDecoder { +public interface Decoder { /** - * Indicate whether the given type and media type can be processed by this decoder. - * @param type the stream element type to ultimately decode to. - * @param mediaType the media type to decode from. - * Typically the value of a {@code Content-Type} header for HTTP request. + * Indicate whether the given type and mime type can be processed by this decoder. + * @param type the stream element type to process. + * @param mimeType the mime type to process. * @param hints Additional information about how to do decode, optional. - * @return {@code true} if decodable; {@code false} otherwise + * @return {@code true} if can process; {@code false} otherwise */ - boolean canDecode(ResolvableType type, MediaType mediaType, Object... hints); + boolean canDecode(ResolvableType type, MimeType mimeType, Object... hints); /** - * Decode a bytes stream to a message stream. - * @param inputStream the input stream that represent the whole object to decode. - * @param type the stream element type to ultimately decode to. + * Decode an input {@link ByteBuffer} stream to an output stream of {@code T}. + * @param inputStream the input stream to process. + * @param type the stream element type to process. + * @param mimeType the mime type to process. * @param hints Additional information about how to do decode, optional. - * @return the decoded message stream + * @return the output stream */ - Publisher decode(Publisher inputStream, ResolvableType type, - MediaType mediaType, Object... hints); + Publisher decode(Publisher inputStream, ResolvableType type, MimeType mimeType, Object... hints); + + /** + * Return the list of {@link MimeType} objects supported by this codec. + * @return the list of supported mime types + */ + List getSupportedMimeTypes(); } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoder.java index 00a3241461..3775b6520f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoder.java @@ -18,6 +18,7 @@ package org.springframework.reactive.codec.decoder; import java.io.IOException; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectReader; @@ -25,10 +26,10 @@ import org.reactivestreams.Publisher; import reactor.Publishers; import org.springframework.core.ResolvableType; -import org.springframework.http.MediaType; import org.springframework.reactive.codec.CodecException; import org.springframework.reactive.codec.encoder.JacksonJsonEncoder; import org.springframework.reactive.io.ByteBufferInputStream; +import org.springframework.util.MimeType; /** * Decode from a bytes stream of JSON objects to a stream of {@code Object} (POJO). @@ -36,31 +37,36 @@ import org.springframework.reactive.io.ByteBufferInputStream; * @author Sebastien Deleuze * @see JacksonJsonEncoder */ -public class JacksonJsonDecoder implements ByteToMessageDecoder { +public class JacksonJsonDecoder extends AbstractDecoder { private final ObjectMapper mapper; + private Decoder preProcessor; + public JacksonJsonDecoder() { - this(new ObjectMapper()); + this(new ObjectMapper(), null); } - public JacksonJsonDecoder(ObjectMapper mapper) { + public JacksonJsonDecoder(Decoder preProcessor) { + this(new ObjectMapper(), preProcessor); + } + + public JacksonJsonDecoder(ObjectMapper mapper, Decoder preProcessor) { + super(new MimeType("application", "json", StandardCharsets.UTF_8), + new MimeType("application", "*+json", StandardCharsets.UTF_8)); this.mapper = mapper; - } - - - @Override - public boolean canDecode(ResolvableType type, MediaType mediaType, Object... hints) { - return mediaType.isCompatibleWith(MediaType.APPLICATION_JSON); + this.preProcessor = preProcessor; } @Override public Publisher decode(Publisher inputStream, ResolvableType type, - MediaType mediaType, Object... hints) { + MimeType mimeType, Object... hints) { ObjectReader reader = this.mapper.readerFor(type.getRawClass()); - return Publishers.map(inputStream, chunk -> { + Publisher decodedStream = this.preProcessor == null ? inputStream : + this.preProcessor.decode(inputStream, type, mimeType, hints); + return Publishers.map(decodedStream, chunk -> { try { return reader.readValue(new ByteBufferInputStream(chunk)); } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/Jaxb2Decoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/Jaxb2Decoder.java index 204483481d..917400ddb5 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/Jaxb2Decoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/Jaxb2Decoder.java @@ -42,6 +42,7 @@ import org.springframework.reactive.codec.CodecException; import org.springframework.reactive.codec.encoder.Jaxb2Encoder; import org.springframework.reactive.io.ByteBufferPublisherInputStream; import org.springframework.util.Assert; +import org.springframework.util.MimeType; /** * Decode from a bytes stream of XML elements to a stream of {@code Object} (POJO). @@ -49,20 +50,17 @@ import org.springframework.util.Assert; * @author Sebastien Deleuze * @see Jaxb2Encoder */ -public class Jaxb2Decoder implements ByteToMessageDecoder { +public class Jaxb2Decoder extends AbstractDecoder { private final ConcurrentMap, JAXBContext> jaxbContexts = new ConcurrentHashMap<>(64); - - @Override - public boolean canDecode(ResolvableType type, MediaType mediaType, Object... hints) { - return (mediaType.isCompatibleWith(MediaType.APPLICATION_XML) || - mediaType.isCompatibleWith(MediaType.TEXT_XML)); + public Jaxb2Decoder() { + super(MediaType.APPLICATION_XML, MediaType.TEXT_XML); } @Override public Publisher decode(Publisher inputStream, ResolvableType type, - MediaType mediaType, Object... hints) { + MimeType mimeType, Object... hints) { Class outputClass = type.getRawClass(); try { @@ -129,4 +127,5 @@ public class Jaxb2Decoder implements ByteToMessageDecoder { } return jaxbContext; } + } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JsonObjectDecoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JsonObjectDecoder.java index 7a2b0bc4c2..5267b82dac 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JsonObjectDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JsonObjectDecoder.java @@ -17,6 +17,7 @@ package org.springframework.reactive.codec.decoder; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; @@ -28,8 +29,8 @@ import reactor.Publishers; import reactor.fn.Function; import org.springframework.core.ResolvableType; -import org.springframework.http.MediaType; import org.springframework.reactive.codec.encoder.JsonObjectEncoder; +import org.springframework.util.MimeType; /** * Decode an arbitrary split byte stream representing JSON objects to a byte @@ -45,7 +46,7 @@ import org.springframework.reactive.codec.encoder.JsonObjectEncoder; * @author Sebastien Deleuze * @see JsonObjectEncoder */ -public class JsonObjectDecoder implements ByteToMessageDecoder { +public class JsonObjectDecoder extends AbstractDecoder { private static final int ST_CORRUPTED = -1; @@ -85,6 +86,8 @@ public class JsonObjectDecoder implements ByteToMessageDecoder { * "infinitely" many elements. */ public JsonObjectDecoder(int maxObjectLength, boolean streamArrayElements) { + super(new MimeType("application", "json", StandardCharsets.UTF_8), + new MimeType("application", "*+json", StandardCharsets.UTF_8)); if (maxObjectLength < 1) { throw new IllegalArgumentException("maxObjectLength must be a positive int"); } @@ -92,15 +95,9 @@ public class JsonObjectDecoder implements ByteToMessageDecoder { this.streamArrayElements = streamArrayElements; } - - @Override - public boolean canDecode(ResolvableType type, MediaType mediaType, Object... hints) { - return mediaType.isCompatibleWith(MediaType.APPLICATION_JSON); - } - @Override public Publisher decode(Publisher inputStream, ResolvableType type, - MediaType mediaType, Object... hints) { + MimeType mimeType, Object... hints) { return Publishers.flatMap(inputStream, new Function>() { @@ -261,4 +258,5 @@ public class JsonObjectDecoder implements ByteToMessageDecoder { } }); } + } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/StringDecoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/StringDecoder.java index b5f44ef73d..fa86f751c6 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/StringDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/StringDecoder.java @@ -25,9 +25,8 @@ import reactor.Publishers; import reactor.io.buffer.Buffer; import org.springframework.core.ResolvableType; -import org.springframework.http.MediaType; import org.springframework.reactive.codec.encoder.StringEncoder; -import org.springframework.reactive.codec.support.HintUtils; +import org.springframework.util.MimeType; /** * Decode from a bytes stream to a String stream. @@ -35,22 +34,31 @@ import org.springframework.reactive.codec.support.HintUtils; * @author Sebastien Deleuze * @see StringEncoder */ -public class StringDecoder implements ByteToMessageDecoder { +public class StringDecoder extends AbstractDecoder { public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; + public StringDecoder() { + super(new MimeType("text", "plain", DEFAULT_CHARSET)); + } @Override - public boolean canDecode(ResolvableType type, MediaType mediaType, Object... hints) { - return mediaType.isCompatibleWith(MediaType.TEXT_PLAIN) + public boolean canDecode(ResolvableType type, MimeType mimeType, Object... hints) { + return super.canDecode(type, mimeType, hints) && String.class.isAssignableFrom(type.getRawClass()); } @Override public Publisher decode(Publisher inputStream, ResolvableType type, - MediaType mediaType, Object... hints) { + MimeType mimeType, Object... hints) { - Charset charset = HintUtils.getHintByClass(Charset.class, hints, DEFAULT_CHARSET); + Charset charset; + if (mimeType != null && mimeType.getCharSet() != null) { + charset = mimeType.getCharSet(); + } + else { + charset = DEFAULT_CHARSET; + } return Publishers.map(inputStream, chunk -> new String(new Buffer(chunk).asBytes(), charset)); } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/AbstractEncoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/AbstractEncoder.java new file mode 100644 index 0000000000..c88926e822 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/AbstractEncoder.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2015 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.reactive.codec.encoder; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.springframework.core.ResolvableType; +import org.springframework.util.MimeType; + +/** + * @author Sebastien Deleuze + */ +public abstract class AbstractEncoder implements Encoder { + + private List supportedMimeTypes = Collections.emptyList(); + + public AbstractEncoder(MimeType... supportedMimeTypes) { + this.supportedMimeTypes = Arrays.asList(supportedMimeTypes); + } + + @Override + public boolean canEncode(ResolvableType type, MimeType mimeType, Object... hints) { + for (MimeType supportedMimeType : this.supportedMimeTypes) { + if (supportedMimeType.isCompatibleWith(mimeType)) { + return true; + } + } + return false; + } + + @Override + public List getSupportedMimeTypes() { + return this.supportedMimeTypes; + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/ByteBufferEncoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/ByteBufferEncoder.java index 321f43357d..1157779714 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/ByteBufferEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/ByteBufferEncoder.java @@ -21,24 +21,28 @@ import java.nio.ByteBuffer; import org.reactivestreams.Publisher; import org.springframework.core.ResolvableType; -import org.springframework.http.MediaType; +import org.springframework.util.MimeType; +import org.springframework.util.MimeTypeUtils; /** * @author Sebastien Deleuze */ -public class ByteBufferEncoder implements MessageToByteEncoder { +public class ByteBufferEncoder extends AbstractEncoder { - @Override - public boolean canEncode(ResolvableType type, MediaType mediaType, Object... hints) { - return ByteBuffer.class.isAssignableFrom(type.getRawClass()); + public ByteBufferEncoder() { + super(MimeTypeUtils.ALL); } @Override - @SuppressWarnings("unchecked") - public Publisher encode(Publisher messageStream, - ResolvableType type, MediaType mediaType, Object... hints) { - - return (Publisher) messageStream; + public boolean canEncode(ResolvableType type, MimeType mimeType, Object... hints) { + return super.canEncode(type, mimeType, hints) + && ByteBuffer.class.isAssignableFrom(type.getRawClass()); } -} + @Override + public Publisher encode(Publisher inputStream, ResolvableType type, + MimeType mimeType, Object... hints) { + + return (Publisher)inputStream; + } +} \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/Encoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/Encoder.java new file mode 100644 index 0000000000..55e5455422 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/Encoder.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2015 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.reactive.codec.encoder; + +import java.nio.ByteBuffer; +import java.util.List; + +import org.reactivestreams.Publisher; + +import org.springframework.core.ResolvableType; +import org.springframework.reactive.codec.decoder.Decoder; +import org.springframework.util.MimeType; + +/** + * Encode from a stream of {@code T} to a bytes stream. + * + * @author Sebastien Deleuze + * @see Decoder + */ +public interface Encoder { + + /** + * Indicate whether the given type and mime type can be processed by this encoder. + * @param type the stream element type to process. + * @param mimeType the mime type to process. + * @param hints Additional information about how to do decode, optional. + * @return {@code true} if can process; {@code false} otherwise + */ + boolean canEncode(ResolvableType type, MimeType mimeType, Object... hints); + + /** + * Encode an input stream of {@code T} to an output {@link ByteBuffer} stream. + * @param inputStream the input stream to process. + * @param type the stream element type to process. + * @param mimeType the mime type to process. + * @param hints Additional information about how to do decode, optional. + * @return the output stream + */ + Publisher encode(Publisher inputStream, ResolvableType type, MimeType mimeType, Object... hints); + + /** + * Return the list of {@link MimeType} objects supported by this codec. + * @return the list of supported mime types + */ + List getSupportedMimeTypes(); + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoder.java index f5e639b752..a2d8e02cc8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoder.java @@ -18,6 +18,7 @@ package org.springframework.reactive.codec.encoder; import java.io.IOException; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import com.fasterxml.jackson.databind.ObjectMapper; import org.reactivestreams.Publisher; @@ -25,10 +26,10 @@ import reactor.Publishers; import reactor.io.buffer.Buffer; import org.springframework.core.ResolvableType; -import org.springframework.http.MediaType; import org.springframework.reactive.codec.CodecException; import org.springframework.reactive.codec.decoder.JacksonJsonDecoder; import org.springframework.reactive.io.BufferOutputStream; +import org.springframework.util.MimeType; /** * Encode from an {@code Object} stream to a byte stream of JSON objects. @@ -36,28 +37,32 @@ import org.springframework.reactive.io.BufferOutputStream; * @author Sebastien Deleuze * @see JacksonJsonDecoder */ -public class JacksonJsonEncoder implements MessageToByteEncoder { +public class JacksonJsonEncoder extends AbstractEncoder { private final ObjectMapper mapper; + private Encoder postProcessor; + public JacksonJsonEncoder() { - this(new ObjectMapper()); + this(new ObjectMapper(), null); } - public JacksonJsonEncoder(ObjectMapper mapper) { + public JacksonJsonEncoder(Encoder postProcessor) { + this(new ObjectMapper(), postProcessor); + } + + public JacksonJsonEncoder(ObjectMapper mapper, Encoder postProcessor) { + super(new MimeType("application", "json", StandardCharsets.UTF_8), + new MimeType("application", "*+json", StandardCharsets.UTF_8)); this.mapper = mapper; + this.postProcessor = postProcessor; } @Override - public boolean canEncode(ResolvableType type, MediaType mediaType, Object... hints) { - return mediaType.isCompatibleWith(MediaType.APPLICATION_JSON); - } + public Publisher encode(Publisher inputStream, + ResolvableType type, MimeType mimeType, Object... hints) { - @Override - public Publisher encode(Publisher messageStream, - ResolvableType type, MediaType mediaType, Object... hints) { - - return Publishers.map(messageStream, value -> { + Publisher stream = Publishers.map(inputStream, value -> { Buffer buffer = new Buffer(); BufferOutputStream outputStream = new BufferOutputStream(buffer); try { @@ -68,6 +73,7 @@ public class JacksonJsonEncoder implements MessageToByteEncoder { buffer.flip(); return buffer.byteBuffer(); }); + return this.postProcessor == null ? stream : this.postProcessor.encode(stream, type, mimeType, hints); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/Jaxb2Encoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/Jaxb2Encoder.java index ea3063d5f7..cc9a7509ee 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/Jaxb2Encoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/Jaxb2Encoder.java @@ -36,6 +36,7 @@ import org.springframework.reactive.codec.decoder.Jaxb2Decoder; import org.springframework.reactive.io.BufferOutputStream; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.MimeType; /** * Encode from an {@code Object} stream to a byte stream of XML elements. @@ -43,20 +44,18 @@ import org.springframework.util.ClassUtils; * @author Sebastien Deleuze * @see Jaxb2Decoder */ -public class Jaxb2Encoder implements MessageToByteEncoder { +public class Jaxb2Encoder extends AbstractEncoder { private final ConcurrentMap, JAXBContext> jaxbContexts = new ConcurrentHashMap<>(64); - @Override - public boolean canEncode(ResolvableType type, MediaType mediaType, Object... hints) { - return (mediaType.isCompatibleWith(MediaType.APPLICATION_XML) || - mediaType.isCompatibleWith(MediaType.TEXT_XML)); + public Jaxb2Encoder() { + super(MediaType.APPLICATION_XML, MediaType.TEXT_XML); } @Override public Publisher encode(Publisher messageStream, ResolvableType type, - MediaType mediaType, Object... hints) { + MimeType mimeType, Object... hints) { return Publishers.map(messageStream, value -> { try { diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JsonObjectEncoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JsonObjectEncoder.java index 103a8e4b74..9b5b8775ef 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JsonObjectEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JsonObjectEncoder.java @@ -17,6 +17,7 @@ package org.springframework.reactive.codec.encoder; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; import java.util.concurrent.atomic.AtomicLongFieldUpdater; @@ -27,8 +28,8 @@ import reactor.core.support.BackpressureUtils; import reactor.io.buffer.Buffer; import org.springframework.core.ResolvableType; -import org.springframework.http.MediaType; import org.springframework.reactive.codec.decoder.JsonObjectDecoder; +import org.springframework.util.MimeType; import static reactor.Publishers.lift; @@ -41,17 +42,16 @@ import static reactor.Publishers.lift; * * @see JsonObjectDecoder */ -public class JsonObjectEncoder implements MessageToByteEncoder { +public class JsonObjectEncoder extends AbstractEncoder { - - @Override - public boolean canEncode(ResolvableType type, MediaType mediaType, Object... hints) { - return mediaType.isCompatibleWith(MediaType.APPLICATION_JSON); + public JsonObjectEncoder() { + super(new MimeType("application", "json", StandardCharsets.UTF_8), + new MimeType("application", "*+json", StandardCharsets.UTF_8)); } @Override public Publisher encode(Publisher messageStream, - ResolvableType type, MediaType mediaType, Object... hints) { + ResolvableType type, MimeType mimeType, Object... hints) { //noinspection Convert2MethodRef return lift(messageStream, bbs -> new JsonEncoderBarrier(bbs)); diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/MessageToByteEncoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/MessageToByteEncoder.java deleted file mode 100644 index 3075702cda..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/MessageToByteEncoder.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2002-2015 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.reactive.codec.encoder; - -import java.nio.ByteBuffer; - -import org.reactivestreams.Publisher; - -import org.springframework.core.ResolvableType; -import org.springframework.http.MediaType; -import org.springframework.reactive.codec.decoder.ByteToMessageDecoder; - -/** - * Encode from a message stream to a bytes stream. - * - * @author Sebastien Deleuze - * @see ByteToMessageDecoder - */ -public interface MessageToByteEncoder { - - /** - * Indicate whether the given type and media type can be processed by this encoder. - * @param type the stream element type to encode. - * @param mediaType the media type to encode. - * Typically the value of an {@code Accept} header for HTTP request. - * @param hints Additional information about how to encode, optional. - * @return {@code true} if encodable; {@code false} otherwise - */ - boolean canEncode(ResolvableType type, MediaType mediaType, Object... hints); - - /** - * Encode a given message stream to the given output byte stream. - * @param messageStream the message stream to encode. - * @param type the stream element type to encode. - * @param mediaType the media type to encode. - * Typically the value of an {@code Accept} header for HTTP request. - * @param hints Additional information about how to encode, optional. - * @return the encoded bytes stream - */ - Publisher encode(Publisher messageStream, ResolvableType type, - MediaType mediaType, Object... hints); - -} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/StringEncoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/StringEncoder.java index 708745e75f..2f5e99ca5b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/StringEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/StringEncoder.java @@ -24,9 +24,8 @@ import org.reactivestreams.Publisher; import reactor.Publishers; import org.springframework.core.ResolvableType; -import org.springframework.http.MediaType; import org.springframework.reactive.codec.decoder.StringDecoder; -import org.springframework.reactive.codec.support.HintUtils; +import org.springframework.util.MimeType; /** * Encode from a String stream to a bytes stream. @@ -34,22 +33,32 @@ import org.springframework.reactive.codec.support.HintUtils; * @author Sebastien Deleuze * @see StringDecoder */ -public class StringEncoder implements MessageToByteEncoder { +public class StringEncoder extends AbstractEncoder { public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; + public StringEncoder() { + super(new MimeType("text", "plain", DEFAULT_CHARSET)); + } + @Override - public boolean canEncode(ResolvableType type, MediaType mediaType, Object... hints) { - return mediaType.isCompatibleWith(MediaType.TEXT_PLAIN) + public boolean canEncode(ResolvableType type, MimeType mimeType, Object... hints) { + return super.canEncode(type, mimeType, hints) && String.class.isAssignableFrom(type.getRawClass()); } @Override public Publisher encode(Publisher elementStream, - ResolvableType type, MediaType mediaType, Object... hints) { + ResolvableType type, MimeType mimeType, Object... hints) { - final Charset charset = HintUtils.getHintByClass(Charset.class, hints, DEFAULT_CHARSET); + Charset charset; + if (mimeType != null && mimeType.getCharSet() != null) { + charset = mimeType.getCharSet(); + } + else { + charset = DEFAULT_CHARSET; + } return Publishers.map(elementStream, s -> ByteBuffer.wrap(s.getBytes(charset))); } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/support/HintUtils.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/support/HintUtils.java index a15c57b77f..892d64b451 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/support/HintUtils.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/support/HintUtils.java @@ -16,15 +16,15 @@ package org.springframework.reactive.codec.support; -import org.springframework.reactive.codec.decoder.ByteToMessageDecoder; -import org.springframework.reactive.codec.encoder.MessageToByteEncoder; +import org.springframework.reactive.codec.decoder.Decoder; +import org.springframework.reactive.codec.encoder.Encoder; /** * Utility methods for dealing with codec hints. * * @author Sebastien Deleuze - * @see MessageToByteEncoder - * @see ByteToMessageDecoder + * @see Encoder + * @see Decoder */ public abstract class HintUtils { diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java index 60f8aa8055..72cfbe9735 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java @@ -17,10 +17,6 @@ package org.springframework.reactive.web.dispatch.method.annotation; import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; import java.util.List; import org.reactivestreams.Publisher; @@ -32,8 +28,7 @@ import org.springframework.core.convert.ConversionService; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.server.ReactiveServerHttpRequest; -import org.springframework.reactive.codec.decoder.ByteToMessageDecoder; -import org.springframework.reactive.codec.decoder.JsonObjectDecoder; +import org.springframework.reactive.codec.decoder.Decoder; import org.springframework.reactive.web.dispatch.method.HandlerMethodArgumentResolver; import org.springframework.util.Assert; import org.springframework.web.bind.annotation.RequestBody; @@ -44,21 +39,15 @@ import org.springframework.web.bind.annotation.RequestBody; */ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolver { - private static final Charset UTF_8 = Charset.forName("UTF-8"); - - - private final List> decoders; + private final List> deserializers; private final ConversionService conversionService; - // TODO: remove field - private final List> preProcessors = Arrays.asList(new JsonObjectDecoder()); - - public RequestBodyArgumentResolver(List> decoders, ConversionService service) { - Assert.notEmpty(decoders, "At least one decoder is required."); + public RequestBodyArgumentResolver(List> deserializers, ConversionService service) { + Assert.notEmpty(deserializers, "At least one deserializer is required."); Assert.notNull(service, "'conversionService' is required."); - this.decoders = decoders; + this.deserializers = deserializers; this.conversionService = service; } @@ -72,20 +61,12 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve public Publisher resolveArgument(MethodParameter parameter, ReactiveServerHttpRequest request) { MediaType mediaType = resolveMediaType(request); ResolvableType type = ResolvableType.forMethodParameter(parameter); - List hints = new ArrayList<>(); - hints.add(UTF_8); Publisher inputStream = request.getBody(); Publisher elementStream = inputStream; ResolvableType elementType = type.hasGenerics() ? type.getGeneric(0) : type; - ByteToMessageDecoder decoder = resolveDecoder(elementType, mediaType, hints.toArray()); - if (decoder != null) { - List> preProcessors = resolvePreProcessors( - elementType, mediaType,hints.toArray()); - - for (ByteToMessageDecoder preProcessor : preProcessors) { - inputStream = preProcessor.decode(inputStream, elementType, mediaType, hints.toArray()); - } - elementStream = decoder.decode(inputStream, elementType, mediaType, hints.toArray()); + Decoder deserializer = resolveDeserializer(elementType, mediaType); + if (deserializer != null) { + elementStream = deserializer.decode(inputStream, elementType, mediaType); } if (this.conversionService.canConvert(Publisher.class, type.getRawClass())) { return Publishers.just(this.conversionService.convert(elementStream, type.getRawClass())); @@ -100,8 +81,8 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve return ( mediaTypes.size() > 0 ? mediaTypes.get(0) : MediaType.TEXT_PLAIN); } - private ByteToMessageDecoder resolveDecoder(ResolvableType type, MediaType mediaType, Object[] hints) { - for (ByteToMessageDecoder deserializer : this.decoders) { + private Decoder resolveDeserializer(ResolvableType type, MediaType mediaType, Object... hints) { + for (Decoder deserializer : this.deserializers) { if (deserializer.canDecode(type, mediaType, hints)) { return deserializer; } @@ -109,16 +90,4 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve return null; } - private List> resolvePreProcessors(ResolvableType type, - MediaType mediaType, Object[] hints) { - - List> preProcessors = new ArrayList<>(); - for (ByteToMessageDecoder preProcessor : this.preProcessors) { - if (preProcessor.canDecode(type, mediaType, hints)) { - preProcessors.add(preProcessor); - } - } - return preProcessors; - } - } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java index f604fa0b3c..6d3e94e3e6 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java @@ -28,8 +28,9 @@ import org.springframework.core.convert.ConversionService; import org.springframework.http.server.ReactiveServerHttpRequest; import org.springframework.http.server.ReactiveServerHttpResponse; import org.springframework.reactive.codec.decoder.ByteBufferDecoder; -import org.springframework.reactive.codec.decoder.ByteToMessageDecoder; +import org.springframework.reactive.codec.decoder.Decoder; import org.springframework.reactive.codec.decoder.JacksonJsonDecoder; +import org.springframework.reactive.codec.decoder.JsonObjectDecoder; import org.springframework.reactive.codec.decoder.StringDecoder; import org.springframework.reactive.web.dispatch.HandlerAdapter; import org.springframework.reactive.web.dispatch.HandlerResult; @@ -70,8 +71,8 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Initializin public void afterPropertiesSet() throws Exception { if (this.argumentResolvers == null) { - List> decoders = Arrays.asList(new ByteBufferDecoder(), - new StringDecoder(), new JacksonJsonDecoder()); + List> decoders = Arrays.asList(new ByteBufferDecoder(), + new StringDecoder(), new JacksonJsonDecoder(new JsonObjectDecoder())); this.argumentResolvers = new ArrayList<>(); this.argumentResolvers.add(new RequestParamArgumentResolver()); diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java index ec66870464..d1818f3ebe 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java @@ -18,9 +18,6 @@ package org.springframework.reactive.web.dispatch.method.annotation; import java.lang.reflect.Method; import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import org.reactivestreams.Publisher; @@ -35,42 +32,33 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.server.ReactiveServerHttpRequest; import org.springframework.http.server.ReactiveServerHttpResponse; -import org.springframework.reactive.codec.encoder.JsonObjectEncoder; -import org.springframework.reactive.codec.encoder.MessageToByteEncoder; +import org.springframework.reactive.codec.encoder.Encoder; import org.springframework.reactive.web.dispatch.HandlerResult; import org.springframework.reactive.web.dispatch.HandlerResultHandler; import org.springframework.util.Assert; +import org.springframework.util.MimeType; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.method.HandlerMethod; /** - * First version using {@link MessageToByteEncoder}s - * * @author Rossen Stoyanchev * @author Stephane Maldini * @author Sebastien Deleuze */ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered { - private static final Charset UTF_8 = Charset.forName("UTF-8"); - - - private final List> serializers; + private final List> serializers; private final ConversionService conversionService; private int order = 0; - // TODO: remove field - private final List> postProcessors = Arrays.asList(new JsonObjectEncoder()); - - - public ResponseBodyResultHandler(List> encoders, ConversionService service) { - Assert.notEmpty(encoders, "At least one encoder is required."); + public ResponseBodyResultHandler(List> serializers, ConversionService service) { + Assert.notEmpty(serializers, "At least one serializers is required."); Assert.notNull(service, "'conversionService' is required."); - this.serializers = encoders; + this.serializers = serializers; this.conversionService = service; } @@ -110,9 +98,7 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered ResolvableType type = ResolvableType.forMethodParameter(returnType); MediaType mediaType = resolveMediaType(request); - List hints = new ArrayList<>(); - hints.add(UTF_8); - Publisher elementStream; + Publisher elementStream; ResolvableType elementType; if (conversionService.canConvert(type.getRawClass(), Publisher.class)) { elementStream = conversionService.convert(value, Publisher.class); @@ -123,17 +109,20 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered elementType = type; } - MessageToByteEncoder encoder = (MessageToByteEncoder) resolveEncoder( - elementType, mediaType, hints.toArray()); + Encoder serializer = resolveSerializer(elementType, mediaType); - if (encoder != null) { - Publisher outputStream = encoder.encode(elementStream, type, mediaType, hints.toArray()); - List> postProcessors = resolvePostProcessors( - elementType, mediaType, hints.toArray()); - for (MessageToByteEncoder postProcessor : postProcessors) { - outputStream = postProcessor.encode(outputStream, elementType, mediaType, hints.toArray()); + if (serializer != null) { + Publisher outputStream = serializer.encode((Publisher)elementStream, type, mediaType); + if (mediaType == null || mediaType.isWildcardType() || mediaType.isWildcardSubtype()) { + List mimeTypes = serializer.getSupportedMimeTypes(); + if (!mimeTypes.isEmpty()) { + MimeType mimeType = mimeTypes.get(0); + mediaType = new MediaType(mimeType.getType(), mimeType.getSubtype(), mimeType.getParameters()); + } + } + if (mediaType != null && !mediaType.equals(MediaType.ALL)) { + response.getHeaders().setContentType(mediaType); } - response.getHeaders().setContentType(mediaType); return response.setBody(outputStream); } String returnTypeName = returnType.getParameterType().getName(); @@ -148,25 +137,13 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered return ( mediaTypes.size() > 0 ? mediaTypes.get(0) : MediaType.TEXT_PLAIN); } - private MessageToByteEncoder resolveEncoder(ResolvableType type, MediaType mediaType, Object[] hints) { - for (MessageToByteEncoder codec : this.serializers) { - if (codec.canEncode(type, mediaType, hints)) { - return codec; + private Encoder resolveSerializer(ResolvableType type, MediaType mediaType, Object... hints) { + for (Encoder serializer : this.serializers) { + if (serializer.canEncode(type, mediaType, hints)) { + return serializer; } } return null; } - private List> resolvePostProcessors(ResolvableType type, - MediaType mediaType, Object[] hints) { - - List> postProcessors = new ArrayList<>(); - for (MessageToByteEncoder postProcessor : this.postProcessors) { - if (postProcessor.canEncode(type, mediaType, hints)) { - postProcessors.add(postProcessor); - } - } - return postProcessors; - } - } diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/ByteBufferDecoderEncoder.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/ByteBufferEncoderTests.java similarity index 98% rename from spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/ByteBufferDecoderEncoder.java rename to spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/ByteBufferEncoderTests.java index 3fac6e58a9..4c5a8657e0 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/ByteBufferDecoderEncoder.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/ByteBufferEncoderTests.java @@ -32,7 +32,7 @@ import org.springframework.http.MediaType; /** * @author Sebastien Deleuze */ -public class ByteBufferDecoderEncoder { +public class ByteBufferEncoderTests { private final ByteBufferEncoder encoder = new ByteBufferEncoder(); diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingIntegrationTests.java index f8f14cd64e..180aad81cd 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingIntegrationTests.java @@ -39,7 +39,6 @@ import org.springframework.context.annotation.Configuration; import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ResolvableType; import org.springframework.core.convert.ConversionService; -import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter; import org.springframework.core.convert.support.ReactiveStreamsToReactorConverter; @@ -50,6 +49,7 @@ import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; import org.springframework.reactive.codec.encoder.ByteBufferEncoder; import org.springframework.reactive.codec.encoder.JacksonJsonEncoder; +import org.springframework.reactive.codec.encoder.JsonObjectEncoder; import org.springframework.reactive.codec.encoder.StringEncoder; import org.springframework.reactive.web.dispatch.DispatcherHandler; import org.springframework.reactive.web.dispatch.SimpleHandlerResultHandler; @@ -290,7 +290,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @Bean public ResponseBodyResultHandler responseBodyResultHandler() { return new ResponseBodyResultHandler(Arrays.asList( - new ByteBufferEncoder(), new StringEncoder(),new JacksonJsonEncoder()), + new ByteBufferEncoder(), new StringEncoder(), new JacksonJsonEncoder(new JsonObjectEncoder())), conversionService()); } From 2de127ad4ad8dcb81521039749308de58ac81420 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 13 Nov 2015 15:35:19 -0500 Subject: [PATCH 087/498] Polish --- .../codec/decoder/AbstractDecoder.java | 12 ++-- .../codec/decoder/ByteBufferDecoder.java | 7 +- .../reactive/codec/decoder/Decoder.java | 10 +-- .../codec/decoder/JacksonJsonDecoder.java | 13 ++-- .../reactive/codec/decoder/Jaxb2Decoder.java | 2 + .../reactive/codec/decoder/StringDecoder.java | 2 +- .../codec/encoder/AbstractEncoder.java | 12 ++-- .../codec/encoder/ByteBufferEncoder.java | 10 ++- .../reactive/codec/encoder/Encoder.java | 8 +-- .../codec/encoder/JacksonJsonEncoder.java | 10 ++- .../reactive/codec/encoder/Jaxb2Encoder.java | 1 + .../reactive/codec/encoder/StringEncoder.java | 5 +- .../reactive/codec/support/HintUtils.java | 53 -------------- .../RequestBodyArgumentResolver.java | 29 ++++---- .../annotation/ResponseBodyResultHandler.java | 71 ++++++++++--------- 15 files changed, 111 insertions(+), 134 deletions(-) delete mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/codec/support/HintUtils.java diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/AbstractDecoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/AbstractDecoder.java index af899e079d..aa17856078 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/AbstractDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/AbstractDecoder.java @@ -30,10 +30,17 @@ public abstract class AbstractDecoder implements Decoder { private List supportedMimeTypes = Collections.emptyList(); + public AbstractDecoder(MimeType... supportedMimeTypes) { this.supportedMimeTypes = Arrays.asList(supportedMimeTypes); } + + @Override + public List getSupportedMimeTypes() { + return this.supportedMimeTypes; + } + @Override public boolean canDecode(ResolvableType type, MimeType mimeType, Object... hints) { for (MimeType supportedMimeType : this.supportedMimeTypes) { @@ -44,9 +51,4 @@ public abstract class AbstractDecoder implements Decoder { return false; } - @Override - public List getSupportedMimeTypes() { - return this.supportedMimeTypes; - } - } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/ByteBufferDecoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/ByteBufferDecoder.java index a06063ae48..fd1ddaad9f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/ByteBufferDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/ByteBufferDecoder.java @@ -29,14 +29,16 @@ import org.springframework.util.MimeTypeUtils; */ public class ByteBufferDecoder extends AbstractDecoder { + public ByteBufferDecoder() { super(MimeTypeUtils.ALL); } + @Override public boolean canDecode(ResolvableType type, MimeType mimeType, Object... hints) { - return super.canDecode(type, mimeType, hints) - && ByteBuffer.class.isAssignableFrom(type.getRawClass()); + Class clazz = type.getRawClass(); + return (super.canDecode(type, mimeType, hints) && ByteBuffer.class.isAssignableFrom(clazz)); } @Override @@ -45,4 +47,5 @@ public class ByteBufferDecoder extends AbstractDecoder { return inputStream; } + } \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/Decoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/Decoder.java index 2a7416501e..dbb43b5227 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/Decoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/Decoder.java @@ -26,7 +26,7 @@ import org.springframework.reactive.codec.encoder.Encoder; import org.springframework.util.MimeType; /** - * Decode from a bytes stream to a stream of {@code T}. + * Decode a stream of bytes to a stream of type {@code T}. * * @author Sebastien Deleuze * @see Encoder @@ -34,7 +34,7 @@ import org.springframework.util.MimeType; public interface Decoder { /** - * Indicate whether the given type and mime type can be processed by this decoder. + * Whether the decoder supports the given Java and mime type. * @param type the stream element type to process. * @param mimeType the mime type to process. * @param hints Additional information about how to do decode, optional. @@ -50,11 +50,11 @@ public interface Decoder { * @param hints Additional information about how to do decode, optional. * @return the output stream */ - Publisher decode(Publisher inputStream, ResolvableType type, MimeType mimeType, Object... hints); + Publisher decode(Publisher inputStream, ResolvableType type, + MimeType mimeType, Object... hints); /** - * Return the list of {@link MimeType} objects supported by this codec. - * @return the list of supported mime types + * Return the list of mime types this decoder supports. */ List getSupportedMimeTypes(); diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoder.java index 3775b6520f..39a4133e8c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoder.java @@ -31,6 +31,7 @@ import org.springframework.reactive.codec.encoder.JacksonJsonEncoder; import org.springframework.reactive.io.ByteBufferInputStream; import org.springframework.util.MimeType; + /** * Decode from a bytes stream of JSON objects to a stream of {@code Object} (POJO). * @@ -59,16 +60,20 @@ public class JacksonJsonDecoder extends AbstractDecoder { this.preProcessor = preProcessor; } + @Override public Publisher decode(Publisher inputStream, ResolvableType type, MimeType mimeType, Object... hints) { ObjectReader reader = this.mapper.readerFor(type.getRawClass()); - Publisher decodedStream = this.preProcessor == null ? inputStream : - this.preProcessor.decode(inputStream, type, mimeType, hints); - return Publishers.map(decodedStream, chunk -> { + + if (this.preProcessor != null) { + inputStream = this.preProcessor.decode(inputStream, type, mimeType, hints); + } + + return Publishers.map(inputStream, content -> { try { - return reader.readValue(new ByteBufferInputStream(chunk)); + return reader.readValue(new ByteBufferInputStream(content)); } catch (IOException e) { throw new CodecException("Error while reading the data", e); diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/Jaxb2Decoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/Jaxb2Decoder.java index 917400ddb5..63f0cbc323 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/Jaxb2Decoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/Jaxb2Decoder.java @@ -54,10 +54,12 @@ public class Jaxb2Decoder extends AbstractDecoder { private final ConcurrentMap, JAXBContext> jaxbContexts = new ConcurrentHashMap<>(64); + public Jaxb2Decoder() { super(MediaType.APPLICATION_XML, MediaType.TEXT_XML); } + @Override public Publisher decode(Publisher inputStream, ResolvableType type, MimeType mimeType, Object... hints) { diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/StringDecoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/StringDecoder.java index fa86f751c6..de47a06fda 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/StringDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/StringDecoder.java @@ -59,7 +59,7 @@ public class StringDecoder extends AbstractDecoder { else { charset = DEFAULT_CHARSET; } - return Publishers.map(inputStream, chunk -> new String(new Buffer(chunk).asBytes(), charset)); + return Publishers.map(inputStream, content -> new String(new Buffer(content).asBytes(), charset)); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/AbstractEncoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/AbstractEncoder.java index c88926e822..08f504423b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/AbstractEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/AbstractEncoder.java @@ -30,10 +30,17 @@ public abstract class AbstractEncoder implements Encoder { private List supportedMimeTypes = Collections.emptyList(); + public AbstractEncoder(MimeType... supportedMimeTypes) { this.supportedMimeTypes = Arrays.asList(supportedMimeTypes); } + + @Override + public List getSupportedMimeTypes() { + return this.supportedMimeTypes; + } + @Override public boolean canEncode(ResolvableType type, MimeType mimeType, Object... hints) { for (MimeType supportedMimeType : this.supportedMimeTypes) { @@ -44,9 +51,4 @@ public abstract class AbstractEncoder implements Encoder { return false; } - @Override - public List getSupportedMimeTypes() { - return this.supportedMimeTypes; - } - } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/ByteBufferEncoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/ByteBufferEncoder.java index 1157779714..11ca5ed4ab 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/ByteBufferEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/ByteBufferEncoder.java @@ -29,20 +29,24 @@ import org.springframework.util.MimeTypeUtils; */ public class ByteBufferEncoder extends AbstractEncoder { + public ByteBufferEncoder() { super(MimeTypeUtils.ALL); } + @Override public boolean canEncode(ResolvableType type, MimeType mimeType, Object... hints) { - return super.canEncode(type, mimeType, hints) - && ByteBuffer.class.isAssignableFrom(type.getRawClass()); + Class clazz = type.getRawClass(); + return (super.canEncode(type, mimeType, hints) && ByteBuffer.class.isAssignableFrom(clazz)); } @Override public Publisher encode(Publisher inputStream, ResolvableType type, MimeType mimeType, Object... hints) { - return (Publisher)inputStream; + //noinspection unchecked + return (Publisher) inputStream; } + } \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/Encoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/Encoder.java index 55e5455422..22240f93e5 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/Encoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/Encoder.java @@ -26,7 +26,7 @@ import org.springframework.reactive.codec.decoder.Decoder; import org.springframework.util.MimeType; /** - * Encode from a stream of {@code T} to a bytes stream. + * Encode a stream of Objects of type {@code T} into a stream of bytes. * * @author Sebastien Deleuze * @see Decoder @@ -50,11 +50,11 @@ public interface Encoder { * @param hints Additional information about how to do decode, optional. * @return the output stream */ - Publisher encode(Publisher inputStream, ResolvableType type, MimeType mimeType, Object... hints); + Publisher encode(Publisher inputStream, ResolvableType type, + MimeType mimeType, Object... hints); /** - * Return the list of {@link MimeType} objects supported by this codec. - * @return the list of supported mime types + * Return the list of mime types this encoder supports. */ List getSupportedMimeTypes(); diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoder.java index a2d8e02cc8..d12df127b7 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoder.java @@ -43,6 +43,7 @@ public class JacksonJsonEncoder extends AbstractEncoder { private Encoder postProcessor; + public JacksonJsonEncoder() { this(new ObjectMapper(), null); } @@ -51,6 +52,7 @@ public class JacksonJsonEncoder extends AbstractEncoder { this(new ObjectMapper(), postProcessor); } + public JacksonJsonEncoder(ObjectMapper mapper, Encoder postProcessor) { super(new MimeType("application", "json", StandardCharsets.UTF_8), new MimeType("application", "*+json", StandardCharsets.UTF_8)); @@ -67,13 +69,17 @@ public class JacksonJsonEncoder extends AbstractEncoder { BufferOutputStream outputStream = new BufferOutputStream(buffer); try { this.mapper.writeValue(outputStream, value); - } catch (IOException e) { + } + catch (IOException e) { throw new CodecException("Error while writing the data", e); } buffer.flip(); return buffer.byteBuffer(); }); - return this.postProcessor == null ? stream : this.postProcessor.encode(stream, type, mimeType, hints); + if (this.postProcessor != null) { + stream = this.postProcessor.encode(stream, type, mimeType, hints); + }; + return stream; } } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/Jaxb2Encoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/Jaxb2Encoder.java index cc9a7509ee..1e8c6ff375 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/Jaxb2Encoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/Jaxb2Encoder.java @@ -53,6 +53,7 @@ public class Jaxb2Encoder extends AbstractEncoder { super(MediaType.APPLICATION_XML, MediaType.TEXT_XML); } + @Override public Publisher encode(Publisher messageStream, ResolvableType type, MimeType mimeType, Object... hints) { diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/StringEncoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/StringEncoder.java index 2f5e99ca5b..b87387773f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/StringEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/StringEncoder.java @@ -42,10 +42,11 @@ public class StringEncoder extends AbstractEncoder { super(new MimeType("text", "plain", DEFAULT_CHARSET)); } + @Override public boolean canEncode(ResolvableType type, MimeType mimeType, Object... hints) { - return super.canEncode(type, mimeType, hints) - && String.class.isAssignableFrom(type.getRawClass()); + Class clazz = type.getRawClass(); + return (super.canEncode(type, mimeType, hints) && String.class.isAssignableFrom(clazz)); } @Override diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/support/HintUtils.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/support/HintUtils.java deleted file mode 100644 index 892d64b451..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/support/HintUtils.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2002-2015 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.reactive.codec.support; - -import org.springframework.reactive.codec.decoder.Decoder; -import org.springframework.reactive.codec.encoder.Encoder; - -/** - * Utility methods for dealing with codec hints. - * - * @author Sebastien Deleuze - * @see Encoder - * @see Decoder - */ -public abstract class HintUtils { - - public static T getHintByClass(Class clazz, Object[] hints) { - return getHintByClass(clazz, hints, null); - } - - public static T getHintByClass(Class clazz, Object[] hints, T defaultValue) { - for (Object hint : hints) { - if (hint.getClass().isAssignableFrom(clazz)) { - return (T) hint; - } - } - return defaultValue; - } - - public static boolean containsHint(Object hint, Object[] hints) { - for (Object h : hints) { - if (h.equals(hint)) { - return true; - } - } - return false; - } - -} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java index 72cfbe9735..25e92c6899 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java @@ -39,15 +39,15 @@ import org.springframework.web.bind.annotation.RequestBody; */ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolver { - private final List> deserializers; + private final List> decoders; private final ConversionService conversionService; - public RequestBodyArgumentResolver(List> deserializers, ConversionService service) { - Assert.notEmpty(deserializers, "At least one deserializer is required."); + public RequestBodyArgumentResolver(List> decoders, ConversionService service) { + Assert.notEmpty(decoders, "At least one decoder is required."); Assert.notNull(service, "'conversionService' is required."); - this.deserializers = deserializers; + this.decoders = decoders; this.conversionService = service; } @@ -61,16 +61,19 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve public Publisher resolveArgument(MethodParameter parameter, ReactiveServerHttpRequest request) { MediaType mediaType = resolveMediaType(request); ResolvableType type = ResolvableType.forMethodParameter(parameter); - Publisher inputStream = request.getBody(); - Publisher elementStream = inputStream; + Publisher body = request.getBody(); + Publisher elementStream = body; ResolvableType elementType = type.hasGenerics() ? type.getGeneric(0) : type; - Decoder deserializer = resolveDeserializer(elementType, mediaType); - if (deserializer != null) { - elementStream = deserializer.decode(inputStream, elementType, mediaType); + + Decoder decoder = resolveDecoder(elementType, mediaType); + if (decoder != null) { + elementStream = decoder.decode(body, elementType, mediaType); } + if (this.conversionService.canConvert(Publisher.class, type.getRawClass())) { return Publishers.just(this.conversionService.convert(elementStream, type.getRawClass())); } + return Publishers.map(elementStream, element -> element); } @@ -81,10 +84,10 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve return ( mediaTypes.size() > 0 ? mediaTypes.get(0) : MediaType.TEXT_PLAIN); } - private Decoder resolveDeserializer(ResolvableType type, MediaType mediaType, Object... hints) { - for (Decoder deserializer : this.deserializers) { - if (deserializer.canDecode(type, mediaType, hints)) { - return deserializer; + private Decoder resolveDecoder(ResolvableType type, MediaType mediaType, Object... hints) { + for (Decoder decoder : this.decoders) { + if (decoder.canDecode(type, mediaType, hints)) { + return decoder; } } return null; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java index d1818f3ebe..dd4fd9aa49 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java @@ -48,17 +48,17 @@ import org.springframework.web.method.HandlerMethod; */ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered { - private final List> serializers; + private final List> encoders; private final ConversionService conversionService; private int order = 0; - public ResponseBodyResultHandler(List> serializers, ConversionService service) { - Assert.notEmpty(serializers, "At least one serializers is required."); + public ResponseBodyResultHandler(List> encoders, ConversionService service) { + Assert.notEmpty(encoders, "At least one encoders is required."); Assert.notNull(service, "'conversionService' is required."); - this.serializers = serializers; + this.encoders = encoders; this.conversionService = service; } @@ -89,45 +89,46 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered ReactiveServerHttpResponse response, HandlerResult result) { Object value = result.getValue(); - HandlerMethod handlerMethod = (HandlerMethod) result.getHandler(); - MethodParameter returnType = handlerMethod.getReturnValueType(value); - if (value == null) { return Publishers.empty(); } - ResolvableType type = ResolvableType.forMethodParameter(returnType); - MediaType mediaType = resolveMediaType(request); + HandlerMethod hm = (HandlerMethod) result.getHandler(); + ResolvableType returnType = ResolvableType.forMethodParameter(hm.getReturnValueType(value)); + Publisher elementStream; ResolvableType elementType; - if (conversionService.canConvert(type.getRawClass(), Publisher.class)) { - elementStream = conversionService.convert(value, Publisher.class); - elementType = type.getGeneric(0); + if (this.conversionService.canConvert(returnType.getRawClass(), Publisher.class)) { + elementStream = this.conversionService.convert(value, Publisher.class); + elementType = returnType.getGeneric(0); } else { elementStream = Publishers.just(value); - elementType = type; + elementType = returnType; } - Encoder serializer = resolveSerializer(elementType, mediaType); - - if (serializer != null) { - Publisher outputStream = serializer.encode((Publisher)elementStream, type, mediaType); - if (mediaType == null || mediaType.isWildcardType() || mediaType.isWildcardSubtype()) { - List mimeTypes = serializer.getSupportedMimeTypes(); - if (!mimeTypes.isEmpty()) { - MimeType mimeType = mimeTypes.get(0); - mediaType = new MediaType(mimeType.getType(), mimeType.getSubtype(), mimeType.getParameters()); - } - } - if (mediaType != null && !mediaType.equals(MediaType.ALL)) { - response.getHeaders().setContentType(mediaType); - } - return response.setBody(outputStream); + MediaType mediaType = resolveMediaType(request); + Encoder encoder = resolveEncoder(elementType, mediaType); + if (encoder == null) { + return Publishers.error(new IllegalStateException( + "Return value type '" + returnType + + "' with media type '" + mediaType + "' not supported")); } - String returnTypeName = returnType.getParameterType().getName(); - return Publishers.error(new IllegalStateException("Return value type '" + returnTypeName + - "' with media type '" + mediaType + "' not supported")); + + Publisher outputStream = encoder.encode((Publisher) elementStream, returnType, mediaType); + if (mediaType == null || mediaType.isWildcardType() || mediaType.isWildcardSubtype()) { + List mimeTypes = encoder.getSupportedMimeTypes(); + if (!mimeTypes.isEmpty()) { + MimeType mimeType = mimeTypes.get(0); + mediaType = new MediaType(mimeType.getType(), mimeType.getSubtype(), mimeType.getParameters()); + } + } + + if (mediaType != null && !mediaType.equals(MediaType.ALL)) { + response.getHeaders().setContentType(mediaType); + } + + return response.setBody(outputStream); } private MediaType resolveMediaType(ReactiveServerHttpRequest request) { @@ -137,10 +138,10 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered return ( mediaTypes.size() > 0 ? mediaTypes.get(0) : MediaType.TEXT_PLAIN); } - private Encoder resolveSerializer(ResolvableType type, MediaType mediaType, Object... hints) { - for (Encoder serializer : this.serializers) { - if (serializer.canEncode(type, mediaType, hints)) { - return serializer; + private Encoder resolveEncoder(ResolvableType type, MediaType mediaType, Object... hints) { + for (Encoder encoder : this.encoders) { + if (encoder.canEncode(type, mediaType, hints)) { + return encoder; } } return null; From 5d4201d50079fc36fee21617ece1bfbbcf194120 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 13 Nov 2015 15:47:29 -0500 Subject: [PATCH 088/498] Fix "Content-Type" and "Accept" header processing "Content-Type" is just a single MediaType. For the response, the MediaType must be fully selected before selecting and encoder. The ResponseBodyResultHandler now includes actual content negotiation with a potential 406 response. --- .../RequestBodyArgumentResolver.java | 13 +- .../annotation/ResponseBodyResultHandler.java | 130 +++++++++++++----- 2 files changed, 102 insertions(+), 41 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java index 25e92c6899..fab1615b35 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java @@ -25,7 +25,6 @@ import reactor.Publishers; import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.core.convert.ConversionService; -import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.server.ReactiveServerHttpRequest; import org.springframework.reactive.codec.decoder.Decoder; @@ -59,7 +58,10 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve @Override public Publisher resolveArgument(MethodParameter parameter, ReactiveServerHttpRequest request) { - MediaType mediaType = resolveMediaType(request); + MediaType mediaType = request.getHeaders().getContentType(); + if (mediaType == null) { + mediaType = MediaType.APPLICATION_OCTET_STREAM; + } ResolvableType type = ResolvableType.forMethodParameter(parameter); Publisher body = request.getBody(); Publisher elementStream = body; @@ -77,13 +79,6 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve return Publishers.map(elementStream, element -> element); } - private MediaType resolveMediaType(ReactiveServerHttpRequest request) { - String acceptHeader = request.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE); - List mediaTypes = MediaType.parseMediaTypes(acceptHeader); - MediaType.sortBySpecificityAndQuality(mediaTypes); - return ( mediaTypes.size() > 0 ? mediaTypes.get(0) : MediaType.TEXT_PLAIN); - } - private Decoder resolveDecoder(ResolvableType type, MediaType mediaType, Object... hints) { for (Decoder decoder : this.decoders) { if (decoder.canDecode(type, mediaType, hints)) { diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java index dd4fd9aa49..9567f7de48 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java @@ -17,18 +17,20 @@ package org.springframework.reactive.web.dispatch.method.annotation; import java.lang.reflect.Method; -import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; import org.reactivestreams.Publisher; import reactor.Publishers; -import org.springframework.core.MethodParameter; import org.springframework.core.Ordered; import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.convert.ConversionService; -import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.server.ReactiveServerHttpRequest; import org.springframework.http.server.ReactiveServerHttpResponse; @@ -37,6 +39,7 @@ import org.springframework.reactive.web.dispatch.HandlerResult; import org.springframework.reactive.web.dispatch.HandlerResultHandler; import org.springframework.util.Assert; import org.springframework.util.MimeType; +import org.springframework.web.HttpMediaTypeNotAcceptableException; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.method.HandlerMethod; @@ -48,6 +51,10 @@ import org.springframework.web.method.HandlerMethod; */ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered { + private static final MediaType MEDIA_TYPE_APPLICATION = new MediaType("application"); + + private final List allSupportedMediaTypes; + private final List> encoders; private final ConversionService conversionService; @@ -59,9 +66,23 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered Assert.notEmpty(encoders, "At least one encoders is required."); Assert.notNull(service, "'conversionService' is required."); this.encoders = encoders; + this.allSupportedMediaTypes = getAllSupportedMediaTypes(encoders); this.conversionService = service; } + private static List getAllSupportedMediaTypes(List> encoders) { + Set allSupportedMediaTypes = new LinkedHashSet<>(); + for (Encoder encoder : encoders) { + for (MimeType mimeType : encoder.getSupportedMimeTypes()) { + allSupportedMediaTypes.add( + new MediaType(mimeType.getType(), mimeType.getSubtype(), mimeType.getParameters())); + } + } + List result = new ArrayList<>(allSupportedMediaTypes); + MediaType.sortBySpecificity(result); + return Collections.unmodifiableList(result); + } + public void setOrder(int order) { this.order = order; @@ -96,46 +117,91 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered HandlerMethod hm = (HandlerMethod) result.getHandler(); ResolvableType returnType = ResolvableType.forMethodParameter(hm.getReturnValueType(value)); - Publisher elementStream; - ResolvableType elementType; - if (this.conversionService.canConvert(returnType.getRawClass(), Publisher.class)) { - elementStream = this.conversionService.convert(value, Publisher.class); - elementType = returnType.getGeneric(0); - } - else { - elementStream = Publishers.just(value); - elementType = returnType; + List requestedMediaTypes = getAcceptableMediaTypes(request); + List producibleMediaTypes = getProducibleMediaTypes(returnType); + + if (producibleMediaTypes.isEmpty()) { + Publishers.error(new IllegalArgumentException( + "No encoder found for return value of type: " + returnType)); } - MediaType mediaType = resolveMediaType(request); - Encoder encoder = resolveEncoder(elementType, mediaType); - if (encoder == null) { - return Publishers.error(new IllegalStateException( - "Return value type '" + returnType + - "' with media type '" + mediaType + "' not supported")); + Set compatibleMediaTypes = new LinkedHashSet<>(); + for (MediaType requestedType : requestedMediaTypes) { + for (MediaType producibleType : producibleMediaTypes) { + if (requestedType.isCompatibleWith(producibleType)) { + compatibleMediaTypes.add(getMostSpecificMediaType(requestedType, producibleType)); + } + } + } + if (compatibleMediaTypes.isEmpty()) { + return Publishers.error(new HttpMediaTypeNotAcceptableException(producibleMediaTypes)); } - Publisher outputStream = encoder.encode((Publisher) elementStream, returnType, mediaType); - if (mediaType == null || mediaType.isWildcardType() || mediaType.isWildcardSubtype()) { - List mimeTypes = encoder.getSupportedMimeTypes(); - if (!mimeTypes.isEmpty()) { - MimeType mimeType = mimeTypes.get(0); - mediaType = new MediaType(mimeType.getType(), mimeType.getSubtype(), mimeType.getParameters()); + List mediaTypes = new ArrayList<>(compatibleMediaTypes); + MediaType.sortBySpecificityAndQuality(mediaTypes); + + MediaType selectedMediaType = null; + for (MediaType mediaType : mediaTypes) { + if (mediaType.isConcrete()) { + selectedMediaType = mediaType; + break; + } + else if (mediaType.equals(MediaType.ALL) || mediaType.equals(MEDIA_TYPE_APPLICATION)) { + selectedMediaType = MediaType.APPLICATION_OCTET_STREAM; + break; } } - if (mediaType != null && !mediaType.equals(MediaType.ALL)) { - response.getHeaders().setContentType(mediaType); + if (selectedMediaType != null) { + Publisher publisher; + ResolvableType elementType; + if (this.conversionService.canConvert(returnType.getRawClass(), Publisher.class)) { + publisher = this.conversionService.convert(value, Publisher.class); + elementType = returnType.getGeneric(0); + } + else { + publisher = Publishers.just(value); + elementType = returnType; + } + Encoder encoder = resolveEncoder(elementType, selectedMediaType); + if (encoder != null) { + response.getHeaders().setContentType(selectedMediaType); + return response.setBody(encoder.encode((Publisher) publisher, elementType, selectedMediaType)); + } } - return response.setBody(outputStream); + return Publishers.error(new HttpMediaTypeNotAcceptableException(this.allSupportedMediaTypes)); } - private MediaType resolveMediaType(ReactiveServerHttpRequest request) { - String acceptHeader = request.getHeaders().getFirst(HttpHeaders.ACCEPT); - List mediaTypes = MediaType.parseMediaTypes(acceptHeader); - MediaType.sortBySpecificityAndQuality(mediaTypes); - return ( mediaTypes.size() > 0 ? mediaTypes.get(0) : MediaType.TEXT_PLAIN); + private List getAcceptableMediaTypes(ReactiveServerHttpRequest request) { + List mediaTypes = request.getHeaders().getAccept(); + return (mediaTypes.isEmpty() ? Collections.singletonList(MediaType.ALL) : mediaTypes); + } + + private List getProducibleMediaTypes(ResolvableType type) { + List result = new ArrayList<>(); + for (Encoder encoder : this.encoders) { + if (encoder.canEncode(type, null)) { + for (MimeType mimeType : encoder.getSupportedMimeTypes()) { + result.add(new MediaType(mimeType.getType(), mimeType.getSubtype(), + mimeType.getParameters())); + } + } + } + if (result.isEmpty()) { + result.add(MediaType.ALL); + } + return result; + } + + /** + * Return the more specific of the acceptable and the producible media types + * with the q-value of the former. + */ + private MediaType getMostSpecificMediaType(MediaType acceptType, MediaType produceType) { + produceType = produceType.copyQualityValue(acceptType); + Comparator comparator = MediaType.SPECIFICITY_COMPARATOR; + return (comparator.compare(acceptType, produceType) <= 0 ? acceptType : produceType); } private Encoder resolveEncoder(ResolvableType type, MediaType mediaType, Object... hints) { From c6713c23e327961d021706e5b1e4d5ebe8e2f458 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Mon, 2 Nov 2015 12:16:39 +0100 Subject: [PATCH 089/498] Add a ResolvableType field to HandlerResult This change allows to be able to check generic type on the return value at HandlerAdapter and ResultHandler level. For example, it allows to do a Publisher check in SimpleHandlerResultHandler. --- .../reactive/web/dispatch/HandlerResult.java | 9 ++- .../dispatch/SimpleHandlerResultHandler.java | 7 +- .../dispatch/handler/HttpHandlerAdapter.java | 5 +- .../RequestMappingHandlerAdapter.java | 4 +- .../annotation/ResponseBodyResultHandler.java | 3 +- .../SimpleHandlerResultHandlerTests.java | 68 +++++++++++++++++++ .../ResponseBodyResultHandlerTests.java | 10 ++- 7 files changed, 96 insertions(+), 10 deletions(-) create mode 100644 spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/SimpleHandlerResultHandlerTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerResult.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerResult.java index 156f2f5c96..429d688d2a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerResult.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerResult.java @@ -16,6 +16,7 @@ package org.springframework.reactive.web.dispatch; +import org.springframework.core.ResolvableType; /** * Represent the result of the invocation of an handler. @@ -28,10 +29,13 @@ public class HandlerResult { private final Object value; + private final ResolvableType type; - public HandlerResult(Object handler, Object value) { + + public HandlerResult(Object handler, Object value, ResolvableType type) { this.handler = handler; this.value = value; + this.type = type; } @@ -43,4 +47,7 @@ public class HandlerResult { return this.value; } + public ResolvableType getType() { + return type; + } } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/SimpleHandlerResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/SimpleHandlerResultHandler.java index e124bef083..e5dfd544ee 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/SimpleHandlerResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/SimpleHandlerResultHandler.java @@ -22,6 +22,7 @@ import org.reactivestreams.Publisher; import reactor.Publishers; import org.springframework.core.Ordered; +import org.springframework.core.ResolvableType; import org.springframework.http.server.ReactiveServerHttpRequest; import org.springframework.http.server.ReactiveServerHttpResponse; @@ -32,6 +33,8 @@ import org.springframework.http.server.ReactiveServerHttpResponse; */ public class SimpleHandlerResultHandler implements Ordered, HandlerResultHandler { + private static final ResolvableType PUBLISHER_VOID = ResolvableType.forClassWithGenerics(Publisher.class, Void.class); + private int order = Ordered.LOWEST_PRECEDENCE; @@ -46,8 +49,8 @@ public class SimpleHandlerResultHandler implements Ordered, HandlerResultHandler @Override public boolean supports(HandlerResult result) { - Object value = result.getValue(); - return value != null && Publisher.class.isAssignableFrom(value.getClass()); + ResolvableType type = result.getType(); + return type != null && PUBLISHER_VOID.isAssignableFrom(type); } @Override diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/handler/HttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/handler/HttpHandlerAdapter.java index 2204cd85fe..d8190b1428 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/handler/HttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/handler/HttpHandlerAdapter.java @@ -19,6 +19,7 @@ package org.springframework.reactive.web.dispatch.handler; import org.reactivestreams.Publisher; import reactor.Publishers; +import org.springframework.core.ResolvableType; import org.springframework.http.server.ReactiveServerHttpRequest; import org.springframework.http.server.ReactiveServerHttpResponse; import org.springframework.reactive.web.dispatch.HandlerAdapter; @@ -38,6 +39,8 @@ import org.springframework.reactive.web.http.HttpHandler; */ public class HttpHandlerAdapter implements HandlerAdapter { + private static final ResolvableType PUBLISHER_VOID = ResolvableType.forClassWithGenerics(Publisher.class, Void.class); + @Override public boolean supports(Object handler) { @@ -50,7 +53,7 @@ public class HttpHandlerAdapter implements HandlerAdapter { HttpHandler httpHandler = (HttpHandler)handler; Publisher completion = httpHandler.handle(request, response); - return Publishers.just(new HandlerResult(httpHandler, completion)); + return Publishers.just(new HandlerResult(httpHandler, completion, PUBLISHER_VOID)); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java index 6d3e94e3e6..f840cc5b78 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java @@ -24,6 +24,7 @@ import org.reactivestreams.Publisher; import reactor.Publishers; import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.ResolvableType; import org.springframework.core.convert.ConversionService; import org.springframework.http.server.ReactiveServerHttpRequest; import org.springframework.http.server.ReactiveServerHttpResponse; @@ -91,9 +92,10 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Initializin InvocableHandlerMethod handlerMethod = new InvocableHandlerMethod((HandlerMethod) handler); handlerMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers); + ResolvableType type = ResolvableType.forMethodParameter(handlerMethod.getReturnType()); Publisher resultPublisher = handlerMethod.invokeForRequest(request); - return Publishers.map(resultPublisher, result -> new HandlerResult(handlerMethod, result)); + return Publishers.map(resultPublisher, result -> new HandlerResult(handlerMethod, result, type)); } } \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java index 9567f7de48..a2d683d7ae 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java @@ -114,8 +114,7 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered return Publishers.empty(); } - HandlerMethod hm = (HandlerMethod) result.getHandler(); - ResolvableType returnType = ResolvableType.forMethodParameter(hm.getReturnValueType(value)); + ResolvableType returnType = result.getType(); List requestedMediaTypes = getAcceptableMediaTypes(request); List producibleMediaTypes = getProducibleMediaTypes(returnType); diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/SimpleHandlerResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/SimpleHandlerResultHandlerTests.java new file mode 100644 index 0000000000..fbb7bd2d6d --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/SimpleHandlerResultHandlerTests.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2015 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.reactive.web.dispatch; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import org.junit.Test; +import org.reactivestreams.Publisher; + +import org.springframework.core.ResolvableType; +import org.springframework.web.method.HandlerMethod; + +/** + * @author Sebastien Deleuze + */ +public class SimpleHandlerResultHandlerTests { + + @Test + public void supports() throws NoSuchMethodException { + + SimpleHandlerResultHandler resultHandler = new SimpleHandlerResultHandler(); + TestController controller = new TestController(); + + HandlerMethod hm = new HandlerMethod(controller, TestController.class.getMethod("voidReturnValue")); + ResolvableType type = ResolvableType.forMethodParameter(hm.getReturnType()); + assertFalse(resultHandler.supports(new HandlerResult(hm, null, type))); + + hm = new HandlerMethod(controller, TestController.class.getMethod("publisherString")); + type = ResolvableType.forMethodParameter(hm.getReturnType()); + assertFalse(resultHandler.supports(new HandlerResult(hm, null, type))); + + hm = new HandlerMethod(controller, TestController.class.getMethod("publisherVoid")); + type = ResolvableType.forMethodParameter(hm.getReturnType()); + assertTrue(resultHandler.supports(new HandlerResult(hm, null, type))); + } + + + @SuppressWarnings("unused") + private static class TestController { + + public Publisher voidReturnValue() { + return null; + } + + public Publisher publisherString() { + return null; + } + + public Publisher publisherVoid() { + return null; + } + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandlerTests.java index ea97ee81a9..7bab9e20ca 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandlerTests.java @@ -21,6 +21,7 @@ import java.util.Collections; import org.junit.Test; import org.reactivestreams.Publisher; +import org.springframework.core.ResolvableType; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.reactive.codec.encoder.StringEncoder; import org.springframework.reactive.web.dispatch.HandlerResult; @@ -43,13 +44,16 @@ public class ResponseBodyResultHandlerTests { TestController controller = new TestController(); HandlerMethod hm = new HandlerMethod(controller,TestController.class.getMethod("notAnnotated")); - assertFalse(handler.supports(new HandlerResult(hm, null))); + ResolvableType type = ResolvableType.forMethodParameter(hm.getReturnType()); + assertFalse(handler.supports(new HandlerResult(hm, null, type))); hm = new HandlerMethod(controller, TestController.class.getMethod("publisherString")); - assertTrue(handler.supports(new HandlerResult(hm, null))); + type = ResolvableType.forMethodParameter(hm.getReturnType()); + assertTrue(handler.supports(new HandlerResult(hm, null, type))); hm = new HandlerMethod(controller, TestController.class.getMethod("publisherVoid")); - assertTrue(handler.supports(new HandlerResult(hm, null))); + type = ResolvableType.forMethodParameter(hm.getReturnType()); + assertTrue(handler.supports(new HandlerResult(hm, null, type))); } From 54ce20a5e04275835b40f2cd1453db778264191e Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 13 Nov 2015 17:23:00 -0500 Subject: [PATCH 090/498] Rename field in HandlerResult --- .../reactive/web/dispatch/HandlerResult.java | 14 +++++++++----- .../web/dispatch/SimpleHandlerResultHandler.java | 2 +- .../web/dispatch/handler/HttpHandlerAdapter.java | 3 ++- .../annotation/ResponseBodyResultHandler.java | 2 +- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerResult.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerResult.java index 429d688d2a..7fa18753a4 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerResult.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerResult.java @@ -17,6 +17,7 @@ package org.springframework.reactive.web.dispatch; import org.springframework.core.ResolvableType; +import org.springframework.util.Assert; /** * Represent the result of the invocation of an handler. @@ -29,13 +30,15 @@ public class HandlerResult { private final Object value; - private final ResolvableType type; + private final ResolvableType valueType; - public HandlerResult(Object handler, Object value, ResolvableType type) { + public HandlerResult(Object handler, Object value, ResolvableType valueType) { + Assert.notNull(handler, "'handler' is required"); + Assert.notNull(handler, "'valueType' is required"); this.handler = handler; this.value = value; - this.type = type; + this.valueType = valueType; } @@ -47,7 +50,8 @@ public class HandlerResult { return this.value; } - public ResolvableType getType() { - return type; + public ResolvableType getValueType() { + return this.valueType; } + } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/SimpleHandlerResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/SimpleHandlerResultHandler.java index e5dfd544ee..86e130f0db 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/SimpleHandlerResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/SimpleHandlerResultHandler.java @@ -49,7 +49,7 @@ public class SimpleHandlerResultHandler implements Ordered, HandlerResultHandler @Override public boolean supports(HandlerResult result) { - ResolvableType type = result.getType(); + ResolvableType type = result.getValueType(); return type != null && PUBLISHER_VOID.isAssignableFrom(type); } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/handler/HttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/handler/HttpHandlerAdapter.java index d8190b1428..9e23b256b1 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/handler/HttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/handler/HttpHandlerAdapter.java @@ -39,7 +39,8 @@ import org.springframework.reactive.web.http.HttpHandler; */ public class HttpHandlerAdapter implements HandlerAdapter { - private static final ResolvableType PUBLISHER_VOID = ResolvableType.forClassWithGenerics(Publisher.class, Void.class); + private static final ResolvableType PUBLISHER_VOID = ResolvableType.forClassWithGenerics( + Publisher.class, Void.class); @Override diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java index a2d683d7ae..df6d3e764f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java @@ -114,7 +114,7 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered return Publishers.empty(); } - ResolvableType returnType = result.getType(); + ResolvableType returnType = result.getValueType(); List requestedMediaTypes = getAcceptableMediaTypes(request); List producibleMediaTypes = getProducibleMediaTypes(returnType); From 81867fa4237643ca3eae24a8f085bc4b5f9443b3 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 13 Nov 2015 17:49:31 -0500 Subject: [PATCH 091/498] Refactor package structure for web The web related code is now under org.springframework.web.reactive. This is parallel to org.springframework.web (the top-level package of spring-webmvc). --- .../reactive}/DispatcherHandler.java | 3 +-- .../dispatch => web/reactive}/HandlerAdapter.java | 2 +- .../dispatch => web/reactive}/HandlerMapping.java | 2 +- .../dispatch => web/reactive}/HandlerResult.java | 2 +- .../reactive}/HandlerResultHandler.java | 2 +- .../web/http => web/reactive}/HttpHandler.java | 2 +- .../reactive}/handler/HttpHandlerAdapter.java | 11 ++++++----- .../handler}/SimpleHandlerResultHandler.java | 4 +++- .../reactive}/handler/SimpleUrlHandlerMapping.java | 4 ++-- .../method/HandlerMethodArgumentResolver.java | 2 +- .../reactive}/method/InvocableHandlerMethod.java | 2 +- .../annotation/RequestBodyArgumentResolver.java | 4 ++-- .../annotation/RequestMappingHandlerAdapter.java | 10 +++++----- .../annotation/RequestMappingHandlerMapping.java | 5 ++--- .../annotation/RequestParamArgumentResolver.java | 4 ++-- .../annotation/ResponseBodyResultHandler.java | 6 +++--- .../reactor/PublisherReactorServerHttpRequest.java | 2 +- .../PublisherReactorServerHttpResponse.java | 2 +- .../server}/reactor/ReactorServerHttpRequest.java | 2 +- .../server}/reactor/ReactorServerHttpResponse.java | 2 +- .../server}/reactor/RequestHandlerAdapter.java | 4 ++-- .../server}/rxnetty/RequestHandlerAdapter.java | 4 ++-- .../server}/rxnetty/RxNettyServerHttpRequest.java | 2 +- .../server}/rxnetty/RxNettyServerHttpResponse.java | 2 +- .../server}/servlet/AsyncContextSynchronizer.java | 2 +- .../server}/servlet/HttpHandlerServlet.java | 4 ++-- .../server}/servlet/RequestBodyPublisher.java | 2 +- .../server}/servlet/ResponseBodySubscriber.java | 2 +- .../server}/servlet/ServletServerHttpRequest.java | 2 +- .../server}/servlet/ServletServerHttpResponse.java | 2 +- .../server}/undertow/RequestBodyPublisher.java | 2 +- .../server}/undertow/ResponseBodySubscriber.java | 2 +- .../undertow/UndertowHttpHandlerAdapter.java | 6 +++--- .../undertow/UndertowServerHttpRequest.java | 2 +- .../undertow/UndertowServerHttpResponse.java | 2 +- .../AbstractHttpHandlerIntegrationTests.java | 14 ++++++++------ .../springframework/web/reactive}/HttpServer.java | 3 ++- .../web/reactive}/HttpServerSupport.java | 4 +++- .../web/reactive}/JettyHttpServer.java | 5 ++--- .../web/reactive}/ReactorHttpServer.java | 5 ++--- .../web/reactive}/RxNettyHttpServer.java | 5 ++--- .../web/reactive}/TomcatHttpServer.java | 5 ++--- .../web/reactive}/UndertowHttpServer.java | 5 ++--- .../http => web/reactive/handler}/EchoHandler.java | 3 ++- .../handler}/EchoHandlerIntegrationTests.java | 3 ++- .../reactive/handler}/RandomHandler.java | 3 ++- .../handler}/RandomHandlerIntegrationTests.java | 3 ++- .../handler}/SimpleHandlerResultHandlerTests.java | 4 +++- .../SimpleUrlHandlerMappingIntegrationTests.java | 9 ++++----- .../http => web/reactive/handler}/XmlHandler.java | 3 ++- .../handler}/XmlHandlerIntegrationTests.java | 4 +++- .../RequestMappingHandlerMappingTests.java | 3 ++- .../annotation/RequestMappingIntegrationTests.java | 10 +++++----- .../annotation/ResponseBodyResultHandlerTests.java | 5 +++-- .../servlet/AsyncContextSynchronizerTests.java | 4 +++- 55 files changed, 113 insertions(+), 100 deletions(-) rename spring-web-reactive/src/main/java/org/springframework/{reactive/web/dispatch => web/reactive}/DispatcherHandler.java (97%) rename spring-web-reactive/src/main/java/org/springframework/{reactive/web/dispatch => web/reactive}/HandlerAdapter.java (97%) rename spring-web-reactive/src/main/java/org/springframework/{reactive/web/dispatch => web/reactive}/HandlerMapping.java (94%) rename spring-web-reactive/src/main/java/org/springframework/{reactive/web/dispatch => web/reactive}/HandlerResult.java (96%) rename spring-web-reactive/src/main/java/org/springframework/{reactive/web/dispatch => web/reactive}/HandlerResultHandler.java (97%) rename spring-web-reactive/src/main/java/org/springframework/{reactive/web/http => web/reactive}/HttpHandler.java (97%) rename spring-web-reactive/src/main/java/org/springframework/{reactive/web/dispatch => web/reactive}/handler/HttpHandlerAdapter.java (85%) rename spring-web-reactive/src/main/java/org/springframework/{reactive/web/dispatch => web/reactive/handler}/SimpleHandlerResultHandler.java (92%) rename spring-web-reactive/src/main/java/org/springframework/{reactive/web/dispatch => web/reactive}/handler/SimpleUrlHandlerMapping.java (90%) rename spring-web-reactive/src/main/java/org/springframework/{reactive/web/dispatch => web/reactive}/method/HandlerMethodArgumentResolver.java (95%) rename spring-web-reactive/src/main/java/org/springframework/{reactive/web/dispatch => web/reactive}/method/InvocableHandlerMethod.java (99%) rename spring-web-reactive/src/main/java/org/springframework/{reactive/web/dispatch => web/reactive}/method/annotation/RequestBodyArgumentResolver.java (95%) rename spring-web-reactive/src/main/java/org/springframework/{reactive/web/dispatch => web/reactive}/method/annotation/RequestMappingHandlerAdapter.java (90%) rename spring-web-reactive/src/main/java/org/springframework/{reactive/web/dispatch => web/reactive}/method/annotation/RequestMappingHandlerMapping.java (96%) rename spring-web-reactive/src/main/java/org/springframework/{reactive/web/dispatch => web/reactive}/method/annotation/RequestParamArgumentResolver.java (92%) rename spring-web-reactive/src/main/java/org/springframework/{reactive/web/dispatch => web/reactive}/method/annotation/ResponseBodyResultHandler.java (97%) rename spring-web-reactive/src/main/java/org/springframework/{reactive/web/http => web/reactive/server}/reactor/PublisherReactorServerHttpRequest.java (97%) rename spring-web-reactive/src/main/java/org/springframework/{reactive/web/http => web/reactive/server}/reactor/PublisherReactorServerHttpResponse.java (97%) rename spring-web-reactive/src/main/java/org/springframework/{reactive/web/http => web/reactive/server}/reactor/ReactorServerHttpRequest.java (95%) rename spring-web-reactive/src/main/java/org/springframework/{reactive/web/http => web/reactive/server}/reactor/ReactorServerHttpResponse.java (95%) rename spring-web-reactive/src/main/java/org/springframework/{reactive/web/http => web/reactive/server}/reactor/RequestHandlerAdapter.java (94%) rename spring-web-reactive/src/main/java/org/springframework/{reactive/web/http => web/reactive/server}/rxnetty/RequestHandlerAdapter.java (93%) rename spring-web-reactive/src/main/java/org/springframework/{reactive/web/http => web/reactive/server}/rxnetty/RxNettyServerHttpRequest.java (97%) rename spring-web-reactive/src/main/java/org/springframework/{reactive/web/http => web/reactive/server}/rxnetty/RxNettyServerHttpResponse.java (97%) rename spring-web-reactive/src/main/java/org/springframework/{reactive/web/http => web/reactive/server}/servlet/AsyncContextSynchronizer.java (98%) rename spring-web-reactive/src/main/java/org/springframework/{reactive/web/http => web/reactive/server}/servlet/HttpHandlerServlet.java (96%) rename spring-web-reactive/src/main/java/org/springframework/{reactive/web/http => web/reactive/server}/servlet/RequestBodyPublisher.java (98%) rename spring-web-reactive/src/main/java/org/springframework/{reactive/web/http => web/reactive/server}/servlet/ResponseBodySubscriber.java (97%) rename spring-web-reactive/src/main/java/org/springframework/{reactive/web/http => web/reactive/server}/servlet/ServletServerHttpRequest.java (98%) rename spring-web-reactive/src/main/java/org/springframework/{reactive/web/http => web/reactive/server}/servlet/ServletServerHttpResponse.java (98%) rename spring-web-reactive/src/main/java/org/springframework/{reactive/web/http => web/reactive/server}/undertow/RequestBodyPublisher.java (98%) rename spring-web-reactive/src/main/java/org/springframework/{reactive/web/http => web/reactive/server}/undertow/ResponseBodySubscriber.java (98%) rename spring-web-reactive/src/main/java/org/springframework/{reactive/web/http => web/reactive/server}/undertow/UndertowHttpHandlerAdapter.java (92%) rename spring-web-reactive/src/main/java/org/springframework/{reactive/web/http => web/reactive/server}/undertow/UndertowServerHttpRequest.java (97%) rename spring-web-reactive/src/main/java/org/springframework/{reactive/web/http => web/reactive/server}/undertow/UndertowServerHttpResponse.java (97%) rename spring-web-reactive/src/test/java/org/springframework/{reactive/web/http => web/reactive}/AbstractHttpHandlerIntegrationTests.java (78%) rename spring-web-reactive/src/{main/java/org/springframework/reactive/web/http => test/java/org/springframework/web/reactive}/HttpServer.java (90%) rename spring-web-reactive/src/{main/java/org/springframework/reactive/web/http => test/java/org/springframework/web/reactive}/HttpServerSupport.java (91%) rename spring-web-reactive/src/{main/java/org/springframework/reactive/web/http/servlet => test/java/org/springframework/web/reactive}/JettyHttpServer.java (92%) rename spring-web-reactive/src/{main/java/org/springframework/reactive/web/http/reactor => test/java/org/springframework/web/reactive}/ReactorHttpServer.java (91%) rename spring-web-reactive/src/{main/java/org/springframework/reactive/web/http/rxnetty => test/java/org/springframework/web/reactive}/RxNettyHttpServer.java (90%) rename spring-web-reactive/src/{main/java/org/springframework/reactive/web/http/servlet => test/java/org/springframework/web/reactive}/TomcatHttpServer.java (92%) rename spring-web-reactive/src/{main/java/org/springframework/reactive/web/http/undertow => test/java/org/springframework/web/reactive}/UndertowHttpServer.java (90%) rename spring-web-reactive/src/test/java/org/springframework/{reactive/web/http => web/reactive/handler}/EchoHandler.java (91%) rename spring-web-reactive/src/test/java/org/springframework/{reactive/web/http => web/reactive/handler}/EchoHandlerIntegrationTests.java (95%) rename spring-web-reactive/src/test/java/org/springframework/{reactive/web/http => web/reactive/handler}/RandomHandler.java (95%) rename spring-web-reactive/src/test/java/org/springframework/{reactive/web/http => web/reactive/handler}/RandomHandlerIntegrationTests.java (93%) rename spring-web-reactive/src/test/java/org/springframework/{reactive/web/dispatch => web/reactive/handler}/SimpleHandlerResultHandlerTests.java (92%) rename spring-web-reactive/src/test/java/org/springframework/{reactive/web/dispatch => web/reactive}/handler/SimpleUrlHandlerMappingIntegrationTests.java (91%) rename spring-web-reactive/src/test/java/org/springframework/{reactive/web/http => web/reactive/handler}/XmlHandler.java (96%) rename spring-web-reactive/src/test/java/org/springframework/{reactive/web/http => web/reactive/handler}/XmlHandlerIntegrationTests.java (91%) rename spring-web-reactive/src/test/java/org/springframework/{reactive/web/dispatch => web/reactive}/method/annotation/RequestMappingHandlerMappingTests.java (96%) rename spring-web-reactive/src/test/java/org/springframework/{reactive/web/dispatch => web/reactive}/method/annotation/RequestMappingIntegrationTests.java (97%) rename spring-web-reactive/src/test/java/org/springframework/{reactive/web/dispatch => web/reactive}/method/annotation/ResponseBodyResultHandlerTests.java (92%) rename spring-web-reactive/src/test/java/org/springframework/{reactive/web/http => web/reactive/server}/servlet/AsyncContextSynchronizerTests.java (90%) diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/DispatcherHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java similarity index 97% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/DispatcherHandler.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java index 51c9945e81..eabcfcf16d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/DispatcherHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.web.dispatch; +package org.springframework.web.reactive; import java.util.ArrayList; import java.util.List; @@ -33,7 +33,6 @@ import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.http.HttpStatus; import org.springframework.http.server.ReactiveServerHttpRequest; import org.springframework.http.server.ReactiveServerHttpResponse; -import org.springframework.reactive.web.http.HttpHandler; /** * Central dispatcher for HTTP request handlers/controllers. Dispatches to registered diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerAdapter.java similarity index 97% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerAdapter.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerAdapter.java index f9788ffb15..605676a56b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerAdapter.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.web.dispatch; +package org.springframework.web.reactive; import org.reactivestreams.Publisher; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerMapping.java similarity index 94% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerMapping.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerMapping.java index 2a087989a7..89e11f6521 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerMapping.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerMapping.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.web.dispatch; +package org.springframework.web.reactive; import org.springframework.http.server.ReactiveServerHttpRequest; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerResult.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java similarity index 96% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerResult.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java index 7fa18753a4..b2e921e143 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerResult.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.web.dispatch; +package org.springframework.web.reactive; import org.springframework.core.ResolvableType; import org.springframework.util.Assert; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResultHandler.java similarity index 97% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerResultHandler.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResultHandler.java index 3f3b593dca..91a3cb3535 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/HandlerResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResultHandler.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.web.dispatch; +package org.springframework.web.reactive; import org.reactivestreams.Publisher; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/HttpHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HttpHandler.java similarity index 97% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/http/HttpHandler.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/HttpHandler.java index ff86a9a722..9f97ce023d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/HttpHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HttpHandler.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.web.http; +package org.springframework.web.reactive; import org.reactivestreams.Publisher; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/handler/HttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/HttpHandlerAdapter.java similarity index 85% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/handler/HttpHandlerAdapter.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/HttpHandlerAdapter.java index 9e23b256b1..22f3375203 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/handler/HttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/HttpHandlerAdapter.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.web.dispatch.handler; +package org.springframework.web.reactive.handler; import org.reactivestreams.Publisher; import reactor.Publishers; @@ -22,13 +22,14 @@ import reactor.Publishers; import org.springframework.core.ResolvableType; import org.springframework.http.server.ReactiveServerHttpRequest; import org.springframework.http.server.ReactiveServerHttpResponse; -import org.springframework.reactive.web.dispatch.HandlerAdapter; -import org.springframework.reactive.web.dispatch.HandlerResult; -import org.springframework.reactive.web.http.HttpHandler; +import org.springframework.web.reactive.HandlerAdapter; +import org.springframework.web.reactive.HandlerResult; +import org.springframework.web.reactive.HttpHandler; +import org.springframework.web.reactive.DispatcherHandler; /** * Support use of {@link HttpHandler} with - * {@link org.springframework.reactive.web.dispatch.DispatcherHandler + * {@link DispatcherHandler * DispatcherHandler} (which implements the same contract). * The use of {@code DispatcherHandler} this way enables routing requests to * one of many {@code HttpHandler} instances depending on the configured diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/SimpleHandlerResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java similarity index 92% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/SimpleHandlerResultHandler.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java index 86e130f0db..628ac443b5 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/SimpleHandlerResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.web.dispatch; +package org.springframework.web.reactive.handler; import java.util.Arrays; @@ -25,6 +25,8 @@ import org.springframework.core.Ordered; import org.springframework.core.ResolvableType; import org.springframework.http.server.ReactiveServerHttpRequest; import org.springframework.http.server.ReactiveServerHttpResponse; +import org.springframework.web.reactive.HandlerResult; +import org.springframework.web.reactive.HandlerResultHandler; /** * Supports {@link HandlerResult} with a {@code Publisher} value. diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/handler/SimpleUrlHandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMapping.java similarity index 90% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/handler/SimpleUrlHandlerMapping.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMapping.java index c49f9fa0c5..8e1c906dd2 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/handler/SimpleUrlHandlerMapping.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMapping.java @@ -14,13 +14,13 @@ * limitations under the License. */ -package org.springframework.reactive.web.dispatch.handler; +package org.springframework.web.reactive.handler; import java.util.HashMap; import java.util.Map; import org.springframework.http.server.ReactiveServerHttpRequest; -import org.springframework.reactive.web.dispatch.HandlerMapping; +import org.springframework.web.reactive.HandlerMapping; /** * @author Rossen Stoyanchev diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/HandlerMethodArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/HandlerMethodArgumentResolver.java similarity index 95% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/HandlerMethodArgumentResolver.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/method/HandlerMethodArgumentResolver.java index a7b5b16f95..b2b018904c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/HandlerMethodArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/HandlerMethodArgumentResolver.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.web.dispatch.method; +package org.springframework.web.reactive.method; import org.reactivestreams.Publisher; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/InvocableHandlerMethod.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java similarity index 99% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/InvocableHandlerMethod.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java index 29dc84358e..b996faf811 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/InvocableHandlerMethod.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.web.dispatch.method; +package org.springframework.web.reactive.method; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestBodyArgumentResolver.java similarity index 95% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestBodyArgumentResolver.java index fab1615b35..853494d410 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestBodyArgumentResolver.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.web.dispatch.method.annotation; +package org.springframework.web.reactive.method.annotation; import java.nio.ByteBuffer; import java.util.List; @@ -28,7 +28,7 @@ import org.springframework.core.convert.ConversionService; import org.springframework.http.MediaType; import org.springframework.http.server.ReactiveServerHttpRequest; import org.springframework.reactive.codec.decoder.Decoder; -import org.springframework.reactive.web.dispatch.method.HandlerMethodArgumentResolver; +import org.springframework.web.reactive.method.HandlerMethodArgumentResolver; import org.springframework.util.Assert; import org.springframework.web.bind.annotation.RequestBody; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java similarity index 90% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java index f840cc5b78..e58f894289 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.web.dispatch.method.annotation; +package org.springframework.web.reactive.method.annotation; import java.util.ArrayList; import java.util.Arrays; @@ -33,10 +33,10 @@ import org.springframework.reactive.codec.decoder.Decoder; import org.springframework.reactive.codec.decoder.JacksonJsonDecoder; import org.springframework.reactive.codec.decoder.JsonObjectDecoder; import org.springframework.reactive.codec.decoder.StringDecoder; -import org.springframework.reactive.web.dispatch.HandlerAdapter; -import org.springframework.reactive.web.dispatch.HandlerResult; -import org.springframework.reactive.web.dispatch.method.HandlerMethodArgumentResolver; -import org.springframework.reactive.web.dispatch.method.InvocableHandlerMethod; +import org.springframework.web.reactive.HandlerAdapter; +import org.springframework.web.reactive.HandlerResult; +import org.springframework.web.reactive.method.HandlerMethodArgumentResolver; +import org.springframework.web.reactive.method.InvocableHandlerMethod; import org.springframework.web.method.HandlerMethod; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMapping.java similarity index 96% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerMapping.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMapping.java index c67acd65cf..8fa2c682be 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerMapping.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMapping.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.web.dispatch.method.annotation; +package org.springframework.web.reactive.method.annotation; import java.util.Arrays; import java.util.Collection; @@ -32,9 +32,8 @@ import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.http.HttpMethod; import org.springframework.http.server.ReactiveServerHttpRequest; -import org.springframework.reactive.web.dispatch.HandlerMapping; +import org.springframework.web.reactive.HandlerMapping; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestParamArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestParamArgumentResolver.java similarity index 92% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestParamArgumentResolver.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestParamArgumentResolver.java index 124f840b13..909adb598b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestParamArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestParamArgumentResolver.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.web.dispatch.method.annotation; +package org.springframework.web.reactive.method.annotation; import java.util.Optional; @@ -24,7 +24,7 @@ import reactor.Publishers; import org.springframework.core.MethodParameter; import org.springframework.http.server.ReactiveServerHttpRequest; -import org.springframework.reactive.web.dispatch.method.HandlerMethodArgumentResolver; +import org.springframework.web.reactive.method.HandlerMethodArgumentResolver; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java similarity index 97% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java index df6d3e764f..95e511979a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.web.dispatch.method.annotation; +package org.springframework.web.reactive.method.annotation; import java.lang.reflect.Method; import java.util.ArrayList; @@ -35,8 +35,8 @@ import org.springframework.http.MediaType; import org.springframework.http.server.ReactiveServerHttpRequest; import org.springframework.http.server.ReactiveServerHttpResponse; import org.springframework.reactive.codec.encoder.Encoder; -import org.springframework.reactive.web.dispatch.HandlerResult; -import org.springframework.reactive.web.dispatch.HandlerResultHandler; +import org.springframework.web.reactive.HandlerResult; +import org.springframework.web.reactive.HandlerResultHandler; import org.springframework.util.Assert; import org.springframework.util.MimeType; import org.springframework.web.HttpMediaTypeNotAcceptableException; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/PublisherReactorServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/reactor/PublisherReactorServerHttpRequest.java similarity index 97% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/PublisherReactorServerHttpRequest.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/server/reactor/PublisherReactorServerHttpRequest.java index 81c761230b..d0c319216c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/PublisherReactorServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/reactor/PublisherReactorServerHttpRequest.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.reactive.web.http.reactor; +package org.springframework.web.reactive.server.reactor; import java.net.URI; import java.net.URISyntaxException; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/PublisherReactorServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/reactor/PublisherReactorServerHttpResponse.java similarity index 97% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/PublisherReactorServerHttpResponse.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/server/reactor/PublisherReactorServerHttpResponse.java index d6b25ba454..78a820a2da 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/PublisherReactorServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/reactor/PublisherReactorServerHttpResponse.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.reactive.web.http.reactor; +package org.springframework.web.reactive.server.reactor; import java.nio.ByteBuffer; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/reactor/ReactorServerHttpRequest.java similarity index 95% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpRequest.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/server/reactor/ReactorServerHttpRequest.java index 7dff1f2b7c..ad6913d333 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/reactor/ReactorServerHttpRequest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.web.http.reactor; +package org.springframework.web.reactive.server.reactor; import java.nio.ByteBuffer; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/reactor/ReactorServerHttpResponse.java similarity index 95% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpResponse.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/server/reactor/ReactorServerHttpResponse.java index 831e0f2478..fdf8c4bf3e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/reactor/ReactorServerHttpResponse.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.web.http.reactor; +package org.springframework.web.reactive.server.reactor; import java.nio.ByteBuffer; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/RequestHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/reactor/RequestHandlerAdapter.java similarity index 94% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/RequestHandlerAdapter.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/server/reactor/RequestHandlerAdapter.java index 2e6d120794..1b70426bbb 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/RequestHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/reactor/RequestHandlerAdapter.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.reactive.web.http.reactor; +package org.springframework.web.reactive.server.reactor; import org.reactivestreams.Publisher; import reactor.core.publisher.convert.DependencyUtils; @@ -21,7 +21,7 @@ import reactor.io.buffer.Buffer; import reactor.io.net.ReactiveChannelHandler; import reactor.io.net.http.HttpChannel; -import org.springframework.reactive.web.http.HttpHandler; +import org.springframework.web.reactive.HttpHandler; import org.springframework.util.Assert; /** diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RequestHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/rxnetty/RequestHandlerAdapter.java similarity index 93% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RequestHandlerAdapter.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/server/rxnetty/RequestHandlerAdapter.java index 1a8fea2b8f..0448e99be4 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RequestHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/rxnetty/RequestHandlerAdapter.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.web.http.rxnetty; +package org.springframework.web.reactive.server.rxnetty; import io.netty.buffer.ByteBuf; import io.reactivex.netty.protocol.http.server.HttpServerRequest; @@ -24,7 +24,7 @@ import org.reactivestreams.Publisher; import reactor.core.publisher.convert.RxJava1Converter; import rx.Observable; -import org.springframework.reactive.web.http.HttpHandler; +import org.springframework.web.reactive.HttpHandler; import org.springframework.util.Assert; /** diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/rxnetty/RxNettyServerHttpRequest.java similarity index 97% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpRequest.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/server/rxnetty/RxNettyServerHttpRequest.java index 5f3be593e6..48be0c222d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/rxnetty/RxNettyServerHttpRequest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.web.http.rxnetty; +package org.springframework.web.reactive.server.rxnetty; import java.net.URI; import java.net.URISyntaxException; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/rxnetty/RxNettyServerHttpResponse.java similarity index 97% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpResponse.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/server/rxnetty/RxNettyServerHttpResponse.java index 4233be995c..9a51590e52 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/rxnetty/RxNettyServerHttpResponse.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.web.http.rxnetty; +package org.springframework.web.reactive.server.rxnetty; import java.nio.ByteBuffer; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/AsyncContextSynchronizer.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/servlet/AsyncContextSynchronizer.java similarity index 98% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/AsyncContextSynchronizer.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/server/servlet/AsyncContextSynchronizer.java index 65240b963c..8bb6d36004 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/AsyncContextSynchronizer.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/servlet/AsyncContextSynchronizer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.web.http.servlet; +package org.springframework.web.reactive.server.servlet; import java.io.IOException; import java.util.concurrent.atomic.AtomicInteger; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/HttpHandlerServlet.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/servlet/HttpHandlerServlet.java similarity index 96% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/HttpHandlerServlet.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/server/servlet/HttpHandlerServlet.java index 63563aba43..0b91603bec 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/HttpHandlerServlet.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/servlet/HttpHandlerServlet.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.web.http.servlet; +package org.springframework.web.reactive.server.servlet; import java.io.IOException; import javax.servlet.AsyncContext; @@ -30,7 +30,7 @@ import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import org.springframework.http.HttpStatus; -import org.springframework.reactive.web.http.HttpHandler; +import org.springframework.web.reactive.HttpHandler; /** * @author Arjen Poutsma diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/RequestBodyPublisher.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/servlet/RequestBodyPublisher.java similarity index 98% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/RequestBodyPublisher.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/server/servlet/RequestBodyPublisher.java index 12368fc183..e22560d325 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/RequestBodyPublisher.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/servlet/RequestBodyPublisher.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.web.http.servlet; +package org.springframework.web.reactive.server.servlet; import java.io.IOException; import java.nio.ByteBuffer; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ResponseBodySubscriber.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/servlet/ResponseBodySubscriber.java similarity index 97% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ResponseBodySubscriber.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/server/servlet/ResponseBodySubscriber.java index 4d7ca62011..028e4aa4ba 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ResponseBodySubscriber.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/servlet/ResponseBodySubscriber.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.web.http.servlet; +package org.springframework.web.reactive.server.servlet; import java.io.IOException; import java.nio.ByteBuffer; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/servlet/ServletServerHttpRequest.java similarity index 98% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpRequest.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/server/servlet/ServletServerHttpRequest.java index 34aca4ee29..407474a4e8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/servlet/ServletServerHttpRequest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.web.http.servlet; +package org.springframework.web.reactive.server.servlet; import java.net.URI; import java.net.URISyntaxException; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/servlet/ServletServerHttpResponse.java similarity index 98% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpResponse.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/server/servlet/ServletServerHttpResponse.java index 3a9949eb5b..25709e4000 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/ServletServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/servlet/ServletServerHttpResponse.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.web.http.servlet; +package org.springframework.web.reactive.server.servlet; import java.nio.ByteBuffer; import java.nio.charset.Charset; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/RequestBodyPublisher.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/undertow/RequestBodyPublisher.java similarity index 98% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/RequestBodyPublisher.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/server/undertow/RequestBodyPublisher.java index db0a0e89e7..5c1b567b20 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/RequestBodyPublisher.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/undertow/RequestBodyPublisher.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.web.http.undertow; +package org.springframework.web.reactive.server.undertow; import static org.xnio.IoUtils.safeClose; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/ResponseBodySubscriber.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/undertow/ResponseBodySubscriber.java similarity index 98% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/ResponseBodySubscriber.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/server/undertow/ResponseBodySubscriber.java index b78a687fa6..d362085095 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/ResponseBodySubscriber.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/undertow/ResponseBodySubscriber.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.web.http.undertow; +package org.springframework.web.reactive.server.undertow; import java.io.IOException; import java.nio.ByteBuffer; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/UndertowHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/undertow/UndertowHttpHandlerAdapter.java similarity index 92% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/UndertowHttpHandlerAdapter.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/server/undertow/UndertowHttpHandlerAdapter.java index 92ad9ff3f0..365aed0054 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/UndertowHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/undertow/UndertowHttpHandlerAdapter.java @@ -14,11 +14,11 @@ * limitations under the License. */ -package org.springframework.reactive.web.http.undertow; +package org.springframework.web.reactive.server.undertow; import org.springframework.http.server.ReactiveServerHttpRequest; import org.springframework.http.server.ReactiveServerHttpResponse; -import org.springframework.reactive.web.http.HttpHandler; +import org.springframework.web.reactive.HttpHandler; import org.springframework.util.Assert; import io.undertow.server.HttpServerExchange; @@ -32,7 +32,7 @@ import org.reactivestreams.Subscription; * @author Marek Hawrylczak * @author Rossen Stoyanchev */ -class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandler { +public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandler { private static Log logger = LogFactory.getLog(UndertowHttpHandlerAdapter.class); diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/UndertowServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/undertow/UndertowServerHttpRequest.java similarity index 97% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/UndertowServerHttpRequest.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/server/undertow/UndertowServerHttpRequest.java index 6f856595f3..4d68be46ba 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/UndertowServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/undertow/UndertowServerHttpRequest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.web.http.undertow; +package org.springframework.web.reactive.server.undertow; import java.net.URI; import java.net.URISyntaxException; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/UndertowServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/undertow/UndertowServerHttpResponse.java similarity index 97% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/UndertowServerHttpResponse.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/server/undertow/UndertowServerHttpResponse.java index 5efe890de1..0897492291 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/UndertowServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/undertow/UndertowServerHttpResponse.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.web.http.undertow; +package org.springframework.web.reactive.server.undertow; import java.nio.ByteBuffer; import java.util.List; diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/AbstractHttpHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/AbstractHttpHandlerIntegrationTests.java similarity index 78% rename from spring-web-reactive/src/test/java/org/springframework/reactive/web/http/AbstractHttpHandlerIntegrationTests.java rename to spring-web-reactive/src/test/java/org/springframework/web/reactive/AbstractHttpHandlerIntegrationTests.java index 41136ac8b0..96b694bdac 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/AbstractHttpHandlerIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/AbstractHttpHandlerIntegrationTests.java @@ -14,19 +14,21 @@ * limitations under the License. */ -package org.springframework.reactive.web.http; +package org.springframework.web.reactive; import org.junit.After; import org.junit.Before; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; -import org.springframework.reactive.web.http.reactor.ReactorHttpServer; -import org.springframework.reactive.web.http.rxnetty.RxNettyHttpServer; -import org.springframework.reactive.web.http.servlet.JettyHttpServer; -import org.springframework.reactive.web.http.servlet.TomcatHttpServer; -import org.springframework.reactive.web.http.undertow.UndertowHttpServer; +import org.springframework.web.reactive.HttpServer; +import org.springframework.web.reactive.ReactorHttpServer; +import org.springframework.web.reactive.RxNettyHttpServer; +import org.springframework.web.reactive.JettyHttpServer; +import org.springframework.web.reactive.TomcatHttpServer; +import org.springframework.web.reactive.UndertowHttpServer; import org.springframework.util.SocketUtils; +import org.springframework.web.reactive.HttpHandler; @RunWith(Parameterized.class) diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/HttpServer.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/HttpServer.java similarity index 90% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/http/HttpServer.java rename to spring-web-reactive/src/test/java/org/springframework/web/reactive/HttpServer.java index 6006e97422..f869d33b04 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/HttpServer.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/HttpServer.java @@ -14,11 +14,12 @@ * limitations under the License. */ -package org.springframework.reactive.web.http; +package org.springframework.web.reactive; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.Lifecycle; +import org.springframework.web.reactive.HttpHandler; /** * @author Rossen Stoyanchev diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/HttpServerSupport.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/HttpServerSupport.java similarity index 91% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/http/HttpServerSupport.java rename to spring-web-reactive/src/test/java/org/springframework/web/reactive/HttpServerSupport.java index f7aa9aae8b..71704f408e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/HttpServerSupport.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/HttpServerSupport.java @@ -14,9 +14,11 @@ * limitations under the License. */ -package org.springframework.reactive.web.http; +package org.springframework.web.reactive; +import org.springframework.web.reactive.HttpHandler; + /** * @author Rossen Stoyanchev */ diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/JettyHttpServer.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/JettyHttpServer.java similarity index 92% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/JettyHttpServer.java rename to spring-web-reactive/src/test/java/org/springframework/web/reactive/JettyHttpServer.java index 4acad5dfe7..a7f5ec7524 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/JettyHttpServer.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/JettyHttpServer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.web.http.servlet; +package org.springframework.web.reactive; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; @@ -22,10 +22,9 @@ import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder; import org.springframework.beans.factory.InitializingBean; -import org.springframework.reactive.web.http.HttpServer; -import org.springframework.reactive.web.http.HttpServerSupport; import org.springframework.util.Assert; import org.springframework.util.SocketUtils; +import org.springframework.web.reactive.server.servlet.HttpHandlerServlet; /** * @author Rossen Stoyanchev diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorHttpServer.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/ReactorHttpServer.java similarity index 91% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorHttpServer.java rename to spring-web-reactive/src/test/java/org/springframework/web/reactive/ReactorHttpServer.java index 5528fea3a8..a824f85686 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/reactor/ReactorHttpServer.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/ReactorHttpServer.java @@ -14,15 +14,14 @@ * limitations under the License. */ -package org.springframework.reactive.web.http.reactor; +package org.springframework.web.reactive; import reactor.io.buffer.Buffer; import reactor.io.net.ReactiveNet; import org.springframework.beans.factory.InitializingBean; -import org.springframework.reactive.web.http.HttpServer; -import org.springframework.reactive.web.http.HttpServerSupport; import org.springframework.util.Assert; +import org.springframework.web.reactive.server.reactor.RequestHandlerAdapter; /** * @author Stephane Maldini diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyHttpServer.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/RxNettyHttpServer.java similarity index 90% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyHttpServer.java rename to spring-web-reactive/src/test/java/org/springframework/web/reactive/RxNettyHttpServer.java index 21dc109a2a..a7cc2fc2a8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/rxnetty/RxNettyHttpServer.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/RxNettyHttpServer.java @@ -14,14 +14,13 @@ * limitations under the License. */ -package org.springframework.reactive.web.http.rxnetty; +package org.springframework.web.reactive; import io.netty.buffer.ByteBuf; import org.springframework.beans.factory.InitializingBean; -import org.springframework.reactive.web.http.HttpServer; -import org.springframework.reactive.web.http.HttpServerSupport; import org.springframework.util.Assert; +import org.springframework.web.reactive.server.rxnetty.RequestHandlerAdapter; /** diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/TomcatHttpServer.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/TomcatHttpServer.java similarity index 92% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/TomcatHttpServer.java rename to spring-web-reactive/src/test/java/org/springframework/web/reactive/TomcatHttpServer.java index 472a74afc3..c1c588696c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/servlet/TomcatHttpServer.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/TomcatHttpServer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.web.http.servlet; +package org.springframework.web.reactive; import java.io.File; @@ -23,10 +23,9 @@ import org.apache.catalina.LifecycleException; import org.apache.catalina.startup.Tomcat; import org.springframework.beans.factory.InitializingBean; -import org.springframework.reactive.web.http.HttpServer; -import org.springframework.reactive.web.http.HttpServerSupport; import org.springframework.util.Assert; import org.springframework.util.SocketUtils; +import org.springframework.web.reactive.server.servlet.HttpHandlerServlet; /** diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/UndertowHttpServer.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/UndertowHttpServer.java similarity index 90% rename from spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/UndertowHttpServer.java rename to spring-web-reactive/src/test/java/org/springframework/web/reactive/UndertowHttpServer.java index b55fa2201f..511490ab86 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/http/undertow/UndertowHttpServer.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/UndertowHttpServer.java @@ -14,12 +14,11 @@ * limitations under the License. */ -package org.springframework.reactive.web.http.undertow; +package org.springframework.web.reactive; import org.springframework.beans.factory.InitializingBean; -import org.springframework.reactive.web.http.HttpServer; -import org.springframework.reactive.web.http.HttpServerSupport; import org.springframework.util.Assert; +import org.springframework.web.reactive.server.undertow.UndertowHttpHandlerAdapter; import io.undertow.Undertow; import io.undertow.server.HttpHandler; diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/EchoHandler.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/EchoHandler.java similarity index 91% rename from spring-web-reactive/src/test/java/org/springframework/reactive/web/http/EchoHandler.java rename to spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/EchoHandler.java index 907191b95f..a3013c131a 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/EchoHandler.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/EchoHandler.java @@ -14,12 +14,13 @@ * limitations under the License. */ -package org.springframework.reactive.web.http; +package org.springframework.web.reactive.handler; import org.reactivestreams.Publisher; import org.springframework.http.server.ReactiveServerHttpRequest; import org.springframework.http.server.ReactiveServerHttpResponse; +import org.springframework.web.reactive.HttpHandler; /** * @author Arjen Poutsma diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/EchoHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/EchoHandlerIntegrationTests.java similarity index 95% rename from spring-web-reactive/src/test/java/org/springframework/reactive/web/http/EchoHandlerIntegrationTests.java rename to spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/EchoHandlerIntegrationTests.java index 270aa1a9da..811d53f812 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/EchoHandlerIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/EchoHandlerIntegrationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.web.http; +package org.springframework.web.reactive.handler; import java.net.URI; import java.util.Random; @@ -24,6 +24,7 @@ import org.junit.Test; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; import org.springframework.web.client.RestTemplate; +import org.springframework.web.reactive.AbstractHttpHandlerIntegrationTests; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/RandomHandler.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/RandomHandler.java similarity index 95% rename from spring-web-reactive/src/test/java/org/springframework/reactive/web/http/RandomHandler.java rename to spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/RandomHandler.java index 79a87fd76e..53f300686c 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/RandomHandler.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/RandomHandler.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.web.http; +package org.springframework.web.reactive.handler; import java.nio.ByteBuffer; import java.util.Random; @@ -29,6 +29,7 @@ import reactor.rx.Streams; import org.springframework.http.server.ReactiveServerHttpRequest; import org.springframework.http.server.ReactiveServerHttpResponse; +import org.springframework.web.reactive.HttpHandler; import static org.junit.Assert.assertEquals; diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/RandomHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/RandomHandlerIntegrationTests.java similarity index 93% rename from spring-web-reactive/src/test/java/org/springframework/reactive/web/http/RandomHandlerIntegrationTests.java rename to spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/RandomHandlerIntegrationTests.java index 3d3e0fdf45..383eb5dfc1 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/RandomHandlerIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/RandomHandlerIntegrationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.web.http; +package org.springframework.web.reactive.handler; import java.net.URI; import java.util.Random; @@ -24,6 +24,7 @@ import org.junit.Test; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; import org.springframework.web.client.RestTemplate; +import org.springframework.web.reactive.AbstractHttpHandlerIntegrationTests; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/SimpleHandlerResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandlerTests.java similarity index 92% rename from spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/SimpleHandlerResultHandlerTests.java rename to spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandlerTests.java index fbb7bd2d6d..51c0a80ee5 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/SimpleHandlerResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandlerTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.web.dispatch; +package org.springframework.web.reactive.handler; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -23,6 +23,8 @@ import org.reactivestreams.Publisher; import org.springframework.core.ResolvableType; import org.springframework.web.method.HandlerMethod; +import org.springframework.web.reactive.HandlerResult; +import org.springframework.web.reactive.handler.SimpleHandlerResultHandler; /** * @author Sebastien Deleuze diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/handler/SimpleUrlHandlerMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java similarity index 91% rename from spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/handler/SimpleUrlHandlerMappingIntegrationTests.java rename to spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java index 34353f8bd4..2e35c4c0ce 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/handler/SimpleUrlHandlerMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.web.dispatch.handler; +package org.springframework.web.reactive.handler; import java.net.URI; import java.nio.charset.Charset; @@ -31,10 +31,9 @@ import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; import org.springframework.http.server.ReactiveServerHttpRequest; import org.springframework.http.server.ReactiveServerHttpResponse; -import org.springframework.reactive.web.dispatch.DispatcherHandler; -import org.springframework.reactive.web.dispatch.SimpleHandlerResultHandler; -import org.springframework.reactive.web.http.AbstractHttpHandlerIntegrationTests; -import org.springframework.reactive.web.http.HttpHandler; +import org.springframework.web.reactive.DispatcherHandler; +import org.springframework.web.reactive.AbstractHttpHandlerIntegrationTests; +import org.springframework.web.reactive.HttpHandler; import org.springframework.web.client.RestTemplate; import org.springframework.web.context.support.StaticWebApplicationContext; diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/XmlHandler.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/XmlHandler.java similarity index 96% rename from spring-web-reactive/src/test/java/org/springframework/reactive/web/http/XmlHandler.java rename to spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/XmlHandler.java index 490cbd4c1b..382759767c 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/XmlHandler.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/XmlHandler.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.web.http; +package org.springframework.web.reactive.handler; import javax.xml.bind.JAXBContext; import javax.xml.bind.Marshaller; @@ -31,6 +31,7 @@ import org.springframework.http.server.ReactiveServerHttpRequest; import org.springframework.http.server.ReactiveServerHttpResponse; import org.springframework.reactive.io.BufferOutputStream; import org.springframework.reactive.io.ByteBufferPublisherInputStream; +import org.springframework.web.reactive.HttpHandler; import static org.junit.Assert.fail; diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/XmlHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/XmlHandlerIntegrationTests.java similarity index 91% rename from spring-web-reactive/src/test/java/org/springframework/reactive/web/http/XmlHandlerIntegrationTests.java rename to spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/XmlHandlerIntegrationTests.java index 004cf6fc4a..4dc9347f0d 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/XmlHandlerIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/XmlHandlerIntegrationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.web.http; +package org.springframework.web.reactive.handler; import java.net.URI; import javax.xml.bind.annotation.XmlRootElement; @@ -24,6 +24,8 @@ import org.junit.Test; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; import org.springframework.web.client.RestTemplate; +import org.springframework.web.reactive.AbstractHttpHandlerIntegrationTests; +import org.springframework.web.reactive.HttpHandler; /** * @author Arjen Poutsma diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerMappingTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java similarity index 96% rename from spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerMappingTests.java rename to spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java index 9ff435f2b6..c97ac45701 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerMappingTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.web.dispatch.method.annotation; +package org.springframework.web.reactive.method.annotation; import java.net.URI; import java.net.URISyntaxException; @@ -32,6 +32,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.context.support.StaticWebApplicationContext; import org.springframework.web.method.HandlerMethod; +import org.springframework.web.reactive.method.annotation.RequestMappingHandlerMapping; import static org.junit.Assert.assertEquals; diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java similarity index 97% rename from spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingIntegrationTests.java rename to spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java index 180aad81cd..a1887e0390 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.web.dispatch.method.annotation; +package org.springframework.web.reactive.method.annotation; import java.net.URI; @@ -51,10 +51,10 @@ import org.springframework.reactive.codec.encoder.ByteBufferEncoder; import org.springframework.reactive.codec.encoder.JacksonJsonEncoder; import org.springframework.reactive.codec.encoder.JsonObjectEncoder; import org.springframework.reactive.codec.encoder.StringEncoder; -import org.springframework.reactive.web.dispatch.DispatcherHandler; -import org.springframework.reactive.web.dispatch.SimpleHandlerResultHandler; -import org.springframework.reactive.web.http.AbstractHttpHandlerIntegrationTests; -import org.springframework.reactive.web.http.HttpHandler; +import org.springframework.web.reactive.DispatcherHandler; +import org.springframework.web.reactive.handler.SimpleHandlerResultHandler; +import org.springframework.web.reactive.AbstractHttpHandlerIntegrationTests; +import org.springframework.web.reactive.HttpHandler; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandlerTests.java similarity index 92% rename from spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandlerTests.java rename to spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandlerTests.java index 7bab9e20ca..c8212a0301 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandlerTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.web.dispatch.method.annotation; +package org.springframework.web.reactive.method.annotation; import java.util.Collections; @@ -24,9 +24,10 @@ import org.reactivestreams.Publisher; import org.springframework.core.ResolvableType; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.reactive.codec.encoder.StringEncoder; -import org.springframework.reactive.web.dispatch.HandlerResult; +import org.springframework.web.reactive.HandlerResult; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.method.HandlerMethod; +import org.springframework.web.reactive.method.annotation.ResponseBodyResultHandler; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/servlet/AsyncContextSynchronizerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/server/servlet/AsyncContextSynchronizerTests.java similarity index 90% rename from spring-web-reactive/src/test/java/org/springframework/reactive/web/http/servlet/AsyncContextSynchronizerTests.java rename to spring-web-reactive/src/test/java/org/springframework/web/reactive/server/servlet/AsyncContextSynchronizerTests.java index 5dc40c306a..f9897fdb5d 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/http/servlet/AsyncContextSynchronizerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/server/servlet/AsyncContextSynchronizerTests.java @@ -14,13 +14,15 @@ * limitations under the License. */ -package org.springframework.reactive.web.http.servlet; +package org.springframework.web.reactive.server.servlet; import javax.servlet.AsyncContext; import org.junit.Before; import org.junit.Test; +import org.springframework.web.reactive.server.servlet.AsyncContextSynchronizer; + import static org.mockito.BDDMockito.mock; import static org.mockito.BDDMockito.verify; From e9e4bcdc599634ae8879815a33a8b4e41b909f61 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 20 Nov 2015 13:10:44 -0500 Subject: [PATCH 092/498] Move ~.web.reactive.server to ~.http.server --- .../server/ReactiveHttpHandler.java} | 4 ++-- .../PublisherReactorServerHttpRequest.java | 2 +- .../PublisherReactorServerHttpResponse.java | 2 +- .../reactor/ReactorHttpHandlerAdapter.java} | 10 +++++----- .../reactor/ReactorServerHttpRequest.java | 2 +- .../reactor/ReactorServerHttpResponse.java | 2 +- .../rxnetty/RxNettyHttpHandlerAdapter.java} | 10 +++++----- .../rxnetty/RxNettyServerHttpRequest.java | 2 +- .../rxnetty/RxNettyServerHttpResponse.java | 2 +- .../servlet31}/AsyncContextSynchronizer.java | 2 +- .../servlet31}/RequestBodyPublisher.java | 2 +- .../servlet31}/ResponseBodySubscriber.java | 2 +- .../Servlet31HttpHandlerAdapter.java} | 20 +++++++++---------- .../Servlet31ServerHttpRequest.java} | 6 +++--- .../Servlet31ServerHttpResponse.java} | 6 +++--- .../server/undertow/RequestBodyPublisher.java | 2 +- .../undertow/ResponseBodySubscriber.java | 2 +- .../undertow/UndertowHttpHandlerAdapter.java | 8 ++++---- .../undertow/UndertowServerHttpRequest.java | 2 +- .../undertow/UndertowServerHttpResponse.java | 2 +- .../web/reactive/DispatcherHandler.java | 3 ++- .../reactive/handler/HttpHandlerAdapter.java | 8 ++++---- .../AbstractHttpHandlerIntegrationTests.java | 11 ++-------- .../handler => http/server}/EchoHandler.java | 6 +++--- .../server}/EchoHandlerIntegrationTests.java | 3 +-- .../reactive => http/server}/HttpServer.java | 6 +++--- .../server}/HttpServerSupport.java | 10 +++++----- .../server}/JettyHttpServer.java | 6 +++--- .../server}/RandomHandler.java | 8 ++------ .../RandomHandlerIntegrationTests.java | 3 +-- .../server}/ReactorHttpServer.java | 8 ++++---- .../server}/RxNettyHttpServer.java | 8 ++++---- .../server}/TomcatHttpServer.java | 6 +++--- .../server}/UndertowHttpServer.java | 4 ++-- .../handler => http/server}/XmlHandler.java | 7 ++----- .../server}/XmlHandlerIntegrationTests.java | 6 ++---- .../AsyncContextSynchronizerTests.java | 4 ++-- ...mpleUrlHandlerMappingIntegrationTests.java | 10 +++++----- .../RequestMappingIntegrationTests.java | 6 +++--- 39 files changed, 98 insertions(+), 115 deletions(-) rename spring-web-reactive/src/main/java/org/springframework/{web/reactive/HttpHandler.java => http/server/ReactiveHttpHandler.java} (96%) rename spring-web-reactive/src/main/java/org/springframework/{web/reactive => http}/server/reactor/PublisherReactorServerHttpRequest.java (97%) rename spring-web-reactive/src/main/java/org/springframework/{web/reactive => http}/server/reactor/PublisherReactorServerHttpResponse.java (97%) rename spring-web-reactive/src/main/java/org/springframework/{web/reactive/server/reactor/RequestHandlerAdapter.java => http/server/reactor/ReactorHttpHandlerAdapter.java} (86%) rename spring-web-reactive/src/main/java/org/springframework/{web/reactive => http}/server/reactor/ReactorServerHttpRequest.java (95%) rename spring-web-reactive/src/main/java/org/springframework/{web/reactive => http}/server/reactor/ReactorServerHttpResponse.java (95%) rename spring-web-reactive/src/main/java/org/springframework/{web/reactive/server/rxnetty/RequestHandlerAdapter.java => http/server/rxnetty/RxNettyHttpHandlerAdapter.java} (83%) rename spring-web-reactive/src/main/java/org/springframework/{web/reactive => http}/server/rxnetty/RxNettyServerHttpRequest.java (97%) rename spring-web-reactive/src/main/java/org/springframework/{web/reactive => http}/server/rxnetty/RxNettyServerHttpResponse.java (97%) rename spring-web-reactive/src/main/java/org/springframework/{web/reactive/server/servlet => http/server/servlet31}/AsyncContextSynchronizer.java (98%) rename spring-web-reactive/src/main/java/org/springframework/{web/reactive/server/servlet => http/server/servlet31}/RequestBodyPublisher.java (98%) rename spring-web-reactive/src/main/java/org/springframework/{web/reactive/server/servlet => http/server/servlet31}/ResponseBodySubscriber.java (97%) rename spring-web-reactive/src/main/java/org/springframework/{web/reactive/server/servlet/HttpHandlerServlet.java => http/server/servlet31/Servlet31HttpHandlerAdapter.java} (81%) rename spring-web-reactive/src/main/java/org/springframework/{web/reactive/server/servlet/ServletServerHttpRequest.java => http/server/servlet31/Servlet31ServerHttpRequest.java} (95%) rename spring-web-reactive/src/main/java/org/springframework/{web/reactive/server/servlet/ServletServerHttpResponse.java => http/server/servlet31/Servlet31ServerHttpResponse.java} (93%) rename spring-web-reactive/src/main/java/org/springframework/{web/reactive => http}/server/undertow/RequestBodyPublisher.java (98%) rename spring-web-reactive/src/main/java/org/springframework/{web/reactive => http}/server/undertow/ResponseBodySubscriber.java (98%) rename spring-web-reactive/src/main/java/org/springframework/{web/reactive => http}/server/undertow/UndertowHttpHandlerAdapter.java (91%) rename spring-web-reactive/src/main/java/org/springframework/{web/reactive => http}/server/undertow/UndertowServerHttpRequest.java (97%) rename spring-web-reactive/src/main/java/org/springframework/{web/reactive => http}/server/undertow/UndertowServerHttpResponse.java (97%) rename spring-web-reactive/src/test/java/org/springframework/{web/reactive => http/server}/AbstractHttpHandlerIntegrationTests.java (76%) rename spring-web-reactive/src/test/java/org/springframework/{web/reactive/handler => http/server}/EchoHandler.java (86%) rename spring-web-reactive/src/test/java/org/springframework/{web/reactive/handler => http/server}/EchoHandlerIntegrationTests.java (95%) rename spring-web-reactive/src/test/java/org/springframework/{web/reactive => http/server}/HttpServer.java (85%) rename spring-web-reactive/src/test/java/org/springframework/{web/reactive => http/server}/HttpServerSupport.java (78%) rename spring-web-reactive/src/test/java/org/springframework/{web/reactive => http/server}/JettyHttpServer.java (92%) rename spring-web-reactive/src/test/java/org/springframework/{web/reactive/handler => http/server}/RandomHandler.java (88%) rename spring-web-reactive/src/test/java/org/springframework/{web/reactive/handler => http/server}/RandomHandlerIntegrationTests.java (93%) rename spring-web-reactive/src/test/java/org/springframework/{web/reactive => http/server}/ReactorHttpServer.java (87%) rename spring-web-reactive/src/test/java/org/springframework/{web/reactive => http/server}/RxNettyHttpServer.java (87%) rename spring-web-reactive/src/test/java/org/springframework/{web/reactive => http/server}/TomcatHttpServer.java (91%) rename spring-web-reactive/src/test/java/org/springframework/{web/reactive => http/server}/UndertowHttpServer.java (92%) rename spring-web-reactive/src/test/java/org/springframework/{web/reactive/handler => http/server}/XmlHandler.java (89%) rename spring-web-reactive/src/test/java/org/springframework/{web/reactive/handler => http/server}/XmlHandlerIntegrationTests.java (89%) rename spring-web-reactive/src/test/java/org/springframework/{web/reactive/server/servlet => http/server/servlet31}/AsyncContextSynchronizerTests.java (90%) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HttpHandler.java b/spring-web-reactive/src/main/java/org/springframework/http/server/ReactiveHttpHandler.java similarity index 96% rename from spring-web-reactive/src/main/java/org/springframework/web/reactive/HttpHandler.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/ReactiveHttpHandler.java index 9f97ce023d..fc683451dd 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HttpHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/ReactiveHttpHandler.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.web.reactive; +package org.springframework.http.server; import org.reactivestreams.Publisher; @@ -33,7 +33,7 @@ import org.springframework.http.server.ReactiveServerHttpResponse; * @see ReactiveServerHttpRequest#getBody() * @see ReactiveServerHttpResponse#setBody(Publisher) */ -public interface HttpHandler { +public interface ReactiveHttpHandler { /** * Process the given request, generating a response in an asynchronous non blocking way. diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/reactor/PublisherReactorServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactor/PublisherReactorServerHttpRequest.java similarity index 97% rename from spring-web-reactive/src/main/java/org/springframework/web/reactive/server/reactor/PublisherReactorServerHttpRequest.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/reactor/PublisherReactorServerHttpRequest.java index d0c319216c..16d883089f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/reactor/PublisherReactorServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactor/PublisherReactorServerHttpRequest.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.web.reactive.server.reactor; +package org.springframework.http.server.reactor; import java.net.URI; import java.net.URISyntaxException; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/reactor/PublisherReactorServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactor/PublisherReactorServerHttpResponse.java similarity index 97% rename from spring-web-reactive/src/main/java/org/springframework/web/reactive/server/reactor/PublisherReactorServerHttpResponse.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/reactor/PublisherReactorServerHttpResponse.java index 78a820a2da..ad7193f528 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/reactor/PublisherReactorServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactor/PublisherReactorServerHttpResponse.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.web.reactive.server.reactor; +package org.springframework.http.server.reactor; import java.nio.ByteBuffer; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/reactor/RequestHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactor/ReactorHttpHandlerAdapter.java similarity index 86% rename from spring-web-reactive/src/main/java/org/springframework/web/reactive/server/reactor/RequestHandlerAdapter.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/reactor/ReactorHttpHandlerAdapter.java index 1b70426bbb..e85564f92e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/reactor/RequestHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactor/ReactorHttpHandlerAdapter.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.web.reactive.server.reactor; +package org.springframework.http.server.reactor; import org.reactivestreams.Publisher; import reactor.core.publisher.convert.DependencyUtils; @@ -21,19 +21,19 @@ import reactor.io.buffer.Buffer; import reactor.io.net.ReactiveChannelHandler; import reactor.io.net.http.HttpChannel; -import org.springframework.web.reactive.HttpHandler; +import org.springframework.http.server.ReactiveHttpHandler; import org.springframework.util.Assert; /** * @author Stephane Maldini */ -public class RequestHandlerAdapter +public class ReactorHttpHandlerAdapter implements ReactiveChannelHandler> { - private final HttpHandler httpHandler; + private final ReactiveHttpHandler httpHandler; - public RequestHandlerAdapter(HttpHandler httpHandler) { + public ReactorHttpHandlerAdapter(ReactiveHttpHandler httpHandler) { Assert.notNull(httpHandler, "'httpHandler' is required."); this.httpHandler = httpHandler; } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/reactor/ReactorServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactor/ReactorServerHttpRequest.java similarity index 95% rename from spring-web-reactive/src/main/java/org/springframework/web/reactive/server/reactor/ReactorServerHttpRequest.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/reactor/ReactorServerHttpRequest.java index ad6913d333..71fff0002b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/reactor/ReactorServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactor/ReactorServerHttpRequest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.web.reactive.server.reactor; +package org.springframework.http.server.reactor; import java.nio.ByteBuffer; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/reactor/ReactorServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactor/ReactorServerHttpResponse.java similarity index 95% rename from spring-web-reactive/src/main/java/org/springframework/web/reactive/server/reactor/ReactorServerHttpResponse.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/reactor/ReactorServerHttpResponse.java index fdf8c4bf3e..ece8e23cd2 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/reactor/ReactorServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactor/ReactorServerHttpResponse.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.web.reactive.server.reactor; +package org.springframework.http.server.reactor; import java.nio.ByteBuffer; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/rxnetty/RequestHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/rxnetty/RxNettyHttpHandlerAdapter.java similarity index 83% rename from spring-web-reactive/src/main/java/org/springframework/web/reactive/server/rxnetty/RequestHandlerAdapter.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/rxnetty/RxNettyHttpHandlerAdapter.java index 0448e99be4..3d6dde0e48 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/rxnetty/RequestHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/rxnetty/RxNettyHttpHandlerAdapter.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.web.reactive.server.rxnetty; +package org.springframework.http.server.rxnetty; import io.netty.buffer.ByteBuf; import io.reactivex.netty.protocol.http.server.HttpServerRequest; @@ -24,18 +24,18 @@ import org.reactivestreams.Publisher; import reactor.core.publisher.convert.RxJava1Converter; import rx.Observable; -import org.springframework.web.reactive.HttpHandler; +import org.springframework.http.server.ReactiveHttpHandler; import org.springframework.util.Assert; /** * @author Rossen Stoyanchev */ -public class RequestHandlerAdapter implements RequestHandler { +public class RxNettyHttpHandlerAdapter implements RequestHandler { - private final HttpHandler httpHandler; + private final ReactiveHttpHandler httpHandler; - public RequestHandlerAdapter(HttpHandler httpHandler) { + public RxNettyHttpHandlerAdapter(ReactiveHttpHandler httpHandler) { Assert.notNull(httpHandler, "'httpHandler' is required."); this.httpHandler = httpHandler; } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/rxnetty/RxNettyServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/rxnetty/RxNettyServerHttpRequest.java similarity index 97% rename from spring-web-reactive/src/main/java/org/springframework/web/reactive/server/rxnetty/RxNettyServerHttpRequest.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/rxnetty/RxNettyServerHttpRequest.java index 48be0c222d..d69683ca2c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/rxnetty/RxNettyServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/rxnetty/RxNettyServerHttpRequest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.web.reactive.server.rxnetty; +package org.springframework.http.server.rxnetty; import java.net.URI; import java.net.URISyntaxException; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/rxnetty/RxNettyServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/rxnetty/RxNettyServerHttpResponse.java similarity index 97% rename from spring-web-reactive/src/main/java/org/springframework/web/reactive/server/rxnetty/RxNettyServerHttpResponse.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/rxnetty/RxNettyServerHttpResponse.java index 9a51590e52..d301799408 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/rxnetty/RxNettyServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/rxnetty/RxNettyServerHttpResponse.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.web.reactive.server.rxnetty; +package org.springframework.http.server.rxnetty; import java.nio.ByteBuffer; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/servlet/AsyncContextSynchronizer.java b/spring-web-reactive/src/main/java/org/springframework/http/server/servlet31/AsyncContextSynchronizer.java similarity index 98% rename from spring-web-reactive/src/main/java/org/springframework/web/reactive/server/servlet/AsyncContextSynchronizer.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/servlet31/AsyncContextSynchronizer.java index 8bb6d36004..3900fbfbb5 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/servlet/AsyncContextSynchronizer.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/servlet31/AsyncContextSynchronizer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.web.reactive.server.servlet; +package org.springframework.http.server.servlet31; import java.io.IOException; import java.util.concurrent.atomic.AtomicInteger; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/servlet/RequestBodyPublisher.java b/spring-web-reactive/src/main/java/org/springframework/http/server/servlet31/RequestBodyPublisher.java similarity index 98% rename from spring-web-reactive/src/main/java/org/springframework/web/reactive/server/servlet/RequestBodyPublisher.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/servlet31/RequestBodyPublisher.java index e22560d325..f79d4d9876 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/servlet/RequestBodyPublisher.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/servlet31/RequestBodyPublisher.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.web.reactive.server.servlet; +package org.springframework.http.server.servlet31; import java.io.IOException; import java.nio.ByteBuffer; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/servlet/ResponseBodySubscriber.java b/spring-web-reactive/src/main/java/org/springframework/http/server/servlet31/ResponseBodySubscriber.java similarity index 97% rename from spring-web-reactive/src/main/java/org/springframework/web/reactive/server/servlet/ResponseBodySubscriber.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/servlet31/ResponseBodySubscriber.java index 028e4aa4ba..75b49a2866 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/servlet/ResponseBodySubscriber.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/servlet31/ResponseBodySubscriber.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.web.reactive.server.servlet; +package org.springframework.http.server.servlet31; import java.io.IOException; import java.nio.ByteBuffer; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/servlet/HttpHandlerServlet.java b/spring-web-reactive/src/main/java/org/springframework/http/server/servlet31/Servlet31HttpHandlerAdapter.java similarity index 81% rename from spring-web-reactive/src/main/java/org/springframework/web/reactive/server/servlet/HttpHandlerServlet.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/servlet31/Servlet31HttpHandlerAdapter.java index 0b91603bec..4766809261 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/servlet/HttpHandlerServlet.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/servlet31/Servlet31HttpHandlerAdapter.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.web.reactive.server.servlet; +package org.springframework.http.server.servlet31; import java.io.IOException; import javax.servlet.AsyncContext; @@ -30,24 +30,24 @@ import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import org.springframework.http.HttpStatus; -import org.springframework.web.reactive.HttpHandler; +import org.springframework.http.server.ReactiveHttpHandler; /** * @author Arjen Poutsma * @author Rossen Stoyanchev */ @WebServlet(asyncSupported = true) -public class HttpHandlerServlet extends HttpServlet { +public class Servlet31HttpHandlerAdapter extends HttpServlet { private static final int BUFFER_SIZE = 8192; - private static Log logger = LogFactory.getLog(HttpHandlerServlet.class); + private static Log logger = LogFactory.getLog(Servlet31HttpHandlerAdapter.class); - private HttpHandler handler; + private ReactiveHttpHandler handler; - public void setHandler(HttpHandler handler) { + public void setHandler(ReactiveHttpHandler handler) { this.handler = handler; } @@ -61,11 +61,11 @@ public class HttpHandlerServlet extends HttpServlet { RequestBodyPublisher requestPublisher = new RequestBodyPublisher(synchronizer, BUFFER_SIZE); request.getInputStream().setReadListener(requestPublisher); - ServletServerHttpRequest httpRequest = new ServletServerHttpRequest(request, requestPublisher); + Servlet31ServerHttpRequest httpRequest = new Servlet31ServerHttpRequest(request, requestPublisher); ResponseBodySubscriber responseSubscriber = new ResponseBodySubscriber(synchronizer); response.getOutputStream().setWriteListener(responseSubscriber); - ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response, responseSubscriber); + Servlet31ServerHttpResponse httpResponse = new Servlet31ServerHttpResponse(response, responseSubscriber); HandlerResultSubscriber resultSubscriber = new HandlerResultSubscriber(synchronizer, httpResponse); this.handler.handle(httpRequest, httpResponse).subscribe(resultSubscriber); @@ -76,11 +76,11 @@ public class HttpHandlerServlet extends HttpServlet { private final AsyncContextSynchronizer synchronizer; - private final ServletServerHttpResponse response; + private final Servlet31ServerHttpResponse response; public HandlerResultSubscriber(AsyncContextSynchronizer synchronizer, - ServletServerHttpResponse response) { + Servlet31ServerHttpResponse response) { this.synchronizer = synchronizer; this.response = response; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/servlet/ServletServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/servlet31/Servlet31ServerHttpRequest.java similarity index 95% rename from spring-web-reactive/src/main/java/org/springframework/web/reactive/server/servlet/ServletServerHttpRequest.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/servlet31/Servlet31ServerHttpRequest.java index 407474a4e8..5ad43ed444 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/servlet/ServletServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/servlet31/Servlet31ServerHttpRequest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.web.reactive.server.servlet; +package org.springframework.http.server.servlet31; import java.net.URI; import java.net.URISyntaxException; @@ -37,7 +37,7 @@ import org.springframework.util.StringUtils; /** * @author Rossen Stoyanchev */ -public class ServletServerHttpRequest implements ReactiveServerHttpRequest { +public class Servlet31ServerHttpRequest implements ReactiveServerHttpRequest { private final HttpServletRequest servletRequest; @@ -46,7 +46,7 @@ public class ServletServerHttpRequest implements ReactiveServerHttpRequest { private HttpHeaders headers; - public ServletServerHttpRequest(HttpServletRequest servletRequest, + public Servlet31ServerHttpRequest(HttpServletRequest servletRequest, Publisher requestBodyPublisher) { Assert.notNull(servletRequest, "HttpServletRequest must not be null"); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/servlet/ServletServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/servlet31/Servlet31ServerHttpResponse.java similarity index 93% rename from spring-web-reactive/src/main/java/org/springframework/web/reactive/server/servlet/ServletServerHttpResponse.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/servlet31/Servlet31ServerHttpResponse.java index 25709e4000..dde179cd47 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/servlet/ServletServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/servlet31/Servlet31ServerHttpResponse.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.web.reactive.server.servlet; +package org.springframework.http.server.servlet31; import java.nio.ByteBuffer; import java.nio.charset.Charset; @@ -34,7 +34,7 @@ import org.springframework.util.Assert; /** * @author Rossen Stoyanchev */ -public class ServletServerHttpResponse implements ReactiveServerHttpResponse { +public class Servlet31ServerHttpResponse implements ReactiveServerHttpResponse { private final HttpServletResponse response; @@ -45,7 +45,7 @@ public class ServletServerHttpResponse implements ReactiveServerHttpResponse { private boolean headersWritten = false; - public ServletServerHttpResponse(HttpServletResponse response, + public Servlet31ServerHttpResponse(HttpServletResponse response, ResponseBodySubscriber subscriber) { Assert.notNull(response, "'response' must not be null"); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/undertow/RequestBodyPublisher.java b/spring-web-reactive/src/main/java/org/springframework/http/server/undertow/RequestBodyPublisher.java similarity index 98% rename from spring-web-reactive/src/main/java/org/springframework/web/reactive/server/undertow/RequestBodyPublisher.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/undertow/RequestBodyPublisher.java index 5c1b567b20..6234e90642 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/undertow/RequestBodyPublisher.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/undertow/RequestBodyPublisher.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.web.reactive.server.undertow; +package org.springframework.http.server.undertow; import static org.xnio.IoUtils.safeClose; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/undertow/ResponseBodySubscriber.java b/spring-web-reactive/src/main/java/org/springframework/http/server/undertow/ResponseBodySubscriber.java similarity index 98% rename from spring-web-reactive/src/main/java/org/springframework/web/reactive/server/undertow/ResponseBodySubscriber.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/undertow/ResponseBodySubscriber.java index d362085095..b809e439ed 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/undertow/ResponseBodySubscriber.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/undertow/ResponseBodySubscriber.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.web.reactive.server.undertow; +package org.springframework.http.server.undertow; import java.io.IOException; import java.nio.ByteBuffer; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/undertow/UndertowHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/undertow/UndertowHttpHandlerAdapter.java similarity index 91% rename from spring-web-reactive/src/main/java/org/springframework/web/reactive/server/undertow/UndertowHttpHandlerAdapter.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/undertow/UndertowHttpHandlerAdapter.java index 365aed0054..040e208765 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/undertow/UndertowHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/undertow/UndertowHttpHandlerAdapter.java @@ -14,11 +14,11 @@ * limitations under the License. */ -package org.springframework.web.reactive.server.undertow; +package org.springframework.http.server.undertow; import org.springframework.http.server.ReactiveServerHttpRequest; import org.springframework.http.server.ReactiveServerHttpResponse; -import org.springframework.web.reactive.HttpHandler; +import org.springframework.http.server.ReactiveHttpHandler; import org.springframework.util.Assert; import io.undertow.server.HttpServerExchange; @@ -37,10 +37,10 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle private static Log logger = LogFactory.getLog(UndertowHttpHandlerAdapter.class); - private final HttpHandler delegate; + private final ReactiveHttpHandler delegate; - public UndertowHttpHandlerAdapter(HttpHandler delegate) { + public UndertowHttpHandlerAdapter(ReactiveHttpHandler delegate) { Assert.notNull(delegate, "'delegate' is required."); this.delegate = delegate; } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/undertow/UndertowServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/undertow/UndertowServerHttpRequest.java similarity index 97% rename from spring-web-reactive/src/main/java/org/springframework/web/reactive/server/undertow/UndertowServerHttpRequest.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/undertow/UndertowServerHttpRequest.java index 4d68be46ba..25094d3f5f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/undertow/UndertowServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/undertow/UndertowServerHttpRequest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.web.reactive.server.undertow; +package org.springframework.http.server.undertow; import java.net.URI; import java.net.URISyntaxException; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/undertow/UndertowServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/undertow/UndertowServerHttpResponse.java similarity index 97% rename from spring-web-reactive/src/main/java/org/springframework/web/reactive/server/undertow/UndertowServerHttpResponse.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/undertow/UndertowServerHttpResponse.java index 0897492291..506b2ff181 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/server/undertow/UndertowServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/undertow/UndertowServerHttpResponse.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.web.reactive.server.undertow; +package org.springframework.http.server.undertow; import java.nio.ByteBuffer; import java.util.List; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java index eabcfcf16d..48a1b180b8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java @@ -31,6 +31,7 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.http.HttpStatus; +import org.springframework.http.server.ReactiveHttpHandler; import org.springframework.http.server.ReactiveServerHttpRequest; import org.springframework.http.server.ReactiveServerHttpResponse; @@ -52,7 +53,7 @@ import org.springframework.http.server.ReactiveServerHttpResponse; * @author Rossen Stoyanchev * @author Sebastien Deleuze */ -public class DispatcherHandler implements HttpHandler, ApplicationContextAware { +public class DispatcherHandler implements ReactiveHttpHandler, ApplicationContextAware { private static final Log logger = LogFactory.getLog(DispatcherHandler.class); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/HttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/HttpHandlerAdapter.java index 22f3375203..7d98704956 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/HttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/HttpHandlerAdapter.java @@ -24,11 +24,11 @@ import org.springframework.http.server.ReactiveServerHttpRequest; import org.springframework.http.server.ReactiveServerHttpResponse; import org.springframework.web.reactive.HandlerAdapter; import org.springframework.web.reactive.HandlerResult; -import org.springframework.web.reactive.HttpHandler; +import org.springframework.http.server.ReactiveHttpHandler; import org.springframework.web.reactive.DispatcherHandler; /** - * Support use of {@link HttpHandler} with + * Support use of {@link ReactiveHttpHandler} with * {@link DispatcherHandler * DispatcherHandler} (which implements the same contract). * The use of {@code DispatcherHandler} this way enables routing requests to @@ -46,14 +46,14 @@ public class HttpHandlerAdapter implements HandlerAdapter { @Override public boolean supports(Object handler) { - return HttpHandler.class.isAssignableFrom(handler.getClass()); + return ReactiveHttpHandler.class.isAssignableFrom(handler.getClass()); } @Override public Publisher handle(ReactiveServerHttpRequest request, ReactiveServerHttpResponse response, Object handler) { - HttpHandler httpHandler = (HttpHandler)handler; + ReactiveHttpHandler httpHandler = (ReactiveHttpHandler)handler; Publisher completion = httpHandler.handle(request, response); return Publishers.just(new HandlerResult(httpHandler, completion, PUBLISHER_VOID)); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/AbstractHttpHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/AbstractHttpHandlerIntegrationTests.java similarity index 76% rename from spring-web-reactive/src/test/java/org/springframework/web/reactive/AbstractHttpHandlerIntegrationTests.java rename to spring-web-reactive/src/test/java/org/springframework/http/server/AbstractHttpHandlerIntegrationTests.java index 96b694bdac..5a53e0b496 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/AbstractHttpHandlerIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/AbstractHttpHandlerIntegrationTests.java @@ -14,21 +14,14 @@ * limitations under the License. */ -package org.springframework.web.reactive; +package org.springframework.http.server; import org.junit.After; import org.junit.Before; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; -import org.springframework.web.reactive.HttpServer; -import org.springframework.web.reactive.ReactorHttpServer; -import org.springframework.web.reactive.RxNettyHttpServer; -import org.springframework.web.reactive.JettyHttpServer; -import org.springframework.web.reactive.TomcatHttpServer; -import org.springframework.web.reactive.UndertowHttpServer; import org.springframework.util.SocketUtils; -import org.springframework.web.reactive.HttpHandler; @RunWith(Parameterized.class) @@ -61,7 +54,7 @@ public abstract class AbstractHttpHandlerIntegrationTests { this.server.start(); } - protected abstract HttpHandler createHttpHandler(); + protected abstract ReactiveHttpHandler createHttpHandler(); @After public void tearDown() throws Exception { diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/EchoHandler.java b/spring-web-reactive/src/test/java/org/springframework/http/server/EchoHandler.java similarity index 86% rename from spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/EchoHandler.java rename to spring-web-reactive/src/test/java/org/springframework/http/server/EchoHandler.java index a3013c131a..bf3874b360 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/EchoHandler.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/EchoHandler.java @@ -14,18 +14,18 @@ * limitations under the License. */ -package org.springframework.web.reactive.handler; +package org.springframework.http.server; import org.reactivestreams.Publisher; import org.springframework.http.server.ReactiveServerHttpRequest; import org.springframework.http.server.ReactiveServerHttpResponse; -import org.springframework.web.reactive.HttpHandler; +import org.springframework.http.server.ReactiveHttpHandler; /** * @author Arjen Poutsma */ -public class EchoHandler implements HttpHandler { +public class EchoHandler implements ReactiveHttpHandler { @Override public Publisher handle(ReactiveServerHttpRequest request, ReactiveServerHttpResponse response) { diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/EchoHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/EchoHandlerIntegrationTests.java similarity index 95% rename from spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/EchoHandlerIntegrationTests.java rename to spring-web-reactive/src/test/java/org/springframework/http/server/EchoHandlerIntegrationTests.java index 811d53f812..8d4b114843 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/EchoHandlerIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/EchoHandlerIntegrationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.web.reactive.handler; +package org.springframework.http.server; import java.net.URI; import java.util.Random; @@ -24,7 +24,6 @@ import org.junit.Test; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; import org.springframework.web.client.RestTemplate; -import org.springframework.web.reactive.AbstractHttpHandlerIntegrationTests; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/HttpServer.java b/spring-web-reactive/src/test/java/org/springframework/http/server/HttpServer.java similarity index 85% rename from spring-web-reactive/src/test/java/org/springframework/web/reactive/HttpServer.java rename to spring-web-reactive/src/test/java/org/springframework/http/server/HttpServer.java index f869d33b04..695609328f 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/HttpServer.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/HttpServer.java @@ -14,12 +14,12 @@ * limitations under the License. */ -package org.springframework.web.reactive; +package org.springframework.http.server; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.Lifecycle; -import org.springframework.web.reactive.HttpHandler; +import org.springframework.http.server.ReactiveHttpHandler; /** * @author Rossen Stoyanchev @@ -28,6 +28,6 @@ public interface HttpServer extends InitializingBean, Lifecycle { void setPort(int port); - void setHandler(HttpHandler handler); + void setHandler(ReactiveHttpHandler handler); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/HttpServerSupport.java b/spring-web-reactive/src/test/java/org/springframework/http/server/HttpServerSupport.java similarity index 78% rename from spring-web-reactive/src/test/java/org/springframework/web/reactive/HttpServerSupport.java rename to spring-web-reactive/src/test/java/org/springframework/http/server/HttpServerSupport.java index 71704f408e..8c8358d872 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/HttpServerSupport.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/HttpServerSupport.java @@ -14,10 +14,10 @@ * limitations under the License. */ -package org.springframework.web.reactive; +package org.springframework.http.server; -import org.springframework.web.reactive.HttpHandler; +import org.springframework.http.server.ReactiveHttpHandler; /** * @author Rossen Stoyanchev @@ -26,7 +26,7 @@ public class HttpServerSupport { private int port = -1; - private HttpHandler httpHandler; + private ReactiveHttpHandler httpHandler; public void setPort(int port) { @@ -37,11 +37,11 @@ public class HttpServerSupport { return this.port; } - public void setHandler(HttpHandler handler) { + public void setHandler(ReactiveHttpHandler handler) { this.httpHandler = handler; } - public HttpHandler getHttpHandler() { + public ReactiveHttpHandler getHttpHandler() { return this.httpHandler; } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/JettyHttpServer.java b/spring-web-reactive/src/test/java/org/springframework/http/server/JettyHttpServer.java similarity index 92% rename from spring-web-reactive/src/test/java/org/springframework/web/reactive/JettyHttpServer.java rename to spring-web-reactive/src/test/java/org/springframework/http/server/JettyHttpServer.java index a7f5ec7524..f34221bcd3 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/JettyHttpServer.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/JettyHttpServer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.web.reactive; +package org.springframework.http.server; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; @@ -24,7 +24,7 @@ import org.eclipse.jetty.servlet.ServletHolder; import org.springframework.beans.factory.InitializingBean; import org.springframework.util.Assert; import org.springframework.util.SocketUtils; -import org.springframework.web.reactive.server.servlet.HttpHandlerServlet; +import org.springframework.http.server.servlet31.Servlet31HttpHandlerAdapter; /** * @author Rossen Stoyanchev @@ -51,7 +51,7 @@ public class JettyHttpServer extends HttpServerSupport implements InitializingBe this.jettyServer = new Server(); Assert.notNull(getHttpHandler()); - HttpHandlerServlet servlet = new HttpHandlerServlet(); + Servlet31HttpHandlerAdapter servlet = new Servlet31HttpHandlerAdapter(); servlet.setHandler(getHttpHandler()); ServletHolder servletHolder = new ServletHolder(servlet); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/RandomHandler.java b/spring-web-reactive/src/test/java/org/springframework/http/server/RandomHandler.java similarity index 88% rename from spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/RandomHandler.java rename to spring-web-reactive/src/test/java/org/springframework/http/server/RandomHandler.java index 53f300686c..cc94febae4 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/RandomHandler.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/RandomHandler.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.web.reactive.handler; +package org.springframework.http.server; import java.nio.ByteBuffer; import java.util.Random; @@ -27,16 +27,12 @@ import org.reactivestreams.Subscription; import reactor.io.buffer.Buffer; import reactor.rx.Streams; -import org.springframework.http.server.ReactiveServerHttpRequest; -import org.springframework.http.server.ReactiveServerHttpResponse; -import org.springframework.web.reactive.HttpHandler; - import static org.junit.Assert.assertEquals; /** * @author Arjen Poutsma */ -public class RandomHandler implements HttpHandler { +public class RandomHandler implements ReactiveHttpHandler { private static final Log logger = LogFactory.getLog(RandomHandler.class); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/RandomHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/RandomHandlerIntegrationTests.java similarity index 93% rename from spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/RandomHandlerIntegrationTests.java rename to spring-web-reactive/src/test/java/org/springframework/http/server/RandomHandlerIntegrationTests.java index 383eb5dfc1..ba3f76a5d0 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/RandomHandlerIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/RandomHandlerIntegrationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.web.reactive.handler; +package org.springframework.http.server; import java.net.URI; import java.util.Random; @@ -24,7 +24,6 @@ import org.junit.Test; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; import org.springframework.web.client.RestTemplate; -import org.springframework.web.reactive.AbstractHttpHandlerIntegrationTests; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/ReactorHttpServer.java b/spring-web-reactive/src/test/java/org/springframework/http/server/ReactorHttpServer.java similarity index 87% rename from spring-web-reactive/src/test/java/org/springframework/web/reactive/ReactorHttpServer.java rename to spring-web-reactive/src/test/java/org/springframework/http/server/ReactorHttpServer.java index a824f85686..30e2edf75e 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/ReactorHttpServer.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/ReactorHttpServer.java @@ -14,14 +14,14 @@ * limitations under the License. */ -package org.springframework.web.reactive; +package org.springframework.http.server; import reactor.io.buffer.Buffer; import reactor.io.net.ReactiveNet; import org.springframework.beans.factory.InitializingBean; import org.springframework.util.Assert; -import org.springframework.web.reactive.server.reactor.RequestHandlerAdapter; +import org.springframework.http.server.reactor.ReactorHttpHandlerAdapter; /** * @author Stephane Maldini @@ -29,7 +29,7 @@ import org.springframework.web.reactive.server.reactor.RequestHandlerAdapter; public class ReactorHttpServer extends HttpServerSupport implements InitializingBean, HttpServer { - private RequestHandlerAdapter reactorHandler; + private ReactorHttpHandlerAdapter reactorHandler; private reactor.io.net.http.HttpServer reactorServer; @@ -44,7 +44,7 @@ public class ReactorHttpServer extends HttpServerSupport public void afterPropertiesSet() throws Exception { Assert.notNull(getHttpHandler()); - this.reactorHandler = new RequestHandlerAdapter(getHttpHandler()); + this.reactorHandler = new ReactorHttpHandlerAdapter(getHttpHandler()); this.reactorServer = (getPort() != -1 ? ReactiveNet.httpServer(getPort()) : ReactiveNet.httpServer()); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/RxNettyHttpServer.java b/spring-web-reactive/src/test/java/org/springframework/http/server/RxNettyHttpServer.java similarity index 87% rename from spring-web-reactive/src/test/java/org/springframework/web/reactive/RxNettyHttpServer.java rename to spring-web-reactive/src/test/java/org/springframework/http/server/RxNettyHttpServer.java index a7cc2fc2a8..44f3f4a228 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/RxNettyHttpServer.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/RxNettyHttpServer.java @@ -14,13 +14,13 @@ * limitations under the License. */ -package org.springframework.web.reactive; +package org.springframework.http.server; import io.netty.buffer.ByteBuf; import org.springframework.beans.factory.InitializingBean; import org.springframework.util.Assert; -import org.springframework.web.reactive.server.rxnetty.RequestHandlerAdapter; +import org.springframework.http.server.rxnetty.RxNettyHttpHandlerAdapter; /** @@ -28,7 +28,7 @@ import org.springframework.web.reactive.server.rxnetty.RequestHandlerAdapter; */ public class RxNettyHttpServer extends HttpServerSupport implements InitializingBean, HttpServer { - private RequestHandlerAdapter rxNettyHandler; + private RxNettyHttpHandlerAdapter rxNettyHandler; private io.reactivex.netty.protocol.http.server.HttpServer rxNettyServer; @@ -45,7 +45,7 @@ public class RxNettyHttpServer extends HttpServerSupport implements Initializing public void afterPropertiesSet() throws Exception { Assert.notNull(getHttpHandler()); - this.rxNettyHandler = new RequestHandlerAdapter(getHttpHandler()); + this.rxNettyHandler = new RxNettyHttpHandlerAdapter(getHttpHandler()); this.rxNettyServer = (getPort() != -1 ? io.reactivex.netty.protocol.http.server.HttpServer.newServer(getPort()) : diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/TomcatHttpServer.java b/spring-web-reactive/src/test/java/org/springframework/http/server/TomcatHttpServer.java similarity index 91% rename from spring-web-reactive/src/test/java/org/springframework/web/reactive/TomcatHttpServer.java rename to spring-web-reactive/src/test/java/org/springframework/http/server/TomcatHttpServer.java index c1c588696c..2dc89f2d79 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/TomcatHttpServer.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/TomcatHttpServer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.web.reactive; +package org.springframework.http.server; import java.io.File; @@ -25,7 +25,7 @@ import org.apache.catalina.startup.Tomcat; import org.springframework.beans.factory.InitializingBean; import org.springframework.util.Assert; import org.springframework.util.SocketUtils; -import org.springframework.web.reactive.server.servlet.HttpHandlerServlet; +import org.springframework.http.server.servlet31.Servlet31HttpHandlerAdapter; /** @@ -54,7 +54,7 @@ public class TomcatHttpServer extends HttpServerSupport implements InitializingB this.tomcatServer.setPort(getPort()); Assert.notNull(getHttpHandler()); - HttpHandlerServlet servlet = new HttpHandlerServlet(); + Servlet31HttpHandlerAdapter servlet = new Servlet31HttpHandlerAdapter(); servlet.setHandler(getHttpHandler()); File base = new File(System.getProperty("java.io.tmpdir")); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/UndertowHttpServer.java b/spring-web-reactive/src/test/java/org/springframework/http/server/UndertowHttpServer.java similarity index 92% rename from spring-web-reactive/src/test/java/org/springframework/web/reactive/UndertowHttpServer.java rename to spring-web-reactive/src/test/java/org/springframework/http/server/UndertowHttpServer.java index 511490ab86..6248bdb085 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/UndertowHttpServer.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/UndertowHttpServer.java @@ -14,11 +14,11 @@ * limitations under the License. */ -package org.springframework.web.reactive; +package org.springframework.http.server; import org.springframework.beans.factory.InitializingBean; import org.springframework.util.Assert; -import org.springframework.web.reactive.server.undertow.UndertowHttpHandlerAdapter; +import org.springframework.http.server.undertow.UndertowHttpHandlerAdapter; import io.undertow.Undertow; import io.undertow.server.HttpHandler; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/XmlHandler.java b/spring-web-reactive/src/test/java/org/springframework/http/server/XmlHandler.java similarity index 89% rename from spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/XmlHandler.java rename to spring-web-reactive/src/test/java/org/springframework/http/server/XmlHandler.java index 382759767c..ea069de7cc 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/XmlHandler.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/XmlHandler.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.web.reactive.handler; +package org.springframework.http.server; import javax.xml.bind.JAXBContext; import javax.xml.bind.Marshaller; @@ -27,18 +27,15 @@ import reactor.io.buffer.Buffer; import reactor.rx.Streams; import org.springframework.http.MediaType; -import org.springframework.http.server.ReactiveServerHttpRequest; -import org.springframework.http.server.ReactiveServerHttpResponse; import org.springframework.reactive.io.BufferOutputStream; import org.springframework.reactive.io.ByteBufferPublisherInputStream; -import org.springframework.web.reactive.HttpHandler; import static org.junit.Assert.fail; /** * @author Arjen Poutsma */ -public class XmlHandler implements HttpHandler { +public class XmlHandler implements ReactiveHttpHandler { private static final Log logger = LogFactory.getLog(XmlHandler.class); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/XmlHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/XmlHandlerIntegrationTests.java similarity index 89% rename from spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/XmlHandlerIntegrationTests.java rename to spring-web-reactive/src/test/java/org/springframework/http/server/XmlHandlerIntegrationTests.java index 4dc9347f0d..54777283ed 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/XmlHandlerIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/XmlHandlerIntegrationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.web.reactive.handler; +package org.springframework.http.server; import java.net.URI; import javax.xml.bind.annotation.XmlRootElement; @@ -24,8 +24,6 @@ import org.junit.Test; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; import org.springframework.web.client.RestTemplate; -import org.springframework.web.reactive.AbstractHttpHandlerIntegrationTests; -import org.springframework.web.reactive.HttpHandler; /** * @author Arjen Poutsma @@ -33,7 +31,7 @@ import org.springframework.web.reactive.HttpHandler; public class XmlHandlerIntegrationTests extends AbstractHttpHandlerIntegrationTests { @Override - protected HttpHandler createHttpHandler() { + protected ReactiveHttpHandler createHttpHandler() { return new XmlHandler(); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/server/servlet/AsyncContextSynchronizerTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/servlet31/AsyncContextSynchronizerTests.java similarity index 90% rename from spring-web-reactive/src/test/java/org/springframework/web/reactive/server/servlet/AsyncContextSynchronizerTests.java rename to spring-web-reactive/src/test/java/org/springframework/http/server/servlet31/AsyncContextSynchronizerTests.java index f9897fdb5d..9271dd6c61 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/server/servlet/AsyncContextSynchronizerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/servlet31/AsyncContextSynchronizerTests.java @@ -14,14 +14,14 @@ * limitations under the License. */ -package org.springframework.web.reactive.server.servlet; +package org.springframework.http.server.servlet31; import javax.servlet.AsyncContext; import org.junit.Before; import org.junit.Test; -import org.springframework.web.reactive.server.servlet.AsyncContextSynchronizer; +import org.springframework.http.server.servlet31.AsyncContextSynchronizer; import static org.mockito.BDDMockito.mock; import static org.mockito.BDDMockito.verify; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java index 2e35c4c0ce..15788670b3 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java @@ -32,8 +32,8 @@ import org.springframework.http.ResponseEntity; import org.springframework.http.server.ReactiveServerHttpRequest; import org.springframework.http.server.ReactiveServerHttpResponse; import org.springframework.web.reactive.DispatcherHandler; -import org.springframework.web.reactive.AbstractHttpHandlerIntegrationTests; -import org.springframework.web.reactive.HttpHandler; +import org.springframework.http.server.AbstractHttpHandlerIntegrationTests; +import org.springframework.http.server.ReactiveHttpHandler; import org.springframework.web.client.RestTemplate; import org.springframework.web.context.support.StaticWebApplicationContext; @@ -50,7 +50,7 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler @Override - protected HttpHandler createHttpHandler() { + protected ReactiveHttpHandler createHttpHandler() { StaticWebApplicationContext wac = new StaticWebApplicationContext(); wac.registerSingleton("hm", TestHandlerMapping.class); @@ -100,7 +100,7 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler } } - private static class FooHandler implements HttpHandler { + private static class FooHandler implements ReactiveHttpHandler { @Override public Publisher handle(ReactiveServerHttpRequest request, ReactiveServerHttpResponse response) { @@ -108,7 +108,7 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler } } - private static class BarHandler implements HttpHandler { + private static class BarHandler implements ReactiveHttpHandler { @Override public Publisher handle(ReactiveServerHttpRequest request, ReactiveServerHttpResponse response) { diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java index a1887e0390..3987d9b12b 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java @@ -53,8 +53,8 @@ import org.springframework.reactive.codec.encoder.JsonObjectEncoder; import org.springframework.reactive.codec.encoder.StringEncoder; import org.springframework.web.reactive.DispatcherHandler; import org.springframework.web.reactive.handler.SimpleHandlerResultHandler; -import org.springframework.web.reactive.AbstractHttpHandlerIntegrationTests; -import org.springframework.web.reactive.HttpHandler; +import org.springframework.http.server.AbstractHttpHandlerIntegrationTests; +import org.springframework.http.server.ReactiveHttpHandler; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -76,7 +76,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @Override - protected HttpHandler createHttpHandler() { + protected ReactiveHttpHandler createHttpHandler() { this.wac = new AnnotationConfigWebApplicationContext(); this.wac.register(FrameworkConfig.class, ApplicationConfig.class); this.wac.refresh(); From 8d30722f21d0c6301ef6bee0e5498ebb37af7c4f Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 20 Nov 2015 13:17:10 -0500 Subject: [PATCH 093/498] Remove @MVC test with multiple matches RequestMappingHandlerMapping currently picks the first match and does have logic to deal with selecting the best match. This caused a random test failure depending on which controller method was matched first. This change removes the test. --- .../RequestMappingHandlerMappingTests.java | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java index c97ac45701..93e7858989 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java @@ -32,7 +32,6 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.context.support.StaticWebApplicationContext; import org.springframework.web.method.HandlerMethod; -import org.springframework.web.reactive.method.annotation.RequestMappingHandlerMapping; import static org.junit.Assert.assertEquals; @@ -43,6 +42,7 @@ public class RequestMappingHandlerMappingTests { private RequestMappingHandlerMapping mapping; + @Before public void setup() { StaticWebApplicationContext wac = new StaticWebApplicationContext(); @@ -52,6 +52,7 @@ public class RequestMappingHandlerMappingTests { this.mapping = (RequestMappingHandlerMapping)wac.getBean("handlerMapping"); } + @Test public void path() throws Exception { ReactiveServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, "boo"); @@ -68,10 +69,6 @@ public class RequestMappingHandlerMappingTests { request = new MockServerHttpRequest(HttpMethod.GET, "foo"); handler = (HandlerMethod) this.mapping.getHandler(request); assertEquals(TestController.class.getMethod("getFoo"), handler.getMethod()); - - request = new MockServerHttpRequest(HttpMethod.PUT, "foo"); - handler = (HandlerMethod) this.mapping.getHandler(request); - assertEquals(TestController.class.getMethod("foo"), handler.getMethod()); } @@ -79,11 +76,6 @@ public class RequestMappingHandlerMappingTests { @SuppressWarnings("unused") private static class TestController { - @RequestMapping("foo") - public String foo() { - return "foo"; - } - @RequestMapping(path = "foo", method = RequestMethod.POST) public String postFoo() { return "postFoo"; @@ -106,7 +98,11 @@ public class RequestMappingHandlerMappingTests { } - private static class MockServerHttpRequest implements ReactiveServerHttpRequest{ + + /** + * TODO: this is more widely needed. + */ + private static class MockServerHttpRequest implements ReactiveServerHttpRequest { private HttpMethod method; From 911d3769224d009fb9fe08c6cb5489686bbc5713 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Sun, 22 Nov 2015 14:41:59 -0500 Subject: [PATCH 094/498] Improve MimeType -> MediaType handling This change adds an Encoder-to-MediaType lookup in ResponseBodyResultHandler to avoid MimeType to MediaType creation at runtime. --- .../annotation/ResponseBodyResultHandler.java | 71 +++++++++++-------- 1 file changed, 41 insertions(+), 30 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java index 95e511979a..326014b98c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java @@ -20,9 +20,12 @@ import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import org.reactivestreams.Publisher; import reactor.Publishers; @@ -35,13 +38,13 @@ import org.springframework.http.MediaType; import org.springframework.http.server.ReactiveServerHttpRequest; import org.springframework.http.server.ReactiveServerHttpResponse; import org.springframework.reactive.codec.encoder.Encoder; -import org.springframework.web.reactive.HandlerResult; -import org.springframework.web.reactive.HandlerResultHandler; import org.springframework.util.Assert; import org.springframework.util.MimeType; import org.springframework.web.HttpMediaTypeNotAcceptableException; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.method.HandlerMethod; +import org.springframework.web.reactive.HandlerResult; +import org.springframework.web.reactive.HandlerResultHandler; /** @@ -53,12 +56,14 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered private static final MediaType MEDIA_TYPE_APPLICATION = new MediaType("application"); - private final List allSupportedMediaTypes; - private final List> encoders; private final ConversionService conversionService; + private final List allMediaTypes; + + private final Map, List> mediaTypesByEncoder; + private int order = 0; @@ -66,23 +71,39 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered Assert.notEmpty(encoders, "At least one encoders is required."); Assert.notNull(service, "'conversionService' is required."); this.encoders = encoders; - this.allSupportedMediaTypes = getAllSupportedMediaTypes(encoders); this.conversionService = service; + this.allMediaTypes = getAllMediaTypes(encoders); + this.mediaTypesByEncoder = getMediaTypesByEncoder(encoders); } - private static List getAllSupportedMediaTypes(List> encoders) { - Set allSupportedMediaTypes = new LinkedHashSet<>(); - for (Encoder encoder : encoders) { - for (MimeType mimeType : encoder.getSupportedMimeTypes()) { - allSupportedMediaTypes.add( - new MediaType(mimeType.getType(), mimeType.getSubtype(), mimeType.getParameters())); - } - } - List result = new ArrayList<>(allSupportedMediaTypes); + private static List getAllMediaTypes(List> encoders) { + Set set = new LinkedHashSet<>(); + encoders.forEach(encoder -> set.addAll(toMediaTypes(encoder.getSupportedMimeTypes()))); + List result = new ArrayList<>(set); MediaType.sortBySpecificity(result); return Collections.unmodifiableList(result); } + private static Map, List> getMediaTypesByEncoder(List> encoders) { + Map, List> result = new HashMap<>(encoders.size()); + encoders.forEach(encoder -> result.put(encoder, toMediaTypes(encoder.getSupportedMimeTypes()))); + return Collections.unmodifiableMap(result); + } + + /** + * TODO: MediaType static method + */ + private static List toMediaTypes(List mimeTypes) { + return mimeTypes.stream().map(ResponseBodyResultHandler::toMediaType).collect(Collectors.toList()); + } + + /** + * TODO: MediaType constructor + */ + private static MediaType toMediaType(MimeType mimeType) { + return new MediaType(mimeType.getType(), mimeType.getSubtype(), mimeType.getParameters()); + } + public void setOrder(int order) { this.order = order; @@ -120,8 +141,7 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered List producibleMediaTypes = getProducibleMediaTypes(returnType); if (producibleMediaTypes.isEmpty()) { - Publishers.error(new IllegalArgumentException( - "No encoder found for return value of type: " + returnType)); + producibleMediaTypes.add(MediaType.ALL); } Set compatibleMediaTypes = new LinkedHashSet<>(); @@ -169,7 +189,7 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered } } - return Publishers.error(new HttpMediaTypeNotAcceptableException(this.allSupportedMediaTypes)); + return Publishers.error(new HttpMediaTypeNotAcceptableException(this.allMediaTypes)); } private List getAcceptableMediaTypes(ReactiveServerHttpRequest request) { @@ -178,19 +198,10 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered } private List getProducibleMediaTypes(ResolvableType type) { - List result = new ArrayList<>(); - for (Encoder encoder : this.encoders) { - if (encoder.canEncode(type, null)) { - for (MimeType mimeType : encoder.getSupportedMimeTypes()) { - result.add(new MediaType(mimeType.getType(), mimeType.getSubtype(), - mimeType.getParameters())); - } - } - } - if (result.isEmpty()) { - result.add(MediaType.ALL); - } - return result; + return this.encoders.stream() + .filter(encoder -> encoder.canEncode(type, null)) + .flatMap(encoder -> this.mediaTypesByEncoder.get(encoder).stream()) + .collect(Collectors.toList()); } /** From 4a3c0bc3b615df84afd7deb18bc102f5d363076e Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Sun, 22 Nov 2015 15:02:18 -0500 Subject: [PATCH 095/498] Remove top-level org.springfamework.reactive package reactive.codec -> core.codec reactive.io -> util These may very well not be the final locations. For now they simply express that there are classes that belong somewhere in core, i.e. they are lower level dependencies than web. --- .../codec/CodecException.java | 2 +- .../codec/decoder => core/codec}/Decoder.java | 4 +- .../codec/encoder => core/codec}/Encoder.java | 3 +- .../codec/support}/AbstractDecoder.java | 3 +- .../codec/support}/AbstractEncoder.java | 3 +- .../codec/support}/ByteBufferDecoder.java | 2 +- .../codec/support}/ByteBufferEncoder.java | 2 +- .../codec/support}/JacksonJsonDecoder.java | 8 +-- .../codec/support}/JacksonJsonEncoder.java | 8 +-- .../codec/support}/Jaxb2Decoder.java | 11 ++- .../codec/support}/Jaxb2Encoder.java | 11 ++- .../codec/support}/JsonObjectDecoder.java | 3 +- .../codec/support}/JsonObjectEncoder.java | 3 +- .../codec/support}/StringDecoder.java | 3 +- .../codec/support}/StringEncoder.java | 3 +- .../servlet31/RequestBodyPublisher.java | 52 +++++++++++++- .../reactive/util/DemandCounter.java | 70 ------------------- .../io => util}/BufferOutputStream.java | 2 +- .../io => util}/ByteBufferInputStream.java | 2 +- .../ByteBufferPublisherInputStream.java | 4 +- .../RequestBodyArgumentResolver.java | 2 +- .../RequestMappingHandlerAdapter.java | 10 +-- .../annotation/ResponseBodyResultHandler.java | 2 +- .../http/server/XmlHandler.java | 4 +- .../codec/decoder/ByteBufferDecoderTests.java | 1 + .../decoder/JacksonJsonDecoderTests.java | 1 + .../codec/decoder/Jaxb2DecoderTests.java | 1 + .../codec/decoder/JsonObjectDecoderTests.java | 2 + .../codec/decoder/StringDecoderTests.java | 1 + .../codec/encoder/ByteBufferEncoderTests.java | 1 + .../encoder/JacksonJsonEncoderTests.java | 1 + .../codec/encoder/Jaxb2EncoderTests.java | 1 + .../codec/encoder/JsonObjectEncoderTests.java | 2 + .../codec/encoder/StringEncoderTests.java | 2 +- .../RequestMappingIntegrationTests.java | 8 +-- .../ResponseBodyResultHandlerTests.java | 3 +- 36 files changed, 112 insertions(+), 129 deletions(-) rename spring-web-reactive/src/main/java/org/springframework/{reactive => core}/codec/CodecException.java (95%) rename spring-web-reactive/src/main/java/org/springframework/{reactive/codec/decoder => core/codec}/Decoder.java (94%) rename spring-web-reactive/src/main/java/org/springframework/{reactive/codec/encoder => core/codec}/Encoder.java (94%) rename spring-web-reactive/src/main/java/org/springframework/{reactive/codec/decoder => core/codec/support}/AbstractDecoder.java (93%) rename spring-web-reactive/src/main/java/org/springframework/{reactive/codec/encoder => core/codec/support}/AbstractEncoder.java (93%) rename spring-web-reactive/src/main/java/org/springframework/{reactive/codec/decoder => core/codec/support}/ByteBufferDecoder.java (96%) rename spring-web-reactive/src/main/java/org/springframework/{reactive/codec/encoder => core/codec/support}/ByteBufferEncoder.java (96%) rename spring-web-reactive/src/main/java/org/springframework/{reactive/codec/decoder => core/codec/support}/JacksonJsonDecoder.java (90%) rename spring-web-reactive/src/main/java/org/springframework/{reactive/codec/encoder => core/codec/support}/JacksonJsonEncoder.java (90%) rename spring-web-reactive/src/main/java/org/springframework/{reactive/codec/decoder => core/codec/support}/Jaxb2Decoder.java (92%) rename spring-web-reactive/src/main/java/org/springframework/{reactive/codec/encoder => core/codec/support}/Jaxb2Encoder.java (90%) rename spring-web-reactive/src/main/java/org/springframework/{reactive/codec/decoder => core/codec/support}/JsonObjectDecoder.java (98%) rename spring-web-reactive/src/main/java/org/springframework/{reactive/codec/encoder => core/codec/support}/JsonObjectEncoder.java (96%) rename spring-web-reactive/src/main/java/org/springframework/{reactive/codec/decoder => core/codec/support}/StringDecoder.java (94%) rename spring-web-reactive/src/main/java/org/springframework/{reactive/codec/encoder => core/codec/support}/StringEncoder.java (94%) delete mode 100644 spring-web-reactive/src/main/java/org/springframework/reactive/util/DemandCounter.java rename spring-web-reactive/src/main/java/org/springframework/{reactive/io => util}/BufferOutputStream.java (96%) rename spring-web-reactive/src/main/java/org/springframework/{reactive/io => util}/ByteBufferInputStream.java (97%) rename spring-web-reactive/src/main/java/org/springframework/{reactive/io => util}/ByteBufferPublisherInputStream.java (97%) diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/CodecException.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/CodecException.java similarity index 95% rename from spring-web-reactive/src/main/java/org/springframework/reactive/codec/CodecException.java rename to spring-web-reactive/src/main/java/org/springframework/core/codec/CodecException.java index 3ed102ea04..51718cddd0 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/CodecException.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/CodecException.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.codec; +package org.springframework.core.codec; import org.springframework.core.NestedRuntimeException; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/Decoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/Decoder.java similarity index 94% rename from spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/Decoder.java rename to spring-web-reactive/src/main/java/org/springframework/core/codec/Decoder.java index dbb43b5227..cd7e399044 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/Decoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/Decoder.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.codec.decoder; +package org.springframework.core.codec; import java.nio.ByteBuffer; import java.util.List; @@ -22,7 +22,7 @@ import java.util.List; import org.reactivestreams.Publisher; import org.springframework.core.ResolvableType; -import org.springframework.reactive.codec.encoder.Encoder; +import org.springframework.core.codec.Encoder; import org.springframework.util.MimeType; /** diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/Encoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/Encoder.java similarity index 94% rename from spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/Encoder.java rename to spring-web-reactive/src/main/java/org/springframework/core/codec/Encoder.java index 22240f93e5..72e9a38f5d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/Encoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/Encoder.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.codec.encoder; +package org.springframework.core.codec; import java.nio.ByteBuffer; import java.util.List; @@ -22,7 +22,6 @@ import java.util.List; import org.reactivestreams.Publisher; import org.springframework.core.ResolvableType; -import org.springframework.reactive.codec.decoder.Decoder; import org.springframework.util.MimeType; /** diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/AbstractDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractDecoder.java similarity index 93% rename from spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/AbstractDecoder.java rename to spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractDecoder.java index aa17856078..9f4b8b5705 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/AbstractDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractDecoder.java @@ -14,13 +14,14 @@ * limitations under the License. */ -package org.springframework.reactive.codec.decoder; +package org.springframework.core.codec.support; import java.util.Arrays; import java.util.Collections; import java.util.List; import org.springframework.core.ResolvableType; +import org.springframework.core.codec.Decoder; import org.springframework.util.MimeType; /** diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/AbstractEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractEncoder.java similarity index 93% rename from spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/AbstractEncoder.java rename to spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractEncoder.java index 08f504423b..8010042158 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/AbstractEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractEncoder.java @@ -14,13 +14,14 @@ * limitations under the License. */ -package org.springframework.reactive.codec.encoder; +package org.springframework.core.codec.support; import java.util.Arrays; import java.util.Collections; import java.util.List; import org.springframework.core.ResolvableType; +import org.springframework.core.codec.Encoder; import org.springframework.util.MimeType; /** diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/ByteBufferDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferDecoder.java similarity index 96% rename from spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/ByteBufferDecoder.java rename to spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferDecoder.java index fd1ddaad9f..54b78143ee 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/ByteBufferDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferDecoder.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.codec.decoder; +package org.springframework.core.codec.support; import java.nio.ByteBuffer; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/ByteBufferEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferEncoder.java similarity index 96% rename from spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/ByteBufferEncoder.java rename to spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferEncoder.java index 11ca5ed4ab..fad1fc8d28 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/ByteBufferEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferEncoder.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.codec.encoder; +package org.springframework.core.codec.support; import java.nio.ByteBuffer; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonDecoder.java similarity index 90% rename from spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoder.java rename to spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonDecoder.java index 39a4133e8c..3280831ee1 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonDecoder.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.codec.decoder; +package org.springframework.core.codec.support; import java.io.IOException; import java.nio.ByteBuffer; @@ -26,9 +26,9 @@ import org.reactivestreams.Publisher; import reactor.Publishers; import org.springframework.core.ResolvableType; -import org.springframework.reactive.codec.CodecException; -import org.springframework.reactive.codec.encoder.JacksonJsonEncoder; -import org.springframework.reactive.io.ByteBufferInputStream; +import org.springframework.core.codec.CodecException; +import org.springframework.core.codec.Decoder; +import org.springframework.util.ByteBufferInputStream; import org.springframework.util.MimeType; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonEncoder.java similarity index 90% rename from spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoder.java rename to spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonEncoder.java index d12df127b7..5b656935f5 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonEncoder.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.codec.encoder; +package org.springframework.core.codec.support; import java.io.IOException; import java.nio.ByteBuffer; @@ -26,9 +26,9 @@ import reactor.Publishers; import reactor.io.buffer.Buffer; import org.springframework.core.ResolvableType; -import org.springframework.reactive.codec.CodecException; -import org.springframework.reactive.codec.decoder.JacksonJsonDecoder; -import org.springframework.reactive.io.BufferOutputStream; +import org.springframework.core.codec.CodecException; +import org.springframework.core.codec.Encoder; +import org.springframework.util.BufferOutputStream; import org.springframework.util.MimeType; /** diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/Jaxb2Decoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Decoder.java similarity index 92% rename from spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/Jaxb2Decoder.java rename to spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Decoder.java index 63f0cbc323..12c2973dd8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/Jaxb2Decoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Decoder.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.codec.decoder; +package org.springframework.core.codec.support; import java.nio.ByteBuffer; import java.util.concurrent.ConcurrentHashMap; @@ -37,12 +37,11 @@ import org.xml.sax.helpers.XMLReaderFactory; import reactor.Publishers; import org.springframework.core.ResolvableType; -import org.springframework.http.MediaType; -import org.springframework.reactive.codec.CodecException; -import org.springframework.reactive.codec.encoder.Jaxb2Encoder; -import org.springframework.reactive.io.ByteBufferPublisherInputStream; +import org.springframework.core.codec.CodecException; +import org.springframework.util.ByteBufferPublisherInputStream; import org.springframework.util.Assert; import org.springframework.util.MimeType; +import org.springframework.util.MimeTypeUtils; /** * Decode from a bytes stream of XML elements to a stream of {@code Object} (POJO). @@ -56,7 +55,7 @@ public class Jaxb2Decoder extends AbstractDecoder { public Jaxb2Decoder() { - super(MediaType.APPLICATION_XML, MediaType.TEXT_XML); + super(MimeTypeUtils.APPLICATION_XML, MimeTypeUtils.TEXT_XML); } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/Jaxb2Encoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Encoder.java similarity index 90% rename from spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/Jaxb2Encoder.java rename to spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Encoder.java index 1e8c6ff375..a3a64e56ae 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/Jaxb2Encoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Encoder.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.codec.encoder; +package org.springframework.core.codec.support; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; @@ -30,13 +30,12 @@ import reactor.Publishers; import reactor.io.buffer.Buffer; import org.springframework.core.ResolvableType; -import org.springframework.http.MediaType; -import org.springframework.reactive.codec.CodecException; -import org.springframework.reactive.codec.decoder.Jaxb2Decoder; -import org.springframework.reactive.io.BufferOutputStream; +import org.springframework.core.codec.CodecException; +import org.springframework.util.BufferOutputStream; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.MimeType; +import org.springframework.util.MimeTypeUtils; /** * Encode from an {@code Object} stream to a byte stream of XML elements. @@ -50,7 +49,7 @@ public class Jaxb2Encoder extends AbstractEncoder { public Jaxb2Encoder() { - super(MediaType.APPLICATION_XML, MediaType.TEXT_XML); + super(MimeTypeUtils.APPLICATION_XML, MimeTypeUtils.TEXT_XML); } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JsonObjectDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectDecoder.java similarity index 98% rename from spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JsonObjectDecoder.java rename to spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectDecoder.java index 5267b82dac..95889640b4 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JsonObjectDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectDecoder.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.codec.decoder; +package org.springframework.core.codec.support; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; @@ -29,7 +29,6 @@ import reactor.Publishers; import reactor.fn.Function; import org.springframework.core.ResolvableType; -import org.springframework.reactive.codec.encoder.JsonObjectEncoder; import org.springframework.util.MimeType; /** diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JsonObjectEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectEncoder.java similarity index 96% rename from spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JsonObjectEncoder.java rename to spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectEncoder.java index 9b5b8775ef..d3079ebe2f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JsonObjectEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectEncoder.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.codec.encoder; +package org.springframework.core.codec.support; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; @@ -28,7 +28,6 @@ import reactor.core.support.BackpressureUtils; import reactor.io.buffer.Buffer; import org.springframework.core.ResolvableType; -import org.springframework.reactive.codec.decoder.JsonObjectDecoder; import org.springframework.util.MimeType; import static reactor.Publishers.lift; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/StringDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java similarity index 94% rename from spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/StringDecoder.java rename to spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java index de47a06fda..c2b20fd8b4 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/StringDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.codec.decoder; +package org.springframework.core.codec.support; import java.nio.ByteBuffer; import java.nio.charset.Charset; @@ -25,7 +25,6 @@ import reactor.Publishers; import reactor.io.buffer.Buffer; import org.springframework.core.ResolvableType; -import org.springframework.reactive.codec.encoder.StringEncoder; import org.springframework.util.MimeType; /** diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/StringEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringEncoder.java similarity index 94% rename from spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/StringEncoder.java rename to spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringEncoder.java index b87387773f..756638198e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/StringEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringEncoder.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.codec.encoder; +package org.springframework.core.codec.support; import java.nio.ByteBuffer; import java.nio.charset.Charset; @@ -24,7 +24,6 @@ import org.reactivestreams.Publisher; import reactor.Publishers; import org.springframework.core.ResolvableType; -import org.springframework.reactive.codec.decoder.StringDecoder; import org.springframework.util.MimeType; /** diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/servlet31/RequestBodyPublisher.java b/spring-web-reactive/src/main/java/org/springframework/http/server/servlet31/RequestBodyPublisher.java index f79d4d9876..956c00e900 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/servlet31/RequestBodyPublisher.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/servlet31/RequestBodyPublisher.java @@ -19,6 +19,7 @@ package org.springframework.http.server.servlet31; import java.io.IOException; import java.nio.ByteBuffer; import java.util.Arrays; +import java.util.concurrent.atomic.AtomicLong; import javax.servlet.ReadListener; import javax.servlet.ServletInputStream; @@ -28,7 +29,7 @@ import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; -import org.springframework.reactive.util.DemandCounter; +import org.springframework.util.Assert; /** * @author Arjen Poutsma @@ -165,4 +166,53 @@ public class RequestBodyPublisher implements ReadListener, Publisher demand.reset(); } } + + + /** + * Small utility class for keeping track of Reactive Streams demand. + */ + private static final class DemandCounter { + + private final AtomicLong demand = new AtomicLong(); + + /** + * Increases the demand by the given number + * @param n the positive number to increase demand by + * @return the increased demand + * @see org.reactivestreams.Subscription#request(long) + */ + public long increase(long n) { + Assert.isTrue(n > 0, "'n' must be higher than 0"); + return demand.updateAndGet(d -> d != Long.MAX_VALUE ? d + n : Long.MAX_VALUE); + } + + /** + * Decreases the demand by one. + * @return the decremented demand + */ + public long decrement() { + return demand.updateAndGet(d -> d != Long.MAX_VALUE ? d - 1 : Long.MAX_VALUE); + } + + /** + * Indicates whether this counter has demand, i.e. whether it is higher than 0. + * @return {@code true} if this counter has demand; {@code false} otherwise + */ + public boolean hasDemand() { + return this.demand.get() > 0; + } + + /** + * Resets this counter to 0. + * @see org.reactivestreams.Subscription#cancel() + */ + public void reset() { + this.demand.set(0); + } + + @Override + public String toString() { + return demand.toString(); + } + } } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/util/DemandCounter.java b/spring-web-reactive/src/main/java/org/springframework/reactive/util/DemandCounter.java deleted file mode 100644 index cf8cf42c45..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/util/DemandCounter.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2002-2015 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.reactive.util; - -import java.util.concurrent.atomic.AtomicLong; - -import org.springframework.util.Assert; - -/** - * Small utility class for keeping track of Reactive Streams demand. - * @author Arjen Poutsma - */ -public final class DemandCounter { - - private final AtomicLong demand = new AtomicLong(); - - /** - * Increases the demand by the given number - * @param n the positive number to increase demand by - * @return the increased demand - * @see org.reactivestreams.Subscription#request(long) - */ - public long increase(long n) { - Assert.isTrue(n > 0, "'n' must be higher than 0"); - return demand.updateAndGet(d -> d != Long.MAX_VALUE ? d + n : Long.MAX_VALUE); - } - - /** - * Decreases the demand by one. - * @return the decremented demand - */ - public long decrement() { - return demand.updateAndGet(d -> d != Long.MAX_VALUE ? d - 1 : Long.MAX_VALUE); - } - - /** - * Indicates whether this counter has demand, i.e. whether it is higher than 0. - * @return {@code true} if this counter has demand; {@code false} otherwise - */ - public boolean hasDemand() { - return this.demand.get() > 0; - } - - /** - * Resets this counter to 0. - * @see org.reactivestreams.Subscription#cancel() - */ - public void reset() { - this.demand.set(0); - } - - @Override - public String toString() { - return demand.toString(); - } -} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/io/BufferOutputStream.java b/spring-web-reactive/src/main/java/org/springframework/util/BufferOutputStream.java similarity index 96% rename from spring-web-reactive/src/main/java/org/springframework/reactive/io/BufferOutputStream.java rename to spring-web-reactive/src/main/java/org/springframework/util/BufferOutputStream.java index 6c97f7b612..8f826f67c6 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/io/BufferOutputStream.java +++ b/spring-web-reactive/src/main/java/org/springframework/util/BufferOutputStream.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.io; +package org.springframework.util; import java.io.IOException; import java.io.OutputStream; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteBufferInputStream.java b/spring-web-reactive/src/main/java/org/springframework/util/ByteBufferInputStream.java similarity index 97% rename from spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteBufferInputStream.java rename to spring-web-reactive/src/main/java/org/springframework/util/ByteBufferInputStream.java index a006596b2a..b3a4945117 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteBufferInputStream.java +++ b/spring-web-reactive/src/main/java/org/springframework/util/ByteBufferInputStream.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.io; +package org.springframework.util; import java.io.IOException; import java.io.InputStream; diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteBufferPublisherInputStream.java b/spring-web-reactive/src/main/java/org/springframework/util/ByteBufferPublisherInputStream.java similarity index 97% rename from spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteBufferPublisherInputStream.java rename to spring-web-reactive/src/main/java/org/springframework/util/ByteBufferPublisherInputStream.java index 9f93f3aa62..26d2e5ee41 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/io/ByteBufferPublisherInputStream.java +++ b/spring-web-reactive/src/main/java/org/springframework/util/ByteBufferPublisherInputStream.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.io; +package org.springframework.util; import java.io.IOException; import java.io.InputStream; @@ -25,8 +25,6 @@ import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; import reactor.Publishers; -import org.springframework.util.Assert; - /** * {@code InputStream} implementation based on a byte array {@link Publisher}. * diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestBodyArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestBodyArgumentResolver.java index 853494d410..de3700b47a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestBodyArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestBodyArgumentResolver.java @@ -27,7 +27,7 @@ import org.springframework.core.ResolvableType; import org.springframework.core.convert.ConversionService; import org.springframework.http.MediaType; import org.springframework.http.server.ReactiveServerHttpRequest; -import org.springframework.reactive.codec.decoder.Decoder; +import org.springframework.core.codec.Decoder; import org.springframework.web.reactive.method.HandlerMethodArgumentResolver; import org.springframework.util.Assert; import org.springframework.web.bind.annotation.RequestBody; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java index e58f894289..a73e53a0b9 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java @@ -28,11 +28,11 @@ import org.springframework.core.ResolvableType; import org.springframework.core.convert.ConversionService; import org.springframework.http.server.ReactiveServerHttpRequest; import org.springframework.http.server.ReactiveServerHttpResponse; -import org.springframework.reactive.codec.decoder.ByteBufferDecoder; -import org.springframework.reactive.codec.decoder.Decoder; -import org.springframework.reactive.codec.decoder.JacksonJsonDecoder; -import org.springframework.reactive.codec.decoder.JsonObjectDecoder; -import org.springframework.reactive.codec.decoder.StringDecoder; +import org.springframework.core.codec.support.ByteBufferDecoder; +import org.springframework.core.codec.Decoder; +import org.springframework.core.codec.support.JacksonJsonDecoder; +import org.springframework.core.codec.support.JsonObjectDecoder; +import org.springframework.core.codec.support.StringDecoder; import org.springframework.web.reactive.HandlerAdapter; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.method.HandlerMethodArgumentResolver; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java index 326014b98c..47f2ff691e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java @@ -37,7 +37,7 @@ import org.springframework.core.convert.ConversionService; import org.springframework.http.MediaType; import org.springframework.http.server.ReactiveServerHttpRequest; import org.springframework.http.server.ReactiveServerHttpResponse; -import org.springframework.reactive.codec.encoder.Encoder; +import org.springframework.core.codec.Encoder; import org.springframework.util.Assert; import org.springframework.util.MimeType; import org.springframework.web.HttpMediaTypeNotAcceptableException; diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/XmlHandler.java b/spring-web-reactive/src/test/java/org/springframework/http/server/XmlHandler.java index ea069de7cc..81a68d363f 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/XmlHandler.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/XmlHandler.java @@ -27,8 +27,8 @@ import reactor.io.buffer.Buffer; import reactor.rx.Streams; import org.springframework.http.MediaType; -import org.springframework.reactive.io.BufferOutputStream; -import org.springframework.reactive.io.ByteBufferPublisherInputStream; +import org.springframework.util.BufferOutputStream; +import org.springframework.util.ByteBufferPublisherInputStream; import static org.junit.Assert.fail; diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/ByteBufferDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/ByteBufferDecoderTests.java index 96fa509745..543fd60f68 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/ByteBufferDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/ByteBufferDecoderTests.java @@ -27,6 +27,7 @@ import reactor.rx.Stream; import reactor.rx.Streams; import org.springframework.core.ResolvableType; +import org.springframework.core.codec.support.ByteBufferDecoder; import org.springframework.http.MediaType; /** diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoderTests.java index 82a185efcb..adc55f2e45 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoderTests.java @@ -28,6 +28,7 @@ import reactor.rx.Stream; import reactor.rx.Streams; import org.springframework.core.ResolvableType; +import org.springframework.core.codec.support.JacksonJsonDecoder; import org.springframework.http.MediaType; import org.springframework.reactive.codec.Pojo; diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/Jaxb2DecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/Jaxb2DecoderTests.java index 155dbbb3ff..110db95799 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/Jaxb2DecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/Jaxb2DecoderTests.java @@ -28,6 +28,7 @@ import reactor.rx.Stream; import reactor.rx.Streams; import org.springframework.core.ResolvableType; +import org.springframework.core.codec.support.Jaxb2Decoder; import org.springframework.http.MediaType; import org.springframework.reactive.codec.Pojo; diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/JsonObjectDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/JsonObjectDecoderTests.java index 52bc2253c6..915d9677da 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/JsonObjectDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/JsonObjectDecoderTests.java @@ -26,6 +26,8 @@ import reactor.io.buffer.Buffer; import reactor.rx.Stream; import reactor.rx.Streams; +import org.springframework.core.codec.support.JsonObjectDecoder; + /** * @author Sebastien Deleuze */ diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/StringDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/StringDecoderTests.java index cc9dc6c14c..90ab1978ec 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/StringDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/StringDecoderTests.java @@ -29,6 +29,7 @@ import reactor.rx.Stream; import reactor.rx.Streams; import org.springframework.core.ResolvableType; +import org.springframework.core.codec.support.StringDecoder; import org.springframework.http.MediaType; /** diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/ByteBufferEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/ByteBufferEncoderTests.java index 4c5a8657e0..6c79d56bb4 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/ByteBufferEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/ByteBufferEncoderTests.java @@ -27,6 +27,7 @@ import reactor.rx.Stream; import reactor.rx.Streams; import org.springframework.core.ResolvableType; +import org.springframework.core.codec.support.ByteBufferEncoder; import org.springframework.http.MediaType; /** diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoderTests.java index 22af3b1683..8193b938dc 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoderTests.java @@ -26,6 +26,7 @@ import org.junit.Test; import reactor.rx.Stream; import reactor.rx.Streams; +import org.springframework.core.codec.support.JacksonJsonEncoder; import org.springframework.http.MediaType; import org.springframework.reactive.codec.Pojo; diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/Jaxb2EncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/Jaxb2EncoderTests.java index d0cc833b64..1b6a8b7360 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/Jaxb2EncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/Jaxb2EncoderTests.java @@ -26,6 +26,7 @@ import org.junit.Test; import reactor.rx.Stream; import reactor.rx.Streams; +import org.springframework.core.codec.support.Jaxb2Encoder; import org.springframework.http.MediaType; import org.springframework.reactive.codec.Pojo; diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/JsonObjectEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/JsonObjectEncoderTests.java index 9f4eed32b9..5e56f044c5 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/JsonObjectEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/JsonObjectEncoderTests.java @@ -26,6 +26,8 @@ import reactor.io.buffer.Buffer; import reactor.rx.Stream; import reactor.rx.Streams; +import org.springframework.core.codec.support.JsonObjectEncoder; + /** * @author Sebastien Deleuze */ diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/StringEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/StringEncoderTests.java index 151c4f8562..2c371f0391 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/StringEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/StringEncoderTests.java @@ -23,10 +23,10 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import org.junit.Test; -import org.reactivestreams.Publisher; import reactor.rx.Streams; import org.springframework.core.ResolvableType; +import org.springframework.core.codec.support.StringEncoder; import org.springframework.http.MediaType; /** diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java index 3987d9b12b..26a0a7e9dc 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java @@ -47,10 +47,10 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; -import org.springframework.reactive.codec.encoder.ByteBufferEncoder; -import org.springframework.reactive.codec.encoder.JacksonJsonEncoder; -import org.springframework.reactive.codec.encoder.JsonObjectEncoder; -import org.springframework.reactive.codec.encoder.StringEncoder; +import org.springframework.core.codec.support.ByteBufferEncoder; +import org.springframework.core.codec.support.JacksonJsonEncoder; +import org.springframework.core.codec.support.JsonObjectEncoder; +import org.springframework.core.codec.support.StringEncoder; import org.springframework.web.reactive.DispatcherHandler; import org.springframework.web.reactive.handler.SimpleHandlerResultHandler; import org.springframework.http.server.AbstractHttpHandlerIntegrationTests; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandlerTests.java index c8212a0301..32f30c20c5 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandlerTests.java @@ -23,11 +23,10 @@ import org.reactivestreams.Publisher; import org.springframework.core.ResolvableType; import org.springframework.core.convert.support.DefaultConversionService; -import org.springframework.reactive.codec.encoder.StringEncoder; +import org.springframework.core.codec.support.StringEncoder; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.method.HandlerMethod; -import org.springframework.web.reactive.method.annotation.ResponseBodyResultHandler; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; From dc57e2621cda360ca7f6e9c7ed3cfc18f75dae66 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Tue, 24 Nov 2015 18:11:44 +0100 Subject: [PATCH 096/498] Move HttpServer related classes back to src/main/java This is needed in order to be able to use Spring Reactive in standalone applications. These are temporary classes that will return in src/test/java when Spring Boot support for Reactive applications will be implemented. --- .../springframework/http/server/support}/HttpServer.java | 2 +- .../http/server/support}/HttpServerSupport.java | 2 +- .../http/server/support}/JettyHttpServer.java | 2 +- .../http/server/support}/ReactorHttpServer.java | 2 +- .../http/server/support}/RxNettyHttpServer.java | 2 +- .../http/server/support}/TomcatHttpServer.java | 2 +- .../http/server/support}/UndertowHttpServer.java | 2 +- .../springframework/http/server/support/package-info.java | 5 +++++ .../http/server/AbstractHttpHandlerIntegrationTests.java | 6 ++++++ 9 files changed, 18 insertions(+), 7 deletions(-) rename spring-web-reactive/src/{test/java/org/springframework/http/server => main/java/org/springframework/http/server/support}/HttpServer.java (95%) rename spring-web-reactive/src/{test/java/org/springframework/http/server => main/java/org/springframework/http/server/support}/HttpServerSupport.java (95%) rename spring-web-reactive/src/{test/java/org/springframework/http/server => main/java/org/springframework/http/server/support}/JettyHttpServer.java (98%) rename spring-web-reactive/src/{test/java/org/springframework/http/server => main/java/org/springframework/http/server/support}/ReactorHttpServer.java (97%) rename spring-web-reactive/src/{test/java/org/springframework/http/server => main/java/org/springframework/http/server/support}/RxNettyHttpServer.java (97%) rename spring-web-reactive/src/{test/java/org/springframework/http/server => main/java/org/springframework/http/server/support}/TomcatHttpServer.java (97%) rename spring-web-reactive/src/{test/java/org/springframework/http/server => main/java/org/springframework/http/server/support}/UndertowHttpServer.java (97%) create mode 100644 spring-web-reactive/src/main/java/org/springframework/http/server/support/package-info.java diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/HttpServer.java b/spring-web-reactive/src/main/java/org/springframework/http/server/support/HttpServer.java similarity index 95% rename from spring-web-reactive/src/test/java/org/springframework/http/server/HttpServer.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/support/HttpServer.java index 695609328f..f1510c8abe 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/HttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/support/HttpServer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.http.server; +package org.springframework.http.server.support; import org.springframework.beans.factory.InitializingBean; diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/HttpServerSupport.java b/spring-web-reactive/src/main/java/org/springframework/http/server/support/HttpServerSupport.java similarity index 95% rename from spring-web-reactive/src/test/java/org/springframework/http/server/HttpServerSupport.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/support/HttpServerSupport.java index 8c8358d872..3510b9d610 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/HttpServerSupport.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/support/HttpServerSupport.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.http.server; +package org.springframework.http.server.support; import org.springframework.http.server.ReactiveHttpHandler; diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/JettyHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/http/server/support/JettyHttpServer.java similarity index 98% rename from spring-web-reactive/src/test/java/org/springframework/http/server/JettyHttpServer.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/support/JettyHttpServer.java index f34221bcd3..38cc7d1a83 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/JettyHttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/support/JettyHttpServer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.http.server; +package org.springframework.http.server.support; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/ReactorHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/http/server/support/ReactorHttpServer.java similarity index 97% rename from spring-web-reactive/src/test/java/org/springframework/http/server/ReactorHttpServer.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/support/ReactorHttpServer.java index 30e2edf75e..868f0f53a6 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/ReactorHttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/support/ReactorHttpServer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.http.server; +package org.springframework.http.server.support; import reactor.io.buffer.Buffer; import reactor.io.net.ReactiveNet; diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/RxNettyHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/http/server/support/RxNettyHttpServer.java similarity index 97% rename from spring-web-reactive/src/test/java/org/springframework/http/server/RxNettyHttpServer.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/support/RxNettyHttpServer.java index 44f3f4a228..fe0bf6323d 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/RxNettyHttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/support/RxNettyHttpServer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.http.server; +package org.springframework.http.server.support; import io.netty.buffer.ByteBuf; diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/TomcatHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/http/server/support/TomcatHttpServer.java similarity index 97% rename from spring-web-reactive/src/test/java/org/springframework/http/server/TomcatHttpServer.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/support/TomcatHttpServer.java index 2dc89f2d79..fa4d989480 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/TomcatHttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/support/TomcatHttpServer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.http.server; +package org.springframework.http.server.support; import java.io.File; diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/UndertowHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/http/server/support/UndertowHttpServer.java similarity index 97% rename from spring-web-reactive/src/test/java/org/springframework/http/server/UndertowHttpServer.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/support/UndertowHttpServer.java index 6248bdb085..8a0cb5b40a 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/UndertowHttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/support/UndertowHttpServer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.http.server; +package org.springframework.http.server.support; import org.springframework.beans.factory.InitializingBean; import org.springframework.util.Assert; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/support/package-info.java b/spring-web-reactive/src/main/java/org/springframework/http/server/support/package-info.java new file mode 100644 index 0000000000..f404a34927 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/support/package-info.java @@ -0,0 +1,5 @@ +/** + * This package contains temporary interfaces and classes for running embedded servers. + * They are expected to be replaced by an upcoming Spring Boot support. + */ +package org.springframework.http.server.support; diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/AbstractHttpHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/AbstractHttpHandlerIntegrationTests.java index 5a53e0b496..0f75b552da 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/AbstractHttpHandlerIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/AbstractHttpHandlerIntegrationTests.java @@ -21,6 +21,12 @@ import org.junit.Before; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; +import org.springframework.http.server.support.HttpServer; +import org.springframework.http.server.support.JettyHttpServer; +import org.springframework.http.server.support.ReactorHttpServer; +import org.springframework.http.server.support.RxNettyHttpServer; +import org.springframework.http.server.support.TomcatHttpServer; +import org.springframework.http.server.support.UndertowHttpServer; import org.springframework.util.SocketUtils; From d319edba28d0fbf24b58525bff3cc5bd04dc934f Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 24 Nov 2015 17:29:37 -0500 Subject: [PATCH 097/498] HandlerMapping is now asynchronous --- .../web/reactive/DispatcherHandler.java | 45 ++++++++++--------- .../web/reactive/HandlerMapping.java | 4 +- .../handler/SimpleUrlHandlerMapping.java | 9 +++- .../RequestMappingHandlerMapping.java | 9 ++-- .../RequestMappingHandlerMappingTests.java | 26 ++++++++--- 5 files changed, 59 insertions(+), 34 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java index 48a1b180b8..3070db7f27 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java @@ -98,41 +98,33 @@ public class DispatcherHandler implements ReactiveHttpHandler, ApplicationContex logger.debug("Processing " + request.getMethod() + " request for [" + request.getURI() + "]"); } - Object handler = getHandler(request); - if (handler == null) { + Publisher handlerPublisher = getHandler(request); + if (handlerPublisher == null) { // No exception handling mechanism yet response.setStatusCode(HttpStatus.NOT_FOUND); response.writeHeaders(); return Publishers.empty(); } - HandlerAdapter handlerAdapter = getHandlerAdapter(handler); - if (handlerAdapter == null) { - return Publishers.error(new IllegalStateException("No HandlerAdapter for " + handler)); - } - - Publisher resultPublisher = handlerAdapter.handle(request, response, handler); + Publisher resultPublisher = Publishers.concatMap(handlerPublisher, handler -> { + HandlerAdapter handlerAdapter = getHandlerAdapter(handler); + return handlerAdapter.handle(request, response, handler); + }); return Publishers.concatMap(resultPublisher, result -> { - for (HandlerResultHandler resultHandler : resultHandlers) { - if (resultHandler.supports(result)) { - return resultHandler.handleResult(request, response, result); - } - } - return Publishers.error(new IllegalStateException( - "No HandlerResultHandler for " + result.getValue())); + HandlerResultHandler handler = getResultHandler(result); + return handler.handleResult(request, response, result); }); } - protected Object getHandler(ReactiveServerHttpRequest request) { - Object handler = null; + protected Publisher getHandler(ReactiveServerHttpRequest request) { for (HandlerMapping handlerMapping : this.handlerMappings) { - handler = handlerMapping.getHandler(request); - if (handler != null) { - break; + Publisher handlerPublisher = handlerMapping.getHandler(request); + if (handlerPublisher != null) { + return handlerPublisher; } } - return handler; + return null; } protected HandlerAdapter getHandlerAdapter(Object handler) { @@ -141,7 +133,16 @@ public class DispatcherHandler implements ReactiveHttpHandler, ApplicationContex return handlerAdapter; } } - return null; + throw new IllegalStateException("No HandlerAdapter: " + handler); + } + + protected HandlerResultHandler getResultHandler(HandlerResult handlerResult) { + for (HandlerResultHandler resultHandler : resultHandlers) { + if (resultHandler.supports(handlerResult)) { + return resultHandler; + } + } + throw new IllegalStateException("No HandlerResultHandler: " + handlerResult.getValue()); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerMapping.java index 89e11f6521..0fd4741666 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerMapping.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerMapping.java @@ -16,6 +16,8 @@ package org.springframework.web.reactive; +import org.reactivestreams.Publisher; + import org.springframework.http.server.ReactiveServerHttpRequest; /** @@ -23,6 +25,6 @@ import org.springframework.http.server.ReactiveServerHttpRequest; */ public interface HandlerMapping { - Object getHandler(ReactiveServerHttpRequest request); + Publisher getHandler(ReactiveServerHttpRequest request); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMapping.java index 8e1c906dd2..1eac42a113 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMapping.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMapping.java @@ -19,6 +19,9 @@ package org.springframework.web.reactive.handler; import java.util.HashMap; import java.util.Map; +import org.reactivestreams.Publisher; +import reactor.Publishers; + import org.springframework.http.server.ReactiveServerHttpRequest; import org.springframework.web.reactive.HandlerMapping; @@ -39,8 +42,10 @@ public class SimpleUrlHandlerMapping implements HandlerMapping { @Override - public Object getHandler(ReactiveServerHttpRequest request) { - return this.handlerMap.get(request.getURI().getPath()); + public Publisher getHandler(ReactiveServerHttpRequest request) { + String path = request.getURI().getPath(); + Object handler = this.handlerMap.get(path); + return (handler != null ? Publishers.just(handler) : null); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMapping.java index 8fa2c682be..ed9670b3e8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMapping.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMapping.java @@ -27,6 +27,8 @@ import java.util.TreeSet; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.reactivestreams.Publisher; +import reactor.Publishers; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ApplicationContext; @@ -92,15 +94,16 @@ public class RequestMappingHandlerMapping implements HandlerMapping, } @Override - public Object getHandler(ReactiveServerHttpRequest request) { + public Publisher getHandler(ReactiveServerHttpRequest request) { for (Map.Entry entry : this.methodMap.entrySet()) { RequestMappingInfo info = entry.getKey(); if (info.matchesRequest(request)) { + HandlerMethod handlerMethod = entry.getValue(); if (logger.isDebugEnabled()) { logger.debug("Mapped " + request.getMethod() + " " + - request.getURI().getPath() + " to [" + entry.getValue() + "]"); + request.getURI().getPath() + " to [" + handlerMethod + "]"); } - return entry.getValue(); + return Publishers.just(handlerMethod); } } return null; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java index 93e7858989..5fda7eb163 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java @@ -19,10 +19,13 @@ package org.springframework.web.reactive.method.annotation; import java.net.URI; import java.net.URISyntaxException; import java.nio.ByteBuffer; +import java.util.List; +import java.util.concurrent.TimeUnit; import org.junit.Before; import org.junit.Test; import org.reactivestreams.Publisher; +import reactor.rx.Streams; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -34,6 +37,7 @@ import org.springframework.web.context.support.StaticWebApplicationContext; import org.springframework.web.method.HandlerMethod; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; /** * @author Sebastien Deleuze @@ -56,19 +60,29 @@ public class RequestMappingHandlerMappingTests { @Test public void path() throws Exception { ReactiveServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, "boo"); - HandlerMethod handler = (HandlerMethod) this.mapping.getHandler(request); - assertEquals(TestController.class.getMethod("boo"), handler.getMethod()); + Publisher handlerPublisher = this.mapping.getHandler(request); + HandlerMethod handlerMethod = toHandlerMethod(handlerPublisher); + assertEquals(TestController.class.getMethod("boo"), handlerMethod.getMethod()); } @Test public void method() throws Exception { ReactiveServerHttpRequest request = new MockServerHttpRequest(HttpMethod.POST, "foo"); - HandlerMethod handler = (HandlerMethod) this.mapping.getHandler(request); - assertEquals(TestController.class.getMethod("postFoo"), handler.getMethod()); + Publisher handlerPublisher = this.mapping.getHandler(request); + HandlerMethod handlerMethod = toHandlerMethod(handlerPublisher); + assertEquals(TestController.class.getMethod("postFoo"), handlerMethod.getMethod()); request = new MockServerHttpRequest(HttpMethod.GET, "foo"); - handler = (HandlerMethod) this.mapping.getHandler(request); - assertEquals(TestController.class.getMethod("getFoo"), handler.getMethod()); + handlerPublisher = this.mapping.getHandler(request); + handlerMethod = toHandlerMethod(handlerPublisher); + assertEquals(TestController.class.getMethod("getFoo"), handlerMethod.getMethod()); + } + + private HandlerMethod toHandlerMethod(Publisher handlerPublisher) throws InterruptedException { + assertNotNull(handlerPublisher); + List handlerList = Streams.wrap(handlerPublisher).toList().await(5, TimeUnit.SECONDS); + assertEquals(1, handlerList.size()); + return (HandlerMethod) handlerList.get(0); } From b18919d111b3ae6f60df4a85fd668c6fe04fa677 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Wed, 25 Nov 2015 10:35:09 +0100 Subject: [PATCH 098/498] Improve Javadoc for HandlerAdapter and HandlerMapping --- .../org/springframework/web/reactive/HandlerAdapter.java | 2 +- .../org/springframework/web/reactive/HandlerMapping.java | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerAdapter.java index 605676a56b..da3284efee 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerAdapter.java @@ -52,7 +52,7 @@ public interface HandlerAdapter { * @param handler handler to use. This object must have previously been passed * to the {@code supports} method of this interface, which must have * returned {@code true}. - * @return An {@link HandlerResult} instance + * @return A {@link Publisher} object that produces a single {@link HandlerResult} element */ Publisher handle(ReactiveServerHttpRequest request, ReactiveServerHttpResponse response, Object handler); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerMapping.java index 0fd4741666..5b60cc86da 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerMapping.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerMapping.java @@ -21,10 +21,19 @@ import org.reactivestreams.Publisher; import org.springframework.http.server.ReactiveServerHttpRequest; /** + * Interface to be implemented by objects that define a mapping between + * requests and handler objects. + * * @author Rossen Stoyanchev + * @author Sebastien Deleuze */ public interface HandlerMapping { + /** + * Return a handler for this request. + * @param request current HTTP request + * @return A {@link Publisher} object that produces a single handler element + */ Publisher getHandler(ReactiveServerHttpRequest request); } From 0f8a4bf706309e3b445b5a32eed1c6307f9b72f4 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Wed, 25 Nov 2015 10:42:23 +0100 Subject: [PATCH 099/498] Polish imports --- .../src/main/java/org/springframework/core/codec/Decoder.java | 1 - .../org/springframework/http/server/ReactiveHttpHandler.java | 3 --- .../java/org/springframework/http/server/EchoHandler.java | 4 ---- .../http/server/servlet31/AsyncContextSynchronizerTests.java | 2 -- .../web/reactive/handler/SimpleHandlerResultHandlerTests.java | 1 - .../method/annotation/RequestMappingIntegrationTests.java | 1 - 6 files changed, 12 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/Decoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/Decoder.java index cd7e399044..b786547efc 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/Decoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/Decoder.java @@ -22,7 +22,6 @@ import java.util.List; import org.reactivestreams.Publisher; import org.springframework.core.ResolvableType; -import org.springframework.core.codec.Encoder; import org.springframework.util.MimeType; /** diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/ReactiveHttpHandler.java b/spring-web-reactive/src/main/java/org/springframework/http/server/ReactiveHttpHandler.java index fc683451dd..c801111964 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/ReactiveHttpHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/ReactiveHttpHandler.java @@ -18,9 +18,6 @@ package org.springframework.http.server; import org.reactivestreams.Publisher; -import org.springframework.http.server.ReactiveServerHttpRequest; -import org.springframework.http.server.ReactiveServerHttpResponse; - /** * Interface for handlers that process HTTP requests and generate an HTTP response. * This handler is designed to be called when the HTTP headers have been received, making diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/EchoHandler.java b/spring-web-reactive/src/test/java/org/springframework/http/server/EchoHandler.java index bf3874b360..c43d4ef919 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/EchoHandler.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/EchoHandler.java @@ -18,10 +18,6 @@ package org.springframework.http.server; import org.reactivestreams.Publisher; -import org.springframework.http.server.ReactiveServerHttpRequest; -import org.springframework.http.server.ReactiveServerHttpResponse; -import org.springframework.http.server.ReactiveHttpHandler; - /** * @author Arjen Poutsma */ diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/servlet31/AsyncContextSynchronizerTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/servlet31/AsyncContextSynchronizerTests.java index 9271dd6c61..ea516bec6f 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/servlet31/AsyncContextSynchronizerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/servlet31/AsyncContextSynchronizerTests.java @@ -21,8 +21,6 @@ import javax.servlet.AsyncContext; import org.junit.Before; import org.junit.Test; -import org.springframework.http.server.servlet31.AsyncContextSynchronizer; - import static org.mockito.BDDMockito.mock; import static org.mockito.BDDMockito.verify; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandlerTests.java index 51c0a80ee5..481291b7b2 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandlerTests.java @@ -24,7 +24,6 @@ import org.reactivestreams.Publisher; import org.springframework.core.ResolvableType; import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerResult; -import org.springframework.web.reactive.handler.SimpleHandlerResultHandler; /** * @author Sebastien Deleuze diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java index 26a0a7e9dc..98210fbeb9 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java @@ -16,7 +16,6 @@ package org.springframework.web.reactive.method.annotation; - import java.net.URI; import java.nio.ByteBuffer; import java.util.ArrayList; From 57dc8199fb7e3781a8b2a083e85ac0debe35b8bf Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 20 Nov 2015 12:48:24 -0500 Subject: [PATCH 100/498] Add ReactiveHttpFilter --- .../http/server/FilterChainHttpHandler.java | 68 +++++++++ .../http/server/ReactiveHttpFilter.java | 30 ++++ .../http/server/ReactiveHttpFilterChain.java | 31 ++++ .../server/FilterChainHttpHandlerTests.java | 142 ++++++++++++++++++ ...mpleUrlHandlerMappingIntegrationTests.java | 2 +- 5 files changed, 272 insertions(+), 1 deletion(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/http/server/FilterChainHttpHandler.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/http/server/ReactiveHttpFilter.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/http/server/ReactiveHttpFilterChain.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/http/server/FilterChainHttpHandlerTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/FilterChainHttpHandler.java b/spring-web-reactive/src/main/java/org/springframework/http/server/FilterChainHttpHandler.java new file mode 100644 index 0000000000..4550a45212 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/FilterChainHttpHandler.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2015 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.server; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.reactivestreams.Publisher; + +import org.springframework.util.Assert; + +/** + * An {@link ReactiveHttpHandler} decorator that delegates to a list of + * {@link ReactiveHttpFilter}s and the target {@link ReactiveHttpHandler}. + * + * @author Rossen Stoyanchev + */ +public class FilterChainHttpHandler implements ReactiveHttpHandler { + + private final List filters; + + private final ReactiveHttpHandler targetHandler; + + + public FilterChainHttpHandler(ReactiveHttpHandler targetHandler, ReactiveHttpFilter... filters) { + Assert.notNull(targetHandler, "'targetHandler' is required."); + this.filters = (filters != null ? Arrays.asList(filters) : Collections.emptyList()); + this.targetHandler = targetHandler; + } + + + @Override + public Publisher handle(ReactiveServerHttpRequest request, ReactiveServerHttpResponse response) { + return new DefaultHttpFilterChain().filter(request, response); + } + + + private class DefaultHttpFilterChain implements ReactiveHttpFilterChain { + + private int index; + + @Override + public Publisher filter(ReactiveServerHttpRequest request, ReactiveServerHttpResponse response) { + if (this.index < filters.size()) { + ReactiveHttpFilter filter = filters.get(this.index++); + return filter.filter(request, response, this); + } + else { + return targetHandler.handle(request, response); + } + } + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/ReactiveHttpFilter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/ReactiveHttpFilter.java new file mode 100644 index 0000000000..e189e0f777 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/ReactiveHttpFilter.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2015 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.server; + +import org.reactivestreams.Publisher; + +/** + * @author Rossen Stoyanchev + */ +public interface ReactiveHttpFilter { + + + Publisher filter(ReactiveServerHttpRequest request, ReactiveServerHttpResponse response, + ReactiveHttpFilterChain chain); + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/ReactiveHttpFilterChain.java b/spring-web-reactive/src/main/java/org/springframework/http/server/ReactiveHttpFilterChain.java new file mode 100644 index 0000000000..0ea094ff83 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/ReactiveHttpFilterChain.java @@ -0,0 +1,31 @@ +/* + * Copyright 2002-2015 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.server; + +import org.reactivestreams.Publisher; + +import org.springframework.http.server.ReactiveServerHttpRequest; +import org.springframework.http.server.ReactiveServerHttpResponse; + + +/** + * @author Rossen Stoyanchev + */ +public interface ReactiveHttpFilterChain { + + Publisher filter(ReactiveServerHttpRequest request, ReactiveServerHttpResponse response); + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/FilterChainHttpHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/FilterChainHttpHandlerTests.java new file mode 100644 index 0000000000..3617318e53 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/FilterChainHttpHandlerTests.java @@ -0,0 +1,142 @@ +/* + * Copyright 2002-2015 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.server; + + +import java.util.concurrent.TimeUnit; + +import org.junit.Before; +import org.junit.Test; +import org.reactivestreams.Publisher; +import reactor.Publishers; +import reactor.rx.Streams; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +/** + * @author Rossen Stoyanchev + */ +public class FilterChainHttpHandlerTests { + + private ReactiveServerHttpRequest request; + + private ReactiveServerHttpResponse response; + + + @Before + public void setUp() throws Exception { + this.request = mock(ReactiveServerHttpRequest.class); + this.response = mock(ReactiveServerHttpResponse.class); + } + + @Test + public void multipleFilters() throws Exception { + StubHandler handler = new StubHandler(); + TestFilter filter1 = new TestFilter(); + TestFilter filter2 = new TestFilter(); + TestFilter filter3 = new TestFilter(); + FilterChainHttpHandler filterHandler = new FilterChainHttpHandler(handler, filter1, filter2, filter3); + + Publisher voidPublisher = filterHandler.handle(this.request, this.response); + Streams.wrap(voidPublisher).toList().await(10, TimeUnit.SECONDS); + + assertTrue(filter1.invoked()); + assertTrue(filter2.invoked()); + assertTrue(filter3.invoked()); + assertTrue(handler.invoked()); + } + + @Test + public void zeroFilters() throws Exception { + StubHandler handler = new StubHandler(); + FilterChainHttpHandler filterHandler = new FilterChainHttpHandler(handler); + + Publisher voidPublisher = filterHandler.handle(this.request, this.response); + Streams.wrap(voidPublisher).toList().await(10, TimeUnit.SECONDS); + + assertTrue(handler.invoked()); + } + + @Test + public void shortcircuitFilter() throws Exception { + StubHandler handler = new StubHandler(); + TestFilter filter1 = new TestFilter(); + ShortcircuitingFilter filter2 = new ShortcircuitingFilter(); + TestFilter filter3 = new TestFilter(); + FilterChainHttpHandler filterHandler = new FilterChainHttpHandler(handler, filter1, filter2, filter3); + + Publisher voidPublisher = filterHandler.handle(this.request, this.response); + Streams.wrap(voidPublisher).toList().await(10, TimeUnit.SECONDS); + + assertTrue(filter1.invoked()); + assertTrue(filter2.invoked()); + assertFalse(filter3.invoked()); + assertFalse(handler.invoked()); + } + + + private static class TestFilter implements ReactiveHttpFilter { + + private boolean invoked; + + + public boolean invoked() { + return this.invoked; + } + + @Override + public Publisher filter(ReactiveServerHttpRequest req, ReactiveServerHttpResponse res, + ReactiveHttpFilterChain chain) { + + this.invoked = true; + return doFilter(req, res, chain); + } + + public Publisher doFilter(ReactiveServerHttpRequest req, ReactiveServerHttpResponse res, + ReactiveHttpFilterChain chain) { + + return chain.filter(req, res); + } + } + + private static class ShortcircuitingFilter extends TestFilter { + + @Override + public Publisher doFilter(ReactiveServerHttpRequest req, ReactiveServerHttpResponse res, + ReactiveHttpFilterChain chain) { + + return Publishers.empty(); + } + } + + private static class StubHandler implements ReactiveHttpHandler { + + private boolean invoked; + + public boolean invoked() { + return this.invoked; + } + + @Override + public Publisher handle(ReactiveServerHttpRequest req, ReactiveServerHttpResponse res) { + this.invoked = true; + return Publishers.empty(); + } + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java index 15788670b3..94bf276d1d 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java @@ -55,7 +55,7 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler StaticWebApplicationContext wac = new StaticWebApplicationContext(); wac.registerSingleton("hm", TestHandlerMapping.class); wac.registerSingleton("ha", HttpHandlerAdapter.class); - wac.registerSingleton("hhrh", SimpleHandlerResultHandler.class); + wac.registerSingleton("rh", SimpleHandlerResultHandler.class); wac.refresh(); DispatcherHandler dispatcherHandler = new DispatcherHandler(); From 91c2b7afadd28693cbb870504cbcc746be800664 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Mon, 30 Nov 2015 14:13:12 +0100 Subject: [PATCH 101/498] Support handler methods returning void, Observable, etc. --- .../handler/SimpleHandlerResultHandler.java | 33 +++++++-- .../SimpleHandlerResultHandlerTests.java | 73 ++++++++++++++++++- .../RequestMappingIntegrationTests.java | 48 +++++++++--- 3 files changed, 134 insertions(+), 20 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java index 628ac443b5..2b5c482eb9 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java @@ -23,22 +23,35 @@ import reactor.Publishers; import org.springframework.core.Ordered; import org.springframework.core.ResolvableType; +import org.springframework.core.convert.ConversionService; import org.springframework.http.server.ReactiveServerHttpRequest; import org.springframework.http.server.ReactiveServerHttpResponse; +import org.springframework.util.Assert; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.HandlerResultHandler; /** - * Supports {@link HandlerResult} with a {@code Publisher} value. + * Supports {@link HandlerResult} with a {@code void} or {@code Publisher} value. + * An optional {link ConversionService} can be used to support types that can be converted to + * {@code Publisher}, like {@code Observable} or {@code CompletableFuture}. * * @author Sebastien Deleuze */ public class SimpleHandlerResultHandler implements Ordered, HandlerResultHandler { - private static final ResolvableType PUBLISHER_VOID = ResolvableType.forClassWithGenerics(Publisher.class, Void.class); - private int order = Ordered.LOWEST_PRECEDENCE; + private ConversionService conversionService; + + + public SimpleHandlerResultHandler() { + } + + public SimpleHandlerResultHandler(ConversionService conversionService) { + Assert.notNull(conversionService, "'conversionService' is required."); + this.conversionService = conversionService; + } + public void setOrder(int order) { this.order = order; @@ -52,14 +65,24 @@ public class SimpleHandlerResultHandler implements Ordered, HandlerResultHandler @Override public boolean supports(HandlerResult result) { ResolvableType type = result.getValueType(); - return type != null && PUBLISHER_VOID.isAssignableFrom(type); + return type != null && Void.TYPE.equals(type.getRawClass()) || + (Void.class.isAssignableFrom(type.getGeneric(0).getRawClass()) && isConvertibleToPublisher(type)); + } + + private boolean isConvertibleToPublisher(ResolvableType type) { + return Publisher.class.isAssignableFrom(type.getRawClass()) || + ((this.conversionService != null) && this.conversionService.canConvert(type.getRawClass(), Publisher.class)); } @Override public Publisher handleResult(ReactiveServerHttpRequest request, ReactiveServerHttpResponse response, HandlerResult result) { - Publisher completion = Publishers.completable((Publisher)result.getValue()); + Object value = result.getValue(); + if (Void.TYPE.equals(result.getValueType().getRawClass())) { + return response.writeHeaders(); + } + Publisher completion = (value instanceof Publisher ? (Publisher)value : this.conversionService.convert(value, Publisher.class)); return Publishers.concat(Publishers.from(Arrays.asList(completion, response.writeHeaders()))); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandlerTests.java index 481291b7b2..0a04a12eba 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandlerTests.java @@ -16,12 +16,20 @@ package org.springframework.web.reactive.handler; +import java.util.concurrent.CompletableFuture; + import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import org.junit.Test; import org.reactivestreams.Publisher; +import reactor.rx.Stream; +import rx.Observable; import org.springframework.core.ResolvableType; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter; +import org.springframework.core.convert.support.ReactiveStreamsToReactorConverter; +import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter; import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerResult; @@ -38,7 +46,7 @@ public class SimpleHandlerResultHandlerTests { HandlerMethod hm = new HandlerMethod(controller, TestController.class.getMethod("voidReturnValue")); ResolvableType type = ResolvableType.forMethodParameter(hm.getReturnType()); - assertFalse(resultHandler.supports(new HandlerResult(hm, null, type))); + assertTrue(resultHandler.supports(new HandlerResult(hm, null, type))); hm = new HandlerMethod(controller, TestController.class.getMethod("publisherString")); type = ResolvableType.forMethodParameter(hm.getReturnType()); @@ -47,14 +55,61 @@ public class SimpleHandlerResultHandlerTests { hm = new HandlerMethod(controller, TestController.class.getMethod("publisherVoid")); type = ResolvableType.forMethodParameter(hm.getReturnType()); assertTrue(resultHandler.supports(new HandlerResult(hm, null, type))); + + hm = new HandlerMethod(controller, TestController.class.getMethod("streamVoid")); + type = ResolvableType.forMethodParameter(hm.getReturnType()); + // Reactor Stream is a Publisher + assertTrue(resultHandler.supports(new HandlerResult(hm, null, type))); + + hm = new HandlerMethod(controller, TestController.class.getMethod("observableVoid")); + type = ResolvableType.forMethodParameter(hm.getReturnType()); + assertFalse(resultHandler.supports(new HandlerResult(hm, null, type))); + + hm = new HandlerMethod(controller, TestController.class.getMethod("completableFutureVoid")); + type = ResolvableType.forMethodParameter(hm.getReturnType()); + assertFalse(resultHandler.supports(new HandlerResult(hm, null, type))); + } + + @Test + public void supportsWithConversionService() throws NoSuchMethodException { + + GenericConversionService conversionService = new GenericConversionService(); + conversionService.addConverter(new ReactiveStreamsToCompletableFutureConverter()); + conversionService.addConverter(new ReactiveStreamsToReactorConverter()); + conversionService.addConverter(new ReactiveStreamsToRxJava1Converter()); + SimpleHandlerResultHandler resultHandler = new SimpleHandlerResultHandler(conversionService); + TestController controller = new TestController(); + + HandlerMethod hm = new HandlerMethod(controller, TestController.class.getMethod("voidReturnValue")); + ResolvableType type = ResolvableType.forMethodParameter(hm.getReturnType()); + assertTrue(resultHandler.supports(new HandlerResult(hm, null, type))); + + hm = new HandlerMethod(controller, TestController.class.getMethod("publisherString")); + type = ResolvableType.forMethodParameter(hm.getReturnType()); + assertFalse(resultHandler.supports(new HandlerResult(hm, null, type))); + + hm = new HandlerMethod(controller, TestController.class.getMethod("publisherVoid")); + type = ResolvableType.forMethodParameter(hm.getReturnType()); + assertTrue(resultHandler.supports(new HandlerResult(hm, null, type))); + + hm = new HandlerMethod(controller, TestController.class.getMethod("streamVoid")); + type = ResolvableType.forMethodParameter(hm.getReturnType()); + assertTrue(resultHandler.supports(new HandlerResult(hm, null, type))); + + hm = new HandlerMethod(controller, TestController.class.getMethod("observableVoid")); + type = ResolvableType.forMethodParameter(hm.getReturnType()); + assertTrue(resultHandler.supports(new HandlerResult(hm, null, type))); + + hm = new HandlerMethod(controller, TestController.class.getMethod("completableFutureVoid")); + type = ResolvableType.forMethodParameter(hm.getReturnType()); + assertTrue(resultHandler.supports(new HandlerResult(hm, null, type))); } @SuppressWarnings("unused") private static class TestController { - public Publisher voidReturnValue() { - return null; + public void voidReturnValue() { } public Publisher publisherString() { @@ -64,6 +119,18 @@ public class SimpleHandlerResultHandlerTests { public Publisher publisherVoid() { return null; } + + public Stream streamVoid() { + return null; + } + + public Observable observableVoid() { + return null; + } + + public CompletableFuture completableFutureVoid() { + return null; + } } } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java index 98210fbeb9..c5c4bd5eed 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java @@ -197,16 +197,18 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati } @Test - public void create() throws Exception { - RestTemplate restTemplate = new RestTemplate(); - URI url = new URI("http://localhost:" + this.port + "/create"); - RequestEntity> request = RequestEntity.post(url) - .contentType(MediaType.APPLICATION_JSON) - .body(Arrays.asList(new Person("Robert"), new Person("Marie"))); - ResponseEntity response = restTemplate.exchange(request, Void.class); + public void publisherCreate() throws Exception { + create("http://localhost:" + this.port + "/publisher-create"); + } - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertEquals(2, this.wac.getBean(TestController.class).persons.size()); + @Test + public void streamCreate() throws Exception { + create("http://localhost:" + this.port + "/stream-create"); + } + + @Test + public void observableCreate() throws Exception { + create("http://localhost:" + this.port + "/observable-create"); } @@ -259,6 +261,18 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati assertEquals("MARIE", results.get(1).getName()); } + private void create(String requestUrl) throws Exception { + RestTemplate restTemplate = new RestTemplate(); + URI url = new URI(requestUrl); + RequestEntity> request = RequestEntity.post(url) + .contentType(MediaType.APPLICATION_JSON) + .body(Arrays.asList(new Person("Robert"), new Person("Marie"))); + ResponseEntity response = restTemplate.exchange(request, Void.class); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals(2, this.wac.getBean(TestController.class).persons.size()); + } + @Configuration @SuppressWarnings("unused") @@ -295,7 +309,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @Bean public SimpleHandlerResultHandler simpleHandlerResultHandler() { - return new SimpleHandlerResultHandler(); + return new SimpleHandlerResultHandler(conversionService()); } } @@ -448,11 +462,21 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati }); } - @RequestMapping("/create") - public Publisher create(@RequestBody Stream personStream) { + @RequestMapping("/publisher-create") + public Publisher publisherCreate(@RequestBody Publisher personStream) { + return Streams.wrap(personStream).toList().onSuccess(persons::addAll).after(); + } + + @RequestMapping("/stream-create") + public Promise streamCreate(@RequestBody Stream personStream) { return personStream.toList().onSuccess(persons::addAll).after(); } + @RequestMapping("/observable-create") + public Observable observableCreate(@RequestBody Observable personStream) { + return personStream.toList().doOnNext(p -> persons.addAll(p)).flatMap(document -> Observable.empty()); + } + //TODO add mixed and T request mappings tests } From b26e74612036eddddf8603e1bf1e3ff1fb844c82 Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Tue, 1 Dec 2015 10:43:41 +0000 Subject: [PATCH 102/498] Use Publishers.zip instead of Streams.zip --- .../web/reactive/method/InvocableHandlerMethod.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java index b996faf811..d7585a6619 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java @@ -26,7 +26,6 @@ import java.util.Optional; import org.reactivestreams.Publisher; import reactor.Publishers; import reactor.fn.tuple.Tuple; -import reactor.rx.Streams; import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.GenericTypeResolver; @@ -64,7 +63,7 @@ public class InvocableHandlerMethod extends HandlerMethod { List> argPublishers = getMethodArguments(request, providedArgs); Publisher argValues = (!argPublishers.isEmpty() ? - Streams.zip(argPublishers, this::unwrapOptionalArgValues) : + Publishers.zip(argPublishers, this::unwrapOptionalArgValues) : Publishers.just(new Object[0])); return Publishers.map(argValues, args -> { From 71d1d11fac70cb9415e8a10a6add34988d0a1df6 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 25 Nov 2015 17:28:51 -0500 Subject: [PATCH 103/498] Non-blocking HandlerMapping chain --- .../web/reactive/DispatcherHandler.java | 42 +++++++------ .../reactive/HandlerNotFoundException.java | 59 +++++++++++++++++++ .../handler/SimpleUrlHandlerMapping.java | 12 +++- .../RequestMappingHandlerMapping.java | 27 +++++---- ...mpleUrlHandlerMappingIntegrationTests.java | 22 +++++++ 5 files changed, 128 insertions(+), 34 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerNotFoundException.java diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java index 3070db7f27..917a1adb15 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java @@ -19,6 +19,7 @@ package org.springframework.web.reactive; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -30,7 +31,6 @@ import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.core.annotation.AnnotationAwareOrderComparator; -import org.springframework.http.HttpStatus; import org.springframework.http.server.ReactiveHttpHandler; import org.springframework.http.server.ReactiveServerHttpRequest; import org.springframework.http.server.ReactiveServerHttpResponse; @@ -76,6 +76,7 @@ public class DispatcherHandler implements ReactiveHttpHandler, ApplicationContex context, HandlerMapping.class, true, false); this.handlerMappings = new ArrayList<>(mappingBeans.values()); + this.handlerMappings.add(new NotFoundHandlerMapping()); AnnotationAwareOrderComparator.sort(this.handlerMappings); Map adapterBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors( @@ -98,13 +99,9 @@ public class DispatcherHandler implements ReactiveHttpHandler, ApplicationContex logger.debug("Processing " + request.getMethod() + " request for [" + request.getURI() + "]"); } - Publisher handlerPublisher = getHandler(request); - if (handlerPublisher == null) { - // No exception handling mechanism yet - response.setStatusCode(HttpStatus.NOT_FOUND); - response.writeHeaders(); - return Publishers.empty(); - } + Publisher mappings = Publishers.from(this.handlerMappings); + Publisher handlerPublisher = Publishers.concatMap(mappings, m -> m.getHandler(request)); + handlerPublisher = first(handlerPublisher); Publisher resultPublisher = Publishers.concatMap(handlerPublisher, handler -> { HandlerAdapter handlerAdapter = getHandlerAdapter(handler); @@ -117,16 +114,6 @@ public class DispatcherHandler implements ReactiveHttpHandler, ApplicationContex }); } - protected Publisher getHandler(ReactiveServerHttpRequest request) { - for (HandlerMapping handlerMapping : this.handlerMappings) { - Publisher handlerPublisher = handlerMapping.getHandler(request); - if (handlerPublisher != null) { - return handlerPublisher; - } - } - return null; - } - protected HandlerAdapter getHandlerAdapter(Object handler) { for (HandlerAdapter handlerAdapter : this.handlerAdapters) { if (handlerAdapter.supports(handler)) { @@ -145,4 +132,21 @@ public class DispatcherHandler implements ReactiveHttpHandler, ApplicationContex throw new IllegalStateException("No HandlerResultHandler: " + handlerResult.getValue()); } -} + + private static Publisher first(Publisher source) { + return Publishers.lift(source, (e, subscriber) -> { + subscriber.onNext(e); + subscriber.onComplete(); + }); + } + + private static class NotFoundHandlerMapping implements HandlerMapping { + + @Override + public Publisher getHandler(ReactiveServerHttpRequest request) { + return Publishers.error(new HandlerNotFoundException(request.getMethod(), + request.getURI().getPath(), request.getHeaders())); + } + } + +} \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerNotFoundException.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerNotFoundException.java new file mode 100644 index 0000000000..d8bc40f933 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerNotFoundException.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2015 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.web.reactive; + +import org.springframework.core.NestedRuntimeException; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; + +/** + * @author Rossen Stoyanchev + */ +public class HandlerNotFoundException extends NestedRuntimeException { + + private final HttpMethod method; + + private final String requestURL; + + private final HttpHeaders headers; + + + /** + * Constructor for NoHandlerFoundException. + * @param method the HTTP method + * @param requestURL the HTTP request URL + * @param headers the HTTP request headers + */ + public HandlerNotFoundException(HttpMethod method, String requestURL, HttpHeaders headers) { + super("No handler found for " + method + " " + requestURL); + this.method = method; + this.requestURL = requestURL; + this.headers = headers; + } + + + public HttpMethod getMethod() { + return this.method; + } + + public String getRequestURL() { + return this.requestURL; + } + + public HttpHeaders getHeaders() { + return this.headers; + } +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMapping.java index 1eac42a113..986c7498ef 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMapping.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMapping.java @@ -21,6 +21,7 @@ import java.util.Map; import org.reactivestreams.Publisher; import reactor.Publishers; +import reactor.core.publisher.PublisherFactory; import org.springframework.http.server.ReactiveServerHttpRequest; import org.springframework.web.reactive.HandlerMapping; @@ -43,9 +44,14 @@ public class SimpleUrlHandlerMapping implements HandlerMapping { @Override public Publisher getHandler(ReactiveServerHttpRequest request) { - String path = request.getURI().getPath(); - Object handler = this.handlerMap.get(path); - return (handler != null ? Publishers.just(handler) : null); + return PublisherFactory.create(subscriber -> { + String path = request.getURI().getPath(); + Object handler = this.handlerMap.get(path); + if (handler != null) { + subscriber.onNext(handler); + } + subscriber.onComplete(); + }); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMapping.java index ed9670b3e8..86e87f7072 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMapping.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMapping.java @@ -28,19 +28,19 @@ import java.util.TreeSet; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.reactivestreams.Publisher; -import reactor.Publishers; +import reactor.core.publisher.PublisherFactory; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.http.server.ReactiveServerHttpRequest; -import org.springframework.web.reactive.HandlerMapping; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.method.HandlerMethod; import org.springframework.web.method.HandlerMethodSelector; +import org.springframework.web.reactive.HandlerMapping; /** @@ -95,18 +95,21 @@ public class RequestMappingHandlerMapping implements HandlerMapping, @Override public Publisher getHandler(ReactiveServerHttpRequest request) { - for (Map.Entry entry : this.methodMap.entrySet()) { - RequestMappingInfo info = entry.getKey(); - if (info.matchesRequest(request)) { - HandlerMethod handlerMethod = entry.getValue(); - if (logger.isDebugEnabled()) { - logger.debug("Mapped " + request.getMethod() + " " + - request.getURI().getPath() + " to [" + handlerMethod + "]"); + return PublisherFactory.create(subscriber -> { + for (Map.Entry entry : this.methodMap.entrySet()) { + RequestMappingInfo info = entry.getKey(); + if (info.matchesRequest(request)) { + HandlerMethod handlerMethod = entry.getValue(); + if (logger.isDebugEnabled()) { + logger.debug("Mapped " + request.getMethod() + " " + + request.getURI().getPath() + " to [" + handlerMethod + "]"); + } + subscriber.onNext(handlerMethod); + break; } - return Publishers.just(handlerMethod); } - } - return null; + subscriber.onComplete(); + }); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java index 94bf276d1d..73427f772a 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java @@ -21,6 +21,7 @@ import java.nio.charset.Charset; import java.util.HashMap; import java.util.Map; +import org.junit.Ignore; import org.junit.Test; import org.reactivestreams.Publisher; import reactor.io.buffer.Buffer; @@ -31,11 +32,14 @@ import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; import org.springframework.http.server.ReactiveServerHttpRequest; import org.springframework.http.server.ReactiveServerHttpResponse; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestClientException; import org.springframework.web.reactive.DispatcherHandler; import org.springframework.http.server.AbstractHttpHandlerIntegrationTests; import org.springframework.http.server.ReactiveHttpHandler; import org.springframework.web.client.RestTemplate; import org.springframework.web.context.support.StaticWebApplicationContext; +import org.springframework.web.reactive.method.annotation.RequestMappingHandlerMapping; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; @@ -89,6 +93,24 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler assertArrayEquals("bar".getBytes(UTF_8), response.getBody()); } + // TODO: remove @Ignore after 404 default handling + + @Test + @Ignore + public void testNotFound() throws Exception { + + RestTemplate restTemplate = new RestTemplate(); + + URI url = new URI("http://localhost:" + port + "/oops"); + RequestEntity request = RequestEntity.get(url).build(); + try { + restTemplate.exchange(request, byte[].class); + } + catch (HttpClientErrorException ex) { + assertEquals(HttpStatus.NOT_FOUND, ex.getStatusCode()); + } + } + private static class TestHandlerMapping extends SimpleUrlHandlerMapping { From 9ce5c7416bf3cda842121296a0bca4fa6e71fe06 Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Tue, 1 Dec 2015 15:01:05 +0000 Subject: [PATCH 104/498] compiler issue ? --- .../web/reactive/method/InvocableHandlerMethod.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java index d7585a6619..317c3cf063 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java @@ -63,7 +63,7 @@ public class InvocableHandlerMethod extends HandlerMethod { List> argPublishers = getMethodArguments(request, providedArgs); Publisher argValues = (!argPublishers.isEmpty() ? - Publishers.zip(argPublishers, this::unwrapOptionalArgValues) : + Publishers.zip(argPublishers, this::unwrapOptionalArgValues) : Publishers.just(new Object[0])); return Publishers.map(argValues, args -> { From b9a52d5f7c454ab7de0bc9eddc8e35c7c1f51906 Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Tue, 1 Dec 2015 16:51:52 +0000 Subject: [PATCH 105/498] Change to Definitive generic signature for Publishers.zip --- .../web/reactive/method/InvocableHandlerMethod.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java index 317c3cf063..d7585a6619 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java @@ -63,7 +63,7 @@ public class InvocableHandlerMethod extends HandlerMethod { List> argPublishers = getMethodArguments(request, providedArgs); Publisher argValues = (!argPublishers.isEmpty() ? - Publishers.zip(argPublishers, this::unwrapOptionalArgValues) : + Publishers.zip(argPublishers, this::unwrapOptionalArgValues) : Publishers.just(new Object[0])); return Publishers.map(argValues, args -> { From df80ffbf2814c8f0a95bce9095921d53f2786a11 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 1 Dec 2015 18:09:53 -0500 Subject: [PATCH 106/498] Spring-consistent naming in server.http package This change also removes reactor-stream variants of the request and response since the request and response aren't used directly by application code and get passed through reactor.Publishers anyway. --- ...er.java => HttpHandlerChannelHandler.java} | 19 ++--- .../PublisherReactorServerHttpRequest.java | 81 ------------------ .../PublisherReactorServerHttpResponse.java | 85 ------------------- .../reactor/ReactorServerHttpRequest.java | 55 ++++++++++-- .../reactor/ReactorServerHttpResponse.java | 58 +++++++++++-- ...er.java => HttpHandlerRequestHandler.java} | 4 +- ...erAdapter.java => HttpHandlerServlet.java} | 4 +- .../http/server/support/JettyHttpServer.java | 4 +- .../server/support/ReactorHttpServer.java | 6 +- .../server/support/RxNettyHttpServer.java | 6 +- .../http/server/support/TomcatHttpServer.java | 4 +- .../server/support/UndertowHttpServer.java | 4 +- ...apter.java => HttpHandlerHttpHandler.java} | 6 +- 13 files changed, 121 insertions(+), 215 deletions(-) rename spring-web-reactive/src/main/java/org/springframework/http/server/reactor/{ReactorHttpHandlerAdapter.java => HttpHandlerChannelHandler.java} (69%) delete mode 100644 spring-web-reactive/src/main/java/org/springframework/http/server/reactor/PublisherReactorServerHttpRequest.java delete mode 100644 spring-web-reactive/src/main/java/org/springframework/http/server/reactor/PublisherReactorServerHttpResponse.java rename spring-web-reactive/src/main/java/org/springframework/http/server/rxnetty/{RxNettyHttpHandlerAdapter.java => HttpHandlerRequestHandler.java} (93%) rename spring-web-reactive/src/main/java/org/springframework/http/server/servlet31/{Servlet31HttpHandlerAdapter.java => HttpHandlerServlet.java} (95%) rename spring-web-reactive/src/main/java/org/springframework/http/server/undertow/{UndertowHttpHandlerAdapter.java => HttpHandlerHttpHandler.java} (91%) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactor/ReactorHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactor/HttpHandlerChannelHandler.java similarity index 69% rename from spring-web-reactive/src/main/java/org/springframework/http/server/reactor/ReactorHttpHandlerAdapter.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/reactor/HttpHandlerChannelHandler.java index e85564f92e..b77a7e5840 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactor/ReactorHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactor/HttpHandlerChannelHandler.java @@ -16,7 +16,6 @@ package org.springframework.http.server.reactor; import org.reactivestreams.Publisher; -import reactor.core.publisher.convert.DependencyUtils; import reactor.io.buffer.Buffer; import reactor.io.net.ReactiveChannelHandler; import reactor.io.net.http.HttpChannel; @@ -27,30 +26,22 @@ import org.springframework.util.Assert; /** * @author Stephane Maldini */ -public class ReactorHttpHandlerAdapter +public class HttpHandlerChannelHandler implements ReactiveChannelHandler> { private final ReactiveHttpHandler httpHandler; - public ReactorHttpHandlerAdapter(ReactiveHttpHandler httpHandler) { + public HttpHandlerChannelHandler(ReactiveHttpHandler httpHandler) { Assert.notNull(httpHandler, "'httpHandler' is required."); this.httpHandler = httpHandler; } @Override public Publisher apply(HttpChannel channel) { - final PublisherReactorServerHttpRequest adaptedRequest; - final PublisherReactorServerHttpResponse adaptedResponse; - - if(DependencyUtils.hasReactorStream()){ - adaptedRequest = new ReactorServerHttpRequest(channel); - adaptedResponse = new ReactorServerHttpResponse(channel); - } - else{ - adaptedRequest = new PublisherReactorServerHttpRequest(channel); - adaptedResponse = new PublisherReactorServerHttpResponse(channel); - } + ReactorServerHttpRequest adaptedRequest = new ReactorServerHttpRequest(channel); + ReactorServerHttpResponse adaptedResponse = new ReactorServerHttpResponse(channel); return this.httpHandler.handle(adaptedRequest, adaptedResponse); } + } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactor/PublisherReactorServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactor/PublisherReactorServerHttpRequest.java deleted file mode 100644 index 16d883089f..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactor/PublisherReactorServerHttpRequest.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2002-2015 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.server.reactor; - -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.ByteBuffer; - -import org.reactivestreams.Publisher; -import reactor.Publishers; -import reactor.io.buffer.Buffer; -import reactor.io.net.http.HttpChannel; - -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.server.ReactiveServerHttpRequest; -import org.springframework.util.Assert; - -/** - * @author Stephane Maldini - */ -public class PublisherReactorServerHttpRequest implements ReactiveServerHttpRequest { - - private final HttpChannel channel; - - private HttpHeaders headers; - - - public PublisherReactorServerHttpRequest(HttpChannel request) { - Assert.notNull("'request', request must not be null."); - this.channel = request; - } - - - @Override - public HttpHeaders getHeaders() { - if (this.headers == null) { - this.headers = new HttpHeaders(); - for (String name : this.channel.headers().names()) { - for (String value : this.channel.headers().getAll(name)) { - this.headers.add(name, value); - } - } - } - return this.headers; - } - - @Override - public HttpMethod getMethod() { - return HttpMethod.valueOf(this.channel.method().getName()); - } - - @Override - public URI getURI() { - try { - return new URI(this.channel.uri()); - } catch (URISyntaxException ex) { - throw new IllegalStateException("Could not get URI: " + ex.getMessage(), ex); - } - - } - - @Override - public Publisher getBody() { - return Publishers.map(channel.input(), Buffer::byteBuffer); - } - -} diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactor/PublisherReactorServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactor/PublisherReactorServerHttpResponse.java deleted file mode 100644 index ad7193f528..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactor/PublisherReactorServerHttpResponse.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2002-2015 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.server.reactor; - -import java.nio.ByteBuffer; - -import org.reactivestreams.Publisher; -import reactor.Publishers; -import reactor.io.buffer.Buffer; -import reactor.io.net.http.HttpChannel; -import reactor.io.net.http.model.Status; - -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.server.ReactiveServerHttpResponse; -import org.springframework.util.Assert; - -/** - * @author Stephane Maldini - */ -public class PublisherReactorServerHttpResponse implements ReactiveServerHttpResponse { - - private final HttpChannel channel; - - private final HttpHeaders headers; - - private boolean headersWritten = false; - - - public PublisherReactorServerHttpResponse(HttpChannel response) { - Assert.notNull("'response', response must not be null."); - this.channel = response; - this.headers = new HttpHeaders(); - } - - - @Override - public void setStatusCode(HttpStatus status) { - this.channel.responseStatus(Status.valueOf(status.value())); - } - - @Override - public HttpHeaders getHeaders() { - return (this.headersWritten ? HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers); - } - - @Override - public Publisher writeHeaders() { - if (this.headersWritten) { - return Publishers.empty(); - } - applyHeaders(); - return this.channel.writeHeaders(); - } - - @Override - public Publisher setBody(Publisher contentPublisher) { - applyHeaders(); - return this.channel.writeWith(Publishers.map(contentPublisher, Buffer::new)); - } - - private void applyHeaders() { - if (!this.headersWritten) { - for (String name : this.headers.keySet()) { - for (String value : this.headers.get(name)) { - this.channel.responseHeaders().add(name, value); - } - } - this.headersWritten = true; - } - } -} diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactor/ReactorServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactor/ReactorServerHttpRequest.java index 71fff0002b..6d5667b6db 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactor/ReactorServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactor/ReactorServerHttpRequest.java @@ -13,28 +13,69 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.springframework.http.server.reactor; +import java.net.URI; +import java.net.URISyntaxException; import java.nio.ByteBuffer; +import org.reactivestreams.Publisher; +import reactor.Publishers; import reactor.io.buffer.Buffer; import reactor.io.net.http.HttpChannel; -import reactor.rx.Stream; -import reactor.rx.Streams; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.server.ReactiveServerHttpRequest; +import org.springframework.util.Assert; /** * @author Stephane Maldini */ -public class ReactorServerHttpRequest extends PublisherReactorServerHttpRequest { +public class ReactorServerHttpRequest implements ReactiveServerHttpRequest { + + private final HttpChannel channel; + + private HttpHeaders headers; + public ReactorServerHttpRequest(HttpChannel request) { - super(request); + Assert.notNull("'request', request must not be null."); + this.channel = request; + } + + + @Override + public HttpHeaders getHeaders() { + if (this.headers == null) { + this.headers = new HttpHeaders(); + for (String name : this.channel.headers().names()) { + for (String value : this.channel.headers().getAll(name)) { + this.headers.add(name, value); + } + } + } + return this.headers; } @Override - public Stream getBody() { - return Streams.wrap(super.getBody()); + public HttpMethod getMethod() { + return HttpMethod.valueOf(this.channel.method().getName()); + } + + @Override + public URI getURI() { + try { + return new URI(this.channel.uri()); + } catch (URISyntaxException ex) { + throw new IllegalStateException("Could not get URI: " + ex.getMessage(), ex); + } + + } + + @Override + public Publisher getBody() { + return Publishers.map(this.channel.input(), Buffer::byteBuffer); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactor/ReactorServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactor/ReactorServerHttpResponse.java index ece8e23cd2..d42992f3b8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactor/ReactorServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactor/ReactorServerHttpResponse.java @@ -13,33 +13,73 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.springframework.http.server.reactor; import java.nio.ByteBuffer; import org.reactivestreams.Publisher; +import reactor.Publishers; import reactor.io.buffer.Buffer; import reactor.io.net.http.HttpChannel; -import reactor.rx.Stream; -import reactor.rx.Streams; +import reactor.io.net.http.model.Status; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.server.ReactiveServerHttpResponse; +import org.springframework.util.Assert; /** * @author Stephane Maldini */ -public class ReactorServerHttpResponse extends PublisherReactorServerHttpResponse { +public class ReactorServerHttpResponse implements ReactiveServerHttpResponse { + + private final HttpChannel channel; + + private final HttpHeaders headers; + + private boolean headersWritten = false; + public ReactorServerHttpResponse(HttpChannel response) { - super(response); + Assert.notNull("'response', response must not be null."); + this.channel = response; + this.headers = new HttpHeaders(); + } + + + @Override + public void setStatusCode(HttpStatus status) { + this.channel.responseStatus(Status.valueOf(status.value())); } @Override - public Stream writeHeaders() { - return Streams.wrap(super.writeHeaders()); + public HttpHeaders getHeaders() { + return (this.headersWritten ? HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers); } @Override - public Stream setBody(Publisher contentPublisher) { - return Streams.wrap(super.setBody(contentPublisher)); + public Publisher writeHeaders() { + if (this.headersWritten) { + return Publishers.empty(); + } + applyHeaders(); + return this.channel.writeHeaders(); + } + + @Override + public Publisher setBody(Publisher contentPublisher) { + applyHeaders(); + return this.channel.writeWith(Publishers.map(contentPublisher, Buffer::new)); + } + + private void applyHeaders() { + if (!this.headersWritten) { + for (String name : this.headers.keySet()) { + for (String value : this.headers.get(name)) { + this.channel.responseHeaders().add(name, value); + } + } + this.headersWritten = true; + } } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/rxnetty/RxNettyHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/rxnetty/HttpHandlerRequestHandler.java similarity index 93% rename from spring-web-reactive/src/main/java/org/springframework/http/server/rxnetty/RxNettyHttpHandlerAdapter.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/rxnetty/HttpHandlerRequestHandler.java index 3d6dde0e48..07866c3f87 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/rxnetty/RxNettyHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/rxnetty/HttpHandlerRequestHandler.java @@ -30,12 +30,12 @@ import org.springframework.util.Assert; /** * @author Rossen Stoyanchev */ -public class RxNettyHttpHandlerAdapter implements RequestHandler { +public class HttpHandlerRequestHandler implements RequestHandler { private final ReactiveHttpHandler httpHandler; - public RxNettyHttpHandlerAdapter(ReactiveHttpHandler httpHandler) { + public HttpHandlerRequestHandler(ReactiveHttpHandler httpHandler) { Assert.notNull(httpHandler, "'httpHandler' is required."); this.httpHandler = httpHandler; } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/servlet31/Servlet31HttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/servlet31/HttpHandlerServlet.java similarity index 95% rename from spring-web-reactive/src/main/java/org/springframework/http/server/servlet31/Servlet31HttpHandlerAdapter.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/servlet31/HttpHandlerServlet.java index 4766809261..00945747af 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/servlet31/Servlet31HttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/servlet31/HttpHandlerServlet.java @@ -37,11 +37,11 @@ import org.springframework.http.server.ReactiveHttpHandler; * @author Rossen Stoyanchev */ @WebServlet(asyncSupported = true) -public class Servlet31HttpHandlerAdapter extends HttpServlet { +public class HttpHandlerServlet extends HttpServlet { private static final int BUFFER_SIZE = 8192; - private static Log logger = LogFactory.getLog(Servlet31HttpHandlerAdapter.class); + private static Log logger = LogFactory.getLog(HttpHandlerServlet.class); private ReactiveHttpHandler handler; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/support/JettyHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/http/server/support/JettyHttpServer.java index 38cc7d1a83..43bdaf62cf 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/support/JettyHttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/support/JettyHttpServer.java @@ -24,7 +24,7 @@ import org.eclipse.jetty.servlet.ServletHolder; import org.springframework.beans.factory.InitializingBean; import org.springframework.util.Assert; import org.springframework.util.SocketUtils; -import org.springframework.http.server.servlet31.Servlet31HttpHandlerAdapter; +import org.springframework.http.server.servlet31.HttpHandlerServlet; /** * @author Rossen Stoyanchev @@ -51,7 +51,7 @@ public class JettyHttpServer extends HttpServerSupport implements InitializingBe this.jettyServer = new Server(); Assert.notNull(getHttpHandler()); - Servlet31HttpHandlerAdapter servlet = new Servlet31HttpHandlerAdapter(); + HttpHandlerServlet servlet = new HttpHandlerServlet(); servlet.setHandler(getHttpHandler()); ServletHolder servletHolder = new ServletHolder(servlet); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/support/ReactorHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/http/server/support/ReactorHttpServer.java index 868f0f53a6..91245e1ed9 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/support/ReactorHttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/support/ReactorHttpServer.java @@ -21,7 +21,7 @@ import reactor.io.net.ReactiveNet; import org.springframework.beans.factory.InitializingBean; import org.springframework.util.Assert; -import org.springframework.http.server.reactor.ReactorHttpHandlerAdapter; +import org.springframework.http.server.reactor.HttpHandlerChannelHandler; /** * @author Stephane Maldini @@ -29,7 +29,7 @@ import org.springframework.http.server.reactor.ReactorHttpHandlerAdapter; public class ReactorHttpServer extends HttpServerSupport implements InitializingBean, HttpServer { - private ReactorHttpHandlerAdapter reactorHandler; + private HttpHandlerChannelHandler reactorHandler; private reactor.io.net.http.HttpServer reactorServer; @@ -44,7 +44,7 @@ public class ReactorHttpServer extends HttpServerSupport public void afterPropertiesSet() throws Exception { Assert.notNull(getHttpHandler()); - this.reactorHandler = new ReactorHttpHandlerAdapter(getHttpHandler()); + this.reactorHandler = new HttpHandlerChannelHandler(getHttpHandler()); this.reactorServer = (getPort() != -1 ? ReactiveNet.httpServer(getPort()) : ReactiveNet.httpServer()); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/support/RxNettyHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/http/server/support/RxNettyHttpServer.java index fe0bf6323d..f71859cfab 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/support/RxNettyHttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/support/RxNettyHttpServer.java @@ -20,7 +20,7 @@ import io.netty.buffer.ByteBuf; import org.springframework.beans.factory.InitializingBean; import org.springframework.util.Assert; -import org.springframework.http.server.rxnetty.RxNettyHttpHandlerAdapter; +import org.springframework.http.server.rxnetty.HttpHandlerRequestHandler; /** @@ -28,7 +28,7 @@ import org.springframework.http.server.rxnetty.RxNettyHttpHandlerAdapter; */ public class RxNettyHttpServer extends HttpServerSupport implements InitializingBean, HttpServer { - private RxNettyHttpHandlerAdapter rxNettyHandler; + private HttpHandlerRequestHandler rxNettyHandler; private io.reactivex.netty.protocol.http.server.HttpServer rxNettyServer; @@ -45,7 +45,7 @@ public class RxNettyHttpServer extends HttpServerSupport implements Initializing public void afterPropertiesSet() throws Exception { Assert.notNull(getHttpHandler()); - this.rxNettyHandler = new RxNettyHttpHandlerAdapter(getHttpHandler()); + this.rxNettyHandler = new HttpHandlerRequestHandler(getHttpHandler()); this.rxNettyServer = (getPort() != -1 ? io.reactivex.netty.protocol.http.server.HttpServer.newServer(getPort()) : diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/support/TomcatHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/http/server/support/TomcatHttpServer.java index fa4d989480..ed73f3d31e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/support/TomcatHttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/support/TomcatHttpServer.java @@ -25,7 +25,7 @@ import org.apache.catalina.startup.Tomcat; import org.springframework.beans.factory.InitializingBean; import org.springframework.util.Assert; import org.springframework.util.SocketUtils; -import org.springframework.http.server.servlet31.Servlet31HttpHandlerAdapter; +import org.springframework.http.server.servlet31.HttpHandlerServlet; /** @@ -54,7 +54,7 @@ public class TomcatHttpServer extends HttpServerSupport implements InitializingB this.tomcatServer.setPort(getPort()); Assert.notNull(getHttpHandler()); - Servlet31HttpHandlerAdapter servlet = new Servlet31HttpHandlerAdapter(); + HttpHandlerServlet servlet = new HttpHandlerServlet(); servlet.setHandler(getHttpHandler()); File base = new File(System.getProperty("java.io.tmpdir")); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/support/UndertowHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/http/server/support/UndertowHttpServer.java index 8a0cb5b40a..cc267f4c55 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/support/UndertowHttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/support/UndertowHttpServer.java @@ -18,7 +18,7 @@ package org.springframework.http.server.support; import org.springframework.beans.factory.InitializingBean; import org.springframework.util.Assert; -import org.springframework.http.server.undertow.UndertowHttpHandlerAdapter; +import org.springframework.http.server.undertow.HttpHandlerHttpHandler; import io.undertow.Undertow; import io.undertow.server.HttpHandler; @@ -36,7 +36,7 @@ public class UndertowHttpServer extends HttpServerSupport implements Initializin @Override public void afterPropertiesSet() throws Exception { Assert.notNull(getHttpHandler()); - HttpHandler handler = new UndertowHttpHandlerAdapter(getHttpHandler()); + HttpHandler handler = new HttpHandlerHttpHandler(getHttpHandler()); int port = (getPort() != -1 ? getPort() : 8080); this.server = Undertow.builder().addHttpListener(port, "localhost") .setHandler(handler).build(); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/undertow/UndertowHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/undertow/HttpHandlerHttpHandler.java similarity index 91% rename from spring-web-reactive/src/main/java/org/springframework/http/server/undertow/UndertowHttpHandlerAdapter.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/undertow/HttpHandlerHttpHandler.java index 040e208765..1f9cb09076 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/undertow/UndertowHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/undertow/HttpHandlerHttpHandler.java @@ -32,15 +32,15 @@ import org.reactivestreams.Subscription; * @author Marek Hawrylczak * @author Rossen Stoyanchev */ -public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandler { +public class HttpHandlerHttpHandler implements io.undertow.server.HttpHandler { - private static Log logger = LogFactory.getLog(UndertowHttpHandlerAdapter.class); + private static Log logger = LogFactory.getLog(HttpHandlerHttpHandler.class); private final ReactiveHttpHandler delegate; - public UndertowHttpHandlerAdapter(ReactiveHttpHandler delegate) { + public HttpHandlerHttpHandler(ReactiveHttpHandler delegate) { Assert.notNull(delegate, "'delegate' is required."); this.delegate = delegate; } From 382c98f9680fab9a0c2ad50de18e3385f9cba5c1 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 1 Dec 2015 18:19:14 -0500 Subject: [PATCH 107/498] Remove isolated use of Reactor Buffer --- .../http/server/servlet31/ResponseBodySubscriber.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/servlet31/ResponseBodySubscriber.java b/spring-web-reactive/src/main/java/org/springframework/http/server/servlet31/ResponseBodySubscriber.java index 75b49a2866..a2e82bc076 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/servlet31/ResponseBodySubscriber.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/servlet31/ResponseBodySubscriber.java @@ -25,7 +25,6 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; -import reactor.io.buffer.Buffer; import org.springframework.util.Assert; @@ -88,9 +87,10 @@ public class ResponseBodySubscriber implements WriteListener, Subscriber Date: Wed, 2 Dec 2015 17:46:07 -0500 Subject: [PATCH 108/498] Move http.server to http.server.reactive --- .../FilterChainHttpHandler.java | 22 +- .../HttpFilter.java} | 8 +- .../HttpFilterChain.java} | 9 +- .../HttpHandler.java} | 10 +- .../ReactorHttpHandlerAdapter.java} | 12 +- .../ReactorServerHttpRequest.java | 6 +- .../ReactorServerHttpResponse.java | 6 +- .../RxNettyHttpHandlerAdapter.java} | 12 +- .../RxNettyServerHttpRequest.java | 6 +- .../RxNettyServerHttpResponse.java | 6 +- .../ServerHttpRequest.java} | 4 +- .../ServerHttpResponse.java} | 4 +- .../ServletAsyncContextSynchronizer.java} | 6 +- .../ServletHttpHandlerAdapter.java} | 33 +- .../reactive/ServletServerHttpRequest.java | 321 ++++++++++++++++++ .../ServletServerHttpResponse.java} | 110 +++++- .../UndertowHttpHandlerAdapter.java} | 19 +- .../reactive/UndertowServerHttpRequest.java | 298 ++++++++++++++++ .../reactive/UndertowServerHttpResponse.java | 267 +++++++++++++++ .../boot}/HttpServer.java | 6 +- .../boot}/HttpServerSupport.java | 10 +- .../boot}/JettyHttpServer.java | 6 +- .../boot}/ReactorHttpServer.java | 8 +- .../boot}/RxNettyHttpServer.java | 8 +- .../boot}/TomcatHttpServer.java | 6 +- .../boot}/UndertowHttpServer.java | 6 +- .../boot}/package-info.java | 2 +- .../servlet31/RequestBodyPublisher.java | 218 ------------ .../servlet31/ResponseBodySubscriber.java | 112 ------ .../servlet31/Servlet31ServerHttpRequest.java | 122 ------- .../server/undertow/RequestBodyPublisher.java | 248 -------------- .../undertow/ResponseBodySubscriber.java | 204 ----------- .../undertow/UndertowServerHttpRequest.java | 85 ----- .../undertow/UndertowServerHttpResponse.java | 98 ------ .../web/reactive/DispatcherHandler.java | 13 +- .../web/reactive/HandlerAdapter.java | 8 +- .../web/reactive/HandlerMapping.java | 4 +- .../web/reactive/HandlerResultHandler.java | 6 +- .../reactive/handler/HttpHandlerAdapter.java | 16 +- .../handler/SimpleHandlerResultHandler.java | 8 +- .../handler/SimpleUrlHandlerMapping.java | 5 +- .../method/HandlerMethodArgumentResolver.java | 4 +- .../method/InvocableHandlerMethod.java | 6 +- .../RequestBodyArgumentResolver.java | 4 +- .../RequestMappingHandlerAdapter.java | 8 +- .../RequestMappingHandlerMapping.java | 6 +- .../RequestParamArgumentResolver.java | 4 +- .../annotation/ResponseBodyResultHandler.java | 10 +- .../AbstractHttpHandlerIntegrationTests.java | 15 +- .../http/server/EchoHandler.java | 8 +- .../server/FilterChainHttpHandlerTests.java | 33 +- .../http/server/RandomHandler.java | 8 +- .../http/server/XmlHandler.java | 9 +- .../server/XmlHandlerIntegrationTests.java | 3 +- .../AsyncContextSynchronizerTests.java | 6 +- ...mpleUrlHandlerMappingIntegrationTests.java | 18 +- .../RequestMappingHandlerMappingTests.java | 8 +- .../RequestMappingIntegrationTests.java | 4 +- 58 files changed, 1204 insertions(+), 1308 deletions(-) rename spring-web-reactive/src/main/java/org/springframework/http/server/{ => reactive}/FilterChainHttpHandler.java (62%) rename spring-web-reactive/src/main/java/org/springframework/http/server/{ReactiveHttpFilter.java => reactive/HttpFilter.java} (77%) rename spring-web-reactive/src/main/java/org/springframework/http/server/{ReactiveHttpFilterChain.java => reactive/HttpFilterChain.java} (69%) rename spring-web-reactive/src/main/java/org/springframework/http/server/{ReactiveHttpHandler.java => reactive/HttpHandler.java} (85%) rename spring-web-reactive/src/main/java/org/springframework/http/server/{reactor/HttpHandlerChannelHandler.java => reactive/ReactorHttpHandlerAdapter.java} (77%) rename spring-web-reactive/src/main/java/org/springframework/http/server/{reactor => reactive}/ReactorServerHttpRequest.java (91%) rename spring-web-reactive/src/main/java/org/springframework/http/server/{reactor => reactive}/ReactorServerHttpResponse.java (91%) rename spring-web-reactive/src/main/java/org/springframework/http/server/{rxnetty/HttpHandlerRequestHandler.java => reactive/RxNettyHttpHandlerAdapter.java} (78%) rename spring-web-reactive/src/main/java/org/springframework/http/server/{rxnetty => reactive}/RxNettyServerHttpRequest.java (92%) rename spring-web-reactive/src/main/java/org/springframework/http/server/{rxnetty => reactive}/RxNettyServerHttpResponse.java (92%) rename spring-web-reactive/src/main/java/org/springframework/http/server/{ReactiveServerHttpRequest.java => reactive/ServerHttpRequest.java} (86%) rename spring-web-reactive/src/main/java/org/springframework/http/server/{ReactiveServerHttpResponse.java => reactive/ServerHttpResponse.java} (91%) rename spring-web-reactive/src/main/java/org/springframework/http/server/{servlet31/AsyncContextSynchronizer.java => reactive/ServletAsyncContextSynchronizer.java} (95%) rename spring-web-reactive/src/main/java/org/springframework/http/server/{servlet31/HttpHandlerServlet.java => reactive/ServletHttpHandlerAdapter.java} (65%) create mode 100644 spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java rename spring-web-reactive/src/main/java/org/springframework/http/server/{servlet31/Servlet31ServerHttpResponse.java => reactive/ServletServerHttpResponse.java} (52%) rename spring-web-reactive/src/main/java/org/springframework/http/server/{undertow/HttpHandlerHttpHandler.java => reactive/UndertowHttpHandlerAdapter.java} (67%) create mode 100644 spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java rename spring-web-reactive/src/main/java/org/springframework/http/server/{support => reactive/boot}/HttpServer.java (84%) rename spring-web-reactive/src/main/java/org/springframework/http/server/{support => reactive/boot}/HttpServerSupport.java (77%) rename spring-web-reactive/src/main/java/org/springframework/http/server/{support => reactive/boot}/JettyHttpServer.java (91%) rename spring-web-reactive/src/main/java/org/springframework/http/server/{support => reactive/boot}/ReactorHttpServer.java (87%) rename spring-web-reactive/src/main/java/org/springframework/http/server/{support => reactive/boot}/RxNettyHttpServer.java (87%) rename spring-web-reactive/src/main/java/org/springframework/http/server/{support => reactive/boot}/TomcatHttpServer.java (91%) rename spring-web-reactive/src/main/java/org/springframework/http/server/{support => reactive/boot}/UndertowHttpServer.java (88%) rename spring-web-reactive/src/main/java/org/springframework/http/server/{support => reactive/boot}/package-info.java (75%) delete mode 100644 spring-web-reactive/src/main/java/org/springframework/http/server/servlet31/RequestBodyPublisher.java delete mode 100644 spring-web-reactive/src/main/java/org/springframework/http/server/servlet31/ResponseBodySubscriber.java delete mode 100644 spring-web-reactive/src/main/java/org/springframework/http/server/servlet31/Servlet31ServerHttpRequest.java delete mode 100644 spring-web-reactive/src/main/java/org/springframework/http/server/undertow/RequestBodyPublisher.java delete mode 100644 spring-web-reactive/src/main/java/org/springframework/http/server/undertow/ResponseBodySubscriber.java delete mode 100644 spring-web-reactive/src/main/java/org/springframework/http/server/undertow/UndertowServerHttpRequest.java delete mode 100644 spring-web-reactive/src/main/java/org/springframework/http/server/undertow/UndertowServerHttpResponse.java rename spring-web-reactive/src/test/java/org/springframework/http/server/{servlet31 => reactive}/AsyncContextSynchronizerTests.java (88%) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/FilterChainHttpHandler.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/FilterChainHttpHandler.java similarity index 62% rename from spring-web-reactive/src/main/java/org/springframework/http/server/FilterChainHttpHandler.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/reactive/FilterChainHttpHandler.java index 4550a45212..746807b9a7 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/FilterChainHttpHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/FilterChainHttpHandler.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.http.server; +package org.springframework.http.server.reactive; import java.util.Arrays; import java.util.Collections; @@ -24,19 +24,19 @@ import org.reactivestreams.Publisher; import org.springframework.util.Assert; /** - * An {@link ReactiveHttpHandler} decorator that delegates to a list of - * {@link ReactiveHttpFilter}s and the target {@link ReactiveHttpHandler}. + * An {@link HttpHandler} decorator that delegates to a list of + * {@link HttpFilter}s and the target {@link HttpHandler}. * * @author Rossen Stoyanchev */ -public class FilterChainHttpHandler implements ReactiveHttpHandler { +public class FilterChainHttpHandler implements HttpHandler { - private final List filters; + private final List filters; - private final ReactiveHttpHandler targetHandler; + private final HttpHandler targetHandler; - public FilterChainHttpHandler(ReactiveHttpHandler targetHandler, ReactiveHttpFilter... filters) { + public FilterChainHttpHandler(HttpHandler targetHandler, HttpFilter... filters) { Assert.notNull(targetHandler, "'targetHandler' is required."); this.filters = (filters != null ? Arrays.asList(filters) : Collections.emptyList()); this.targetHandler = targetHandler; @@ -44,19 +44,19 @@ public class FilterChainHttpHandler implements ReactiveHttpHandler { @Override - public Publisher handle(ReactiveServerHttpRequest request, ReactiveServerHttpResponse response) { + public Publisher handle(ServerHttpRequest request, ServerHttpResponse response) { return new DefaultHttpFilterChain().filter(request, response); } - private class DefaultHttpFilterChain implements ReactiveHttpFilterChain { + private class DefaultHttpFilterChain implements HttpFilterChain { private int index; @Override - public Publisher filter(ReactiveServerHttpRequest request, ReactiveServerHttpResponse response) { + public Publisher filter(ServerHttpRequest request, ServerHttpResponse response) { if (this.index < filters.size()) { - ReactiveHttpFilter filter = filters.get(this.index++); + HttpFilter filter = filters.get(this.index++); return filter.filter(request, response, this); } else { diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/ReactiveHttpFilter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpFilter.java similarity index 77% rename from spring-web-reactive/src/main/java/org/springframework/http/server/ReactiveHttpFilter.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpFilter.java index e189e0f777..9e9d5fd500 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/ReactiveHttpFilter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpFilter.java @@ -14,17 +14,17 @@ * limitations under the License. */ -package org.springframework.http.server; +package org.springframework.http.server.reactive; import org.reactivestreams.Publisher; /** * @author Rossen Stoyanchev */ -public interface ReactiveHttpFilter { +public interface HttpFilter { - Publisher filter(ReactiveServerHttpRequest request, ReactiveServerHttpResponse response, - ReactiveHttpFilterChain chain); + Publisher filter(ServerHttpRequest request, ServerHttpResponse response, + HttpFilterChain chain); } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/ReactiveHttpFilterChain.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpFilterChain.java similarity index 69% rename from spring-web-reactive/src/main/java/org/springframework/http/server/ReactiveHttpFilterChain.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpFilterChain.java index 0ea094ff83..1c15d90791 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/ReactiveHttpFilterChain.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpFilterChain.java @@ -13,19 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.http.server; +package org.springframework.http.server.reactive; import org.reactivestreams.Publisher; -import org.springframework.http.server.ReactiveServerHttpRequest; -import org.springframework.http.server.ReactiveServerHttpResponse; - /** * @author Rossen Stoyanchev */ -public interface ReactiveHttpFilterChain { +public interface HttpFilterChain { - Publisher filter(ReactiveServerHttpRequest request, ReactiveServerHttpResponse response); + Publisher filter(ServerHttpRequest request, ServerHttpResponse response); } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/ReactiveHttpHandler.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpHandler.java similarity index 85% rename from spring-web-reactive/src/main/java/org/springframework/http/server/ReactiveHttpHandler.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpHandler.java index c801111964..f11cc13a2d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/ReactiveHttpHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpHandler.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.http.server; +package org.springframework.http.server.reactive; import org.reactivestreams.Publisher; @@ -27,10 +27,10 @@ import org.reactivestreams.Publisher; * @author Arjen Poutsma * @author Rossen Stoyanchev * @author Sebastien Deleuze - * @see ReactiveServerHttpRequest#getBody() - * @see ReactiveServerHttpResponse#setBody(Publisher) + * @see ServerHttpRequest#getBody() + * @see ServerHttpResponse#setBody(Publisher) */ -public interface ReactiveHttpHandler { +public interface HttpHandler { /** * Process the given request, generating a response in an asynchronous non blocking way. @@ -43,6 +43,6 @@ public interface ReactiveHttpHandler { * when the handling is complete (success or error) including the flush of the data on the * network. */ - Publisher handle(ReactiveServerHttpRequest request, ReactiveServerHttpResponse response); + Publisher handle(ServerHttpRequest request, ServerHttpResponse response); } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactor/HttpHandlerChannelHandler.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorHttpHandlerAdapter.java similarity index 77% rename from spring-web-reactive/src/main/java/org/springframework/http/server/reactor/HttpHandlerChannelHandler.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorHttpHandlerAdapter.java index b77a7e5840..b5005a3126 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactor/HttpHandlerChannelHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorHttpHandlerAdapter.java @@ -13,26 +13,28 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.http.server.reactor; +package org.springframework.http.server.reactive; import org.reactivestreams.Publisher; import reactor.io.buffer.Buffer; import reactor.io.net.ReactiveChannelHandler; import reactor.io.net.http.HttpChannel; -import org.springframework.http.server.ReactiveHttpHandler; +import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.http.server.reactive.ReactorServerHttpRequest; +import org.springframework.http.server.reactive.ReactorServerHttpResponse; import org.springframework.util.Assert; /** * @author Stephane Maldini */ -public class HttpHandlerChannelHandler +public class ReactorHttpHandlerAdapter implements ReactiveChannelHandler> { - private final ReactiveHttpHandler httpHandler; + private final HttpHandler httpHandler; - public HttpHandlerChannelHandler(ReactiveHttpHandler httpHandler) { + public ReactorHttpHandlerAdapter(HttpHandler httpHandler) { Assert.notNull(httpHandler, "'httpHandler' is required."); this.httpHandler = httpHandler; } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactor/ReactorServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java similarity index 91% rename from spring-web-reactive/src/main/java/org/springframework/http/server/reactor/ReactorServerHttpRequest.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java index 6d5667b6db..343ec20f24 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactor/ReactorServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.http.server.reactor; +package org.springframework.http.server.reactive; import java.net.URI; import java.net.URISyntaxException; @@ -26,13 +26,13 @@ import reactor.io.net.http.HttpChannel; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; -import org.springframework.http.server.ReactiveServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.util.Assert; /** * @author Stephane Maldini */ -public class ReactorServerHttpRequest implements ReactiveServerHttpRequest { +public class ReactorServerHttpRequest implements ServerHttpRequest { private final HttpChannel channel; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactor/ReactorServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java similarity index 91% rename from spring-web-reactive/src/main/java/org/springframework/http/server/reactor/ReactorServerHttpResponse.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java index d42992f3b8..17ab111345 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactor/ReactorServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.http.server.reactor; +package org.springframework.http.server.reactive; import java.nio.ByteBuffer; @@ -25,13 +25,13 @@ import reactor.io.net.http.model.Status; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; -import org.springframework.http.server.ReactiveServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.util.Assert; /** * @author Stephane Maldini */ -public class ReactorServerHttpResponse implements ReactiveServerHttpResponse { +public class ReactorServerHttpResponse implements ServerHttpResponse { private final HttpChannel channel; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/rxnetty/HttpHandlerRequestHandler.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyHttpHandlerAdapter.java similarity index 78% rename from spring-web-reactive/src/main/java/org/springframework/http/server/rxnetty/HttpHandlerRequestHandler.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyHttpHandlerAdapter.java index 07866c3f87..254fcb093f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/rxnetty/HttpHandlerRequestHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyHttpHandlerAdapter.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.http.server.rxnetty; +package org.springframework.http.server.reactive; import io.netty.buffer.ByteBuf; import io.reactivex.netty.protocol.http.server.HttpServerRequest; @@ -24,18 +24,20 @@ import org.reactivestreams.Publisher; import reactor.core.publisher.convert.RxJava1Converter; import rx.Observable; -import org.springframework.http.server.ReactiveHttpHandler; +import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.http.server.reactive.RxNettyServerHttpRequest; +import org.springframework.http.server.reactive.RxNettyServerHttpResponse; import org.springframework.util.Assert; /** * @author Rossen Stoyanchev */ -public class HttpHandlerRequestHandler implements RequestHandler { +public class RxNettyHttpHandlerAdapter implements RequestHandler { - private final ReactiveHttpHandler httpHandler; + private final HttpHandler httpHandler; - public HttpHandlerRequestHandler(ReactiveHttpHandler httpHandler) { + public RxNettyHttpHandlerAdapter(HttpHandler httpHandler) { Assert.notNull(httpHandler, "'httpHandler' is required."); this.httpHandler = httpHandler; } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/rxnetty/RxNettyServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java similarity index 92% rename from spring-web-reactive/src/main/java/org/springframework/http/server/rxnetty/RxNettyServerHttpRequest.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java index d69683ca2c..1fa794ca37 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/rxnetty/RxNettyServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.http.server.rxnetty; +package org.springframework.http.server.reactive; import java.net.URI; import java.net.URISyntaxException; @@ -28,14 +28,14 @@ import rx.Observable; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; -import org.springframework.http.server.ReactiveServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.util.Assert; /** * @author Rossen Stoyanchev * @author Stephane Maldini */ -public class RxNettyServerHttpRequest implements ReactiveServerHttpRequest { +public class RxNettyServerHttpRequest implements ServerHttpRequest { private final HttpServerRequest request; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/rxnetty/RxNettyServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java similarity index 92% rename from spring-web-reactive/src/main/java/org/springframework/http/server/rxnetty/RxNettyServerHttpResponse.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java index d301799408..73ac4864ce 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/rxnetty/RxNettyServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.http.server.rxnetty; +package org.springframework.http.server.reactive; import java.nio.ByteBuffer; @@ -28,14 +28,14 @@ import rx.Observable; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; -import org.springframework.http.server.ReactiveServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.util.Assert; /** * @author Rossen Stoyanchev * @author Stephane Maldini */ -public class RxNettyServerHttpResponse implements ReactiveServerHttpResponse { +public class RxNettyServerHttpResponse implements ServerHttpResponse { private final HttpServerResponse response; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/ReactiveServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java similarity index 86% rename from spring-web-reactive/src/main/java/org/springframework/http/server/ReactiveServerHttpRequest.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java index 4b5d0d1fa2..1cbbfbfb00 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/ReactiveServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.http.server; +package org.springframework.http.server.reactive; import org.springframework.http.HttpRequest; import org.springframework.http.ReactiveHttpInputMessage; @@ -24,6 +24,6 @@ import org.springframework.http.ReactiveHttpInputMessage; * * @author Arjen Poutsma */ -public interface ReactiveServerHttpRequest extends HttpRequest, ReactiveHttpInputMessage { +public interface ServerHttpRequest extends HttpRequest, ReactiveHttpInputMessage { } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/ReactiveServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java similarity index 91% rename from spring-web-reactive/src/main/java/org/springframework/http/server/ReactiveServerHttpResponse.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java index 6ca45f9264..dabf620f1d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/ReactiveServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.http.server; +package org.springframework.http.server.reactive; import org.reactivestreams.Publisher; @@ -26,7 +26,7 @@ import org.springframework.http.ReactiveHttpOutputMessage; * * @author Arjen Poutsma */ -public interface ReactiveServerHttpResponse extends ReactiveHttpOutputMessage { +public interface ServerHttpResponse extends ReactiveHttpOutputMessage { /** * Set the HTTP status code of the response. diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/servlet31/AsyncContextSynchronizer.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletAsyncContextSynchronizer.java similarity index 95% rename from spring-web-reactive/src/main/java/org/springframework/http/server/servlet31/AsyncContextSynchronizer.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletAsyncContextSynchronizer.java index 3900fbfbb5..dc1e015b5e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/servlet31/AsyncContextSynchronizer.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletAsyncContextSynchronizer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.http.server.servlet31; +package org.springframework.http.server.reactive; import java.io.IOException; import java.util.concurrent.atomic.AtomicInteger; @@ -30,7 +30,7 @@ import javax.servlet.ServletOutputStream; * @author Arjen Poutsma * @see AsyncContext */ -final class AsyncContextSynchronizer { +final class ServletAsyncContextSynchronizer { private static final int NONE_COMPLETE = 0; @@ -48,7 +48,7 @@ final class AsyncContextSynchronizer { * Creates a new {@code AsyncContextSynchronizer} based on the given context. * @param asyncContext the context to base this synchronizer on */ - public AsyncContextSynchronizer(AsyncContext asyncContext) { + public ServletAsyncContextSynchronizer(AsyncContext asyncContext) { this.asyncContext = asyncContext; } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/servlet31/HttpHandlerServlet.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java similarity index 65% rename from spring-web-reactive/src/main/java/org/springframework/http/server/servlet31/HttpHandlerServlet.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java index 00945747af..1ea7e4cd0c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/servlet31/HttpHandlerServlet.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.http.server.servlet31; +package org.springframework.http.server.reactive; import java.io.IOException; import javax.servlet.AsyncContext; @@ -30,24 +30,21 @@ import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import org.springframework.http.HttpStatus; -import org.springframework.http.server.ReactiveHttpHandler; /** * @author Arjen Poutsma * @author Rossen Stoyanchev */ @WebServlet(asyncSupported = true) -public class HttpHandlerServlet extends HttpServlet { +public class ServletHttpHandlerAdapter extends HttpServlet { - private static final int BUFFER_SIZE = 8192; - - private static Log logger = LogFactory.getLog(HttpHandlerServlet.class); + private static Log logger = LogFactory.getLog(ServletHttpHandlerAdapter.class); - private ReactiveHttpHandler handler; + private HttpHandler handler; - public void setHandler(ReactiveHttpHandler handler) { + public void setHandler(HttpHandler handler) { this.handler = handler; } @@ -57,15 +54,13 @@ public class HttpHandlerServlet extends HttpServlet { throws ServletException, IOException { AsyncContext context = request.startAsync(); - AsyncContextSynchronizer synchronizer = new AsyncContextSynchronizer(context); + ServletAsyncContextSynchronizer synchronizer = new ServletAsyncContextSynchronizer(context); - RequestBodyPublisher requestPublisher = new RequestBodyPublisher(synchronizer, BUFFER_SIZE); - request.getInputStream().setReadListener(requestPublisher); - Servlet31ServerHttpRequest httpRequest = new Servlet31ServerHttpRequest(request, requestPublisher); + ServletServerHttpRequest httpRequest = new ServletServerHttpRequest(request, synchronizer); + request.getInputStream().setReadListener(httpRequest.getReadListener()); - ResponseBodySubscriber responseSubscriber = new ResponseBodySubscriber(synchronizer); - response.getOutputStream().setWriteListener(responseSubscriber); - Servlet31ServerHttpResponse httpResponse = new Servlet31ServerHttpResponse(response, responseSubscriber); + ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response, synchronizer); + response.getOutputStream().setWriteListener(httpResponse.getWriteListener()); HandlerResultSubscriber resultSubscriber = new HandlerResultSubscriber(synchronizer, httpResponse); this.handler.handle(httpRequest, httpResponse).subscribe(resultSubscriber); @@ -74,13 +69,13 @@ public class HttpHandlerServlet extends HttpServlet { private static class HandlerResultSubscriber implements Subscriber { - private final AsyncContextSynchronizer synchronizer; + private final ServletAsyncContextSynchronizer synchronizer; - private final Servlet31ServerHttpResponse response; + private final ServletServerHttpResponse response; - public HandlerResultSubscriber(AsyncContextSynchronizer synchronizer, - Servlet31ServerHttpResponse response) { + public HandlerResultSubscriber(ServletAsyncContextSynchronizer synchronizer, + ServletServerHttpResponse response) { this.synchronizer = synchronizer; this.response = response; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java new file mode 100644 index 0000000000..46158c306e --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java @@ -0,0 +1,321 @@ +/* + * Copyright 2002-2015 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.server.reactive; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.util.Assert; +import org.springframework.util.LinkedCaseInsensitiveMap; +import org.springframework.util.StringUtils; + +/** + * @author Rossen Stoyanchev + */ +public class ServletServerHttpRequest implements ServerHttpRequest { + + private static final int BUFFER_SIZE = 8192; + + private static final Log logger = LogFactory.getLog(ServletServerHttpRequest.class); + + + private final HttpServletRequest servletRequest; + + private HttpHeaders headers; + + private final RequestBodyPublisher requestBodyPublisher; + + + public ServletServerHttpRequest(HttpServletRequest servletRequest, ServletAsyncContextSynchronizer synchronizer) { + Assert.notNull(servletRequest, "HttpServletRequest must not be null"); + this.servletRequest = servletRequest; + this.requestBodyPublisher = new RequestBodyPublisher(synchronizer, BUFFER_SIZE); + } + + + @Override + public HttpMethod getMethod() { + return HttpMethod.valueOf(this.servletRequest.getMethod()); + } + + @Override + public URI getURI() { + try { + return new URI(this.servletRequest.getScheme(), null, this.servletRequest.getServerName(), + this.servletRequest.getServerPort(), this.servletRequest.getRequestURI(), + this.servletRequest.getQueryString(), null); + } + catch (URISyntaxException ex) { + throw new IllegalStateException("Could not get HttpServletRequest URI: " + ex.getMessage(), ex); + } + } + + @Override + public HttpHeaders getHeaders() { + if (this.headers == null) { + this.headers = new HttpHeaders(); + for (Enumeration names = this.servletRequest.getHeaderNames(); names.hasMoreElements(); ) { + String headerName = (String) names.nextElement(); + for (Enumeration headerValues = this.servletRequest.getHeaders(headerName); + headerValues.hasMoreElements(); ) { + String headerValue = (String) headerValues.nextElement(); + this.headers.add(headerName, headerValue); + } + } + // HttpServletRequest exposes some headers as properties: we should include those if not already present + MediaType contentType = this.headers.getContentType(); + if (contentType == null) { + String requestContentType = this.servletRequest.getContentType(); + if (StringUtils.hasLength(requestContentType)) { + contentType = MediaType.parseMediaType(requestContentType); + this.headers.setContentType(contentType); + } + } + if (contentType != null && contentType.getCharSet() == null) { + String requestEncoding = this.servletRequest.getCharacterEncoding(); + if (StringUtils.hasLength(requestEncoding)) { + Charset charSet = Charset.forName(requestEncoding); + Map params = new LinkedCaseInsensitiveMap<>(); + params.putAll(contentType.getParameters()); + params.put("charset", charSet.toString()); + MediaType newContentType = new MediaType(contentType.getType(), contentType.getSubtype(), params); + this.headers.setContentType(newContentType); + } + } + if (this.headers.getContentLength() == -1) { + int requestContentLength = this.servletRequest.getContentLength(); + if (requestContentLength != -1) { + this.headers.setContentLength(requestContentLength); + } + } + } + return this.headers; + } + + @Override + public Publisher getBody() { + return this.requestBodyPublisher; + } + + ReadListener getReadListener() { + return this.requestBodyPublisher; + } + + + private static class RequestBodyPublisher implements ReadListener, Publisher { + + private final ServletAsyncContextSynchronizer synchronizer; + + private final byte[] buffer; + + private final DemandCounter demand = new DemandCounter(); + + private Subscriber subscriber; + + private boolean stalled; + + private boolean cancelled; + + + public RequestBodyPublisher(ServletAsyncContextSynchronizer synchronizer, int bufferSize) { + this.synchronizer = synchronizer; + this.buffer = new byte[bufferSize]; + } + + + @Override + public void subscribe(Subscriber subscriber) { + if (subscriber == null) { + throw new NullPointerException(); + } + else if (this.subscriber != null) { + subscriber.onError(new IllegalStateException("Only one subscriber allowed")); + } + this.subscriber = subscriber; + this.subscriber.onSubscribe(new RequestBodySubscription()); + } + + @Override + public void onDataAvailable() throws IOException { + if (cancelled) { + return; + } + ServletInputStream input = this.synchronizer.getInputStream(); + logger.debug("onDataAvailable: " + input); + + while (true) { + logger.debug("Demand: " + this.demand); + + if (!demand.hasDemand()) { + stalled = true; + break; + } + + boolean ready = input.isReady(); + logger.debug("Input ready: " + ready + " finished: " + input.isFinished()); + + if (!ready) { + break; + } + + int read = input.read(buffer); + logger.debug("Input read:" + read); + + if (read == -1) { + break; + } + else if (read > 0) { + this.demand.decrement(); + byte[] copy = Arrays.copyOf(this.buffer, read); + +// logger.debug("Next: " + new String(copy, UTF_8)); + + this.subscriber.onNext(ByteBuffer.wrap(copy)); + + } + } + } + + @Override + public void onAllDataRead() throws IOException { + if (cancelled) { + return; + } + logger.debug("All data read"); + this.synchronizer.readComplete(); + if (this.subscriber != null) { + this.subscriber.onComplete(); + } + } + + @Override + public void onError(Throwable t) { + if (cancelled) { + return; + } + logger.error("RequestBodyPublisher Error", t); + this.synchronizer.readComplete(); + if (this.subscriber != null) { + this.subscriber.onError(t); + } + } + + private class RequestBodySubscription implements Subscription { + + @Override + public void request(long n) { + if (cancelled) { + return; + } + logger.debug("Updating demand " + demand + " by " + n); + + demand.increase(n); + + logger.debug("Stalled: " + stalled); + + if (stalled) { + stalled = false; + try { + onDataAvailable(); + } + catch (IOException ex) { + onError(ex); + } + } + } + + @Override + public void cancel() { + if (cancelled) { + return; + } + cancelled = true; + synchronizer.readComplete(); + demand.reset(); + } + } + + + /** + * Small utility class for keeping track of Reactive Streams demand. + */ + private static final class DemandCounter { + + private final AtomicLong demand = new AtomicLong(); + + /** + * Increases the demand by the given number + * @param n the positive number to increase demand by + * @return the increased demand + * @see org.reactivestreams.Subscription#request(long) + */ + public long increase(long n) { + Assert.isTrue(n > 0, "'n' must be higher than 0"); + return demand.updateAndGet(d -> d != Long.MAX_VALUE ? d + n : Long.MAX_VALUE); + } + + /** + * Decreases the demand by one. + * @return the decremented demand + */ + public long decrement() { + return demand.updateAndGet(d -> d != Long.MAX_VALUE ? d - 1 : Long.MAX_VALUE); + } + + /** + * Indicates whether this counter has demand, i.e. whether it is higher than 0. + * @return {@code true} if this counter has demand; {@code false} otherwise + */ + public boolean hasDemand() { + return this.demand.get() > 0; + } + + /** + * Resets this counter to 0. + * @see org.reactivestreams.Subscription#cancel() + */ + public void reset() { + this.demand.set(0); + } + + @Override + public String toString() { + return demand.toString(); + } + } + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/servlet31/Servlet31ServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java similarity index 52% rename from spring-web-reactive/src/main/java/org/springframework/http/server/servlet31/Servlet31ServerHttpResponse.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java index dde179cd47..b992cc6463 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/servlet31/Servlet31ServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java @@ -14,45 +14,51 @@ * limitations under the License. */ -package org.springframework.http.server.servlet31; +package org.springframework.http.server.reactive; +import java.io.IOException; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.util.List; import java.util.Map; +import javax.servlet.ServletOutputStream; +import javax.servlet.WriteListener; import javax.servlet.http.HttpServletResponse; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; import reactor.Publishers; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.http.server.ReactiveServerHttpResponse; import org.springframework.util.Assert; /** * @author Rossen Stoyanchev */ -public class Servlet31ServerHttpResponse implements ReactiveServerHttpResponse { +public class ServletServerHttpResponse implements ServerHttpResponse { + + private static final Log logger = LogFactory.getLog(ServletServerHttpResponse.class); + private final HttpServletResponse response; - private final ResponseBodySubscriber subscriber; - private final HttpHeaders headers; + private final ResponseBodySubscriber subscriber; + private boolean headersWritten = false; - public Servlet31ServerHttpResponse(HttpServletResponse response, - ResponseBodySubscriber subscriber) { - + public ServletServerHttpResponse(HttpServletResponse response, ServletAsyncContextSynchronizer synchronizer) { Assert.notNull(response, "'response' must not be null"); - Assert.notNull(subscriber, "'subscriber' must not be null"); this.response = response; - this.subscriber = subscriber; this.headers = new HttpHeaders(); + this.subscriber = new ResponseBodySubscriber(synchronizer); } @@ -66,6 +72,10 @@ public class Servlet31ServerHttpResponse implements ReactiveServerHttpResponse { return (this.headersWritten ? HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers); } + WriteListener getWriteListener() { + return this.subscriber; + } + @Override public Publisher writeHeaders() { applyHeaders(); @@ -98,4 +108,84 @@ public class Servlet31ServerHttpResponse implements ReactiveServerHttpResponse { } } + + private static class ResponseBodySubscriber implements WriteListener, Subscriber { + + private final ServletAsyncContextSynchronizer synchronizer; + + private Subscription subscription; + + private ByteBuffer buffer; + + private volatile boolean subscriberComplete = false; + + + public ResponseBodySubscriber(ServletAsyncContextSynchronizer synchronizer) { + this.synchronizer = synchronizer; + } + + + @Override + public void onSubscribe(Subscription subscription) { + this.subscription = subscription; + this.subscription.request(1); + } + + @Override + public void onNext(ByteBuffer bytes) { + + Assert.isNull(buffer); + + this.buffer = bytes; + try { + onWritePossible(); + } + catch (IOException e) { + onError(e); + } + } + + @Override + public void onComplete() { + logger.debug("Complete buffer: " + (buffer == null)); + + this.subscriberComplete = true; + + if (buffer == null) { + this.synchronizer.writeComplete(); + } + } + + @Override + public void onWritePossible() throws IOException { + ServletOutputStream output = this.synchronizer.getOutputStream(); + + boolean ready = output.isReady(); + logger.debug("Output: " + ready + " buffer: " + (buffer == null)); + + if (ready) { + if (this.buffer != null) { + byte[] bytes = new byte[this.buffer.remaining()]; + this.buffer.get(bytes); + this.buffer = null; + output.write(bytes); + if (!subscriberComplete) { + this.subscription.request(1); + } + else { + this.synchronizer.writeComplete(); + } + } + else { + this.subscription.request(1); + } + } + } + + @Override + public void onError(Throwable t) { + logger.error("ResponseBodySubscriber error", t); + } + } + } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/undertow/HttpHandlerHttpHandler.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java similarity index 67% rename from spring-web-reactive/src/main/java/org/springframework/http/server/undertow/HttpHandlerHttpHandler.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java index 1f9cb09076..dce2694e75 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/undertow/HttpHandlerHttpHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java @@ -14,11 +14,8 @@ * limitations under the License. */ -package org.springframework.http.server.undertow; +package org.springframework.http.server.reactive; -import org.springframework.http.server.ReactiveServerHttpRequest; -import org.springframework.http.server.ReactiveServerHttpResponse; -import org.springframework.http.server.ReactiveHttpHandler; import org.springframework.util.Assert; import io.undertow.server.HttpServerExchange; @@ -32,15 +29,15 @@ import org.reactivestreams.Subscription; * @author Marek Hawrylczak * @author Rossen Stoyanchev */ -public class HttpHandlerHttpHandler implements io.undertow.server.HttpHandler { +public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandler { - private static Log logger = LogFactory.getLog(HttpHandlerHttpHandler.class); + private static Log logger = LogFactory.getLog(UndertowHttpHandlerAdapter.class); - private final ReactiveHttpHandler delegate; + private final HttpHandler delegate; - public HttpHandlerHttpHandler(ReactiveHttpHandler delegate) { + public UndertowHttpHandlerAdapter(HttpHandler delegate) { Assert.notNull(delegate, "'delegate' is required."); this.delegate = delegate; } @@ -48,11 +45,9 @@ public class HttpHandlerHttpHandler implements io.undertow.server.HttpHandler { @Override public void handleRequest(HttpServerExchange exchange) throws Exception { - RequestBodyPublisher requestPublisher = new RequestBodyPublisher(exchange); - ReactiveServerHttpRequest request = new UndertowServerHttpRequest(exchange, requestPublisher); - ResponseBodySubscriber responseSubscriber = new ResponseBodySubscriber(exchange); - ReactiveServerHttpResponse response = new UndertowServerHttpResponse(exchange, responseSubscriber); + ServerHttpRequest request = new UndertowServerHttpRequest(exchange); + ServerHttpResponse response = new UndertowServerHttpResponse(exchange); exchange.dispatch(); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java new file mode 100644 index 0000000000..db3e5936d3 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java @@ -0,0 +1,298 @@ +/* + * Copyright 2002-2015 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.server.reactive; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicLongFieldUpdater; + +import io.undertow.connector.PooledByteBuffer; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.HeaderValues; +import io.undertow.util.SameThreadExecutor; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import org.xnio.ChannelListener; +import org.xnio.channels.StreamSourceChannel; +import reactor.core.error.SpecificationExceptions; +import reactor.core.support.BackpressureUtils; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.util.Assert; + +import static org.xnio.IoUtils.safeClose; + +/** + * @author Marek Hawrylczak + * @author Rossen Stoyanchev + */ +public class UndertowServerHttpRequest implements ServerHttpRequest { + + private final HttpServerExchange exchange; + + private final Publisher body = new RequestBodyPublisher(); + + private HttpHeaders headers; + + + public UndertowServerHttpRequest(HttpServerExchange exchange) { + Assert.notNull(exchange, "'exchange' is required."); + this.exchange = exchange; + } + + + @Override + public HttpMethod getMethod() { + return HttpMethod.valueOf(this.exchange.getRequestMethod().toString()); + } + + @Override + public URI getURI() { + try { + return new URI(this.exchange.getRequestScheme(), null, this.exchange.getHostName(), + this.exchange.getHostPort(), this.exchange.getRequestURI(), + this.exchange.getQueryString(), null); + } + catch (URISyntaxException ex) { + throw new IllegalStateException("Could not get URI: " + ex.getMessage(), ex); + } + } + + @Override + public HttpHeaders getHeaders() { + if (this.headers == null) { + this.headers = new HttpHeaders(); + for (HeaderValues headerValues : this.exchange.getRequestHeaders()) { + for (String value : headerValues) { + this.headers.add(headerValues.getHeaderName().toString(), value); + } + } + } + return this.headers; + } + + @Override + public Publisher getBody() { + return this.body; + } + + + private static final AtomicLongFieldUpdater DEMAND = + AtomicLongFieldUpdater.newUpdater(RequestBodyPublisher.RequestBodySubscription.class, "demand"); + + private class RequestBodyPublisher implements Publisher { + + private Subscriber subscriber; + + + @Override + public void subscribe(Subscriber subscriber) { + if (subscriber == null) { + throw SpecificationExceptions.spec_2_13_exception(); + } + if (this.subscriber != null) { + subscriber.onError(new IllegalStateException("Only one subscriber allowed")); + } + + this.subscriber = subscriber; + this.subscriber.onSubscribe(new RequestBodySubscription()); + } + + + private class RequestBodySubscription implements Subscription, Runnable, + ChannelListener { + + volatile long demand; + + private PooledByteBuffer pooledBuffer; + + private StreamSourceChannel channel; + + private boolean subscriptionClosed; + + private boolean draining; + + + @Override + public void request(long n) { + BackpressureUtils.checkRequest(n, subscriber); + if (this.subscriptionClosed) { + return; + } + BackpressureUtils.getAndAdd(DEMAND, this, n); + scheduleNextMessage(); + } + + private void scheduleNextMessage() { + exchange.dispatch(exchange.isInIoThread() ? SameThreadExecutor.INSTANCE : + exchange.getIoThread(), this); + } + + @Override + public void cancel() { + this.subscriptionClosed = true; + close(); + } + + private void close() { + if (this.pooledBuffer != null) { + safeClose(this.pooledBuffer); + this.pooledBuffer = null; + } + if (this.channel != null) { + safeClose(this.channel); + this.channel = null; + } + } + + @Override + public void run() { + if (this.subscriptionClosed || this.draining) { + return; + } + if (0 == BackpressureUtils.getAndSub(DEMAND, this, 1)) { + return; + } + + this.draining = true; + + if (this.channel == null) { + this.channel = exchange.getRequestChannel(); + + if (this.channel == null) { + if (exchange.isRequestComplete()) { + return; + } + else { + throw new IllegalStateException("Failed to acquire channel!"); + } + } + } + if (this.pooledBuffer == null) { + this.pooledBuffer = exchange.getConnection().getByteBufferPool().allocate(); + } + else { + this.pooledBuffer.getBuffer().clear(); + } + + try { + ByteBuffer buffer = this.pooledBuffer.getBuffer(); + int count; + do { + count = this.channel.read(buffer); + if (count == 0) { + this.channel.getReadSetter().set(this); + this.channel.resumeReads(); + } + else if (count == -1) { + if (buffer.position() > 0) { + doOnNext(buffer); + } + doOnComplete(); + } + else { + if (buffer.remaining() == 0) { + if (this.demand == 0) { + this.channel.suspendReads(); + } + doOnNext(buffer); + if (this.demand > 0) { + scheduleNextMessage(); + } + break; + } + } + } while (count > 0); + } + catch (IOException e) { + doOnError(e); + } + } + + private void doOnNext(ByteBuffer buffer) { + this.draining = false; + buffer.flip(); + subscriber.onNext(buffer); + } + + private void doOnComplete() { + this.subscriptionClosed = true; + try { + subscriber.onComplete(); + } + finally { + close(); + } + } + + private void doOnError(Throwable t) { + this.subscriptionClosed = true; + try { + subscriber.onError(t); + } + finally { + close(); + } + } + + @Override + public void handleEvent(StreamSourceChannel channel) { + if (this.subscriptionClosed) { + return; + } + + try { + ByteBuffer buffer = this.pooledBuffer.getBuffer(); + int count; + do { + count = channel.read(buffer); + if (count == 0) { + return; + } + else if (count == -1) { + if (buffer.position() > 0) { + doOnNext(buffer); + } + doOnComplete(); + } + else { + if (buffer.remaining() == 0) { + if (this.demand == 0) { + channel.suspendReads(); + } + doOnNext(buffer); + if (this.demand > 0) { + scheduleNextMessage(); + } + break; + } + } + } while (count > 0); + } + catch (IOException e) { + doOnError(e); + } + } + } + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java new file mode 100644 index 0000000000..d96b85bdf3 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java @@ -0,0 +1,267 @@ +/* + * Copyright 2002-2015 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.server.reactive; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.util.Assert; + +import io.undertow.connector.PooledByteBuffer; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.HttpString; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscription; +import org.xnio.ChannelListener; +import org.xnio.channels.StreamSinkChannel; +import reactor.core.subscriber.BaseSubscriber; + +import static org.xnio.ChannelListeners.closingChannelExceptionHandler; +import static org.xnio.ChannelListeners.flushingChannelListener; +import static org.xnio.IoUtils.safeClose; + +/** + * @author Marek Hawrylczak + * @author Rossen Stoyanchev + */ +public class UndertowServerHttpResponse implements ServerHttpResponse { + + private static final Log logger = LogFactory.getLog(UndertowServerHttpResponse.class); + + + private final HttpServerExchange exchange; + + private final ResponseBodySubscriber bodySubscriber = new ResponseBodySubscriber(); + + private final HttpHeaders headers = new HttpHeaders(); + + private boolean headersWritten = false; + + + public UndertowServerHttpResponse(HttpServerExchange exchange) { + Assert.notNull(exchange, "'exchange' is required."); + this.exchange = exchange; + } + + + @Override + public void setStatusCode(HttpStatus status) { + Assert.notNull(status); + this.exchange.setStatusCode(status.value()); + } + + + @Override + public Publisher setBody(Publisher bodyPublisher) { + applyHeaders(); + return (subscriber -> bodyPublisher.subscribe(bodySubscriber)); + } + + @Override + public HttpHeaders getHeaders() { + return (this.headersWritten ? HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers); + } + + @Override + public Publisher writeHeaders() { + applyHeaders(); + return s -> s.onSubscribe(new Subscription() { + @Override + public void request(long n) { + s.onComplete(); + } + + @Override + public void cancel() { + } + }); + } + + private void applyHeaders() { + if (!this.headersWritten) { + for (Map.Entry> entry : this.headers.entrySet()) { + HttpString headerName = HttpString.tryFromString(entry.getKey()); + this.exchange.getResponseHeaders().addAll(headerName, entry.getValue()); + + } + this.headersWritten = true; + } + } + + + private class ResponseBodySubscriber extends BaseSubscriber + implements ChannelListener { + + private Subscription subscription; + + private final Queue buffers = new ConcurrentLinkedQueue<>(); + + private final AtomicInteger writing = new AtomicInteger(); + + private final AtomicBoolean closing = new AtomicBoolean(); + + private StreamSinkChannel responseChannel; + + + @Override + public void onSubscribe(Subscription subscription) { + super.onSubscribe(subscription); + this.subscription = subscription; + this.subscription.request(1); + } + + @Override + public void onNext(ByteBuffer buffer) { + super.onNext(buffer); + + if (this.responseChannel == null) { + this.responseChannel = exchange.getResponseChannel(); + } + + this.writing.incrementAndGet(); + try { + int c; + do { + c = this.responseChannel.write(buffer); + } while (buffer.hasRemaining() && c > 0); + + if (buffer.hasRemaining()) { + this.writing.incrementAndGet(); + enqueue(buffer); + this.responseChannel.getWriteSetter().set(this); + this.responseChannel.resumeWrites(); + } + else { + this.subscription.request(1); + } + + } + catch (IOException ex) { + onError(ex); + } + finally { + this.writing.decrementAndGet(); + if (this.closing.get()) { + closeIfDone(); + } + } + } + + private void enqueue(ByteBuffer src) { + do { + PooledByteBuffer buffer = exchange.getConnection().getByteBufferPool().allocate(); + ByteBuffer dst = buffer.getBuffer(); + copy(dst, src); + dst.flip(); + this.buffers.add(buffer); + } while (src.remaining() > 0); + } + + private void copy(ByteBuffer dst, ByteBuffer src) { + int n = Math.min(dst.capacity(), src.remaining()); + for (int i = 0; i < n; i++) { + dst.put(src.get()); + } + } + + @Override + public void handleEvent(StreamSinkChannel channel) { + try { + int c; + do { + ByteBuffer buffer = this.buffers.peek().getBuffer(); + do { + c = channel.write(buffer); + } while (buffer.hasRemaining() && c > 0); + + if (!buffer.hasRemaining()) { + safeClose(this.buffers.remove()); + } + } while (!this.buffers.isEmpty() && c > 0); + + if (!this.buffers.isEmpty()) { + channel.resumeWrites(); + } + else { + this.writing.decrementAndGet(); + + if (this.closing.get()) { + closeIfDone(); + } + else { + this.subscription.request(1); + } + } + } + catch (IOException ex) { + onError(ex); + } + } + + @Override + public void onError(Throwable ex) { + super.onError(ex); + logger.error("ResponseBodySubscriber error", ex); + if (!exchange.isResponseStarted() && exchange.getStatusCode() < 500) { + exchange.setStatusCode(500); + } + } + + @Override + public void onComplete() { + super.onComplete(); + if (this.responseChannel != null) { + this.closing.set(true); + closeIfDone(); + } + } + + private void closeIfDone() { + if (this.writing.get() == 0) { + if (this.closing.compareAndSet(true, false)) { + closeChannel(); + } + } + } + + private void closeChannel() { + try { + this.responseChannel.shutdownWrites(); + + if (!this.responseChannel.flush()) { + this.responseChannel.getWriteSetter().set(flushingChannelListener( + o -> safeClose(this.responseChannel), closingChannelExceptionHandler())); + this.responseChannel.resumeWrites(); + } + this.responseChannel = null; + } + catch (IOException ex) { + onError(ex); + } + } + } +} diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/support/HttpServer.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/HttpServer.java similarity index 84% rename from spring-web-reactive/src/main/java/org/springframework/http/server/support/HttpServer.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/HttpServer.java index f1510c8abe..d2255b3609 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/support/HttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/HttpServer.java @@ -14,12 +14,12 @@ * limitations under the License. */ -package org.springframework.http.server.support; +package org.springframework.http.server.reactive.boot; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.Lifecycle; -import org.springframework.http.server.ReactiveHttpHandler; +import org.springframework.http.server.reactive.HttpHandler; /** * @author Rossen Stoyanchev @@ -28,6 +28,6 @@ public interface HttpServer extends InitializingBean, Lifecycle { void setPort(int port); - void setHandler(ReactiveHttpHandler handler); + void setHandler(HttpHandler handler); } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/support/HttpServerSupport.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/HttpServerSupport.java similarity index 77% rename from spring-web-reactive/src/main/java/org/springframework/http/server/support/HttpServerSupport.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/HttpServerSupport.java index 3510b9d610..e498af70e6 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/support/HttpServerSupport.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/HttpServerSupport.java @@ -14,10 +14,10 @@ * limitations under the License. */ -package org.springframework.http.server.support; +package org.springframework.http.server.reactive.boot; -import org.springframework.http.server.ReactiveHttpHandler; +import org.springframework.http.server.reactive.HttpHandler; /** * @author Rossen Stoyanchev @@ -26,7 +26,7 @@ public class HttpServerSupport { private int port = -1; - private ReactiveHttpHandler httpHandler; + private HttpHandler httpHandler; public void setPort(int port) { @@ -37,11 +37,11 @@ public class HttpServerSupport { return this.port; } - public void setHandler(ReactiveHttpHandler handler) { + public void setHandler(HttpHandler handler) { this.httpHandler = handler; } - public ReactiveHttpHandler getHttpHandler() { + public HttpHandler getHttpHandler() { return this.httpHandler; } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/support/JettyHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/JettyHttpServer.java similarity index 91% rename from spring-web-reactive/src/main/java/org/springframework/http/server/support/JettyHttpServer.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/JettyHttpServer.java index 43bdaf62cf..d6780ae19c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/support/JettyHttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/JettyHttpServer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.http.server.support; +package org.springframework.http.server.reactive.boot; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; @@ -24,7 +24,7 @@ import org.eclipse.jetty.servlet.ServletHolder; import org.springframework.beans.factory.InitializingBean; import org.springframework.util.Assert; import org.springframework.util.SocketUtils; -import org.springframework.http.server.servlet31.HttpHandlerServlet; +import org.springframework.http.server.reactive.ServletHttpHandlerAdapter; /** * @author Rossen Stoyanchev @@ -51,7 +51,7 @@ public class JettyHttpServer extends HttpServerSupport implements InitializingBe this.jettyServer = new Server(); Assert.notNull(getHttpHandler()); - HttpHandlerServlet servlet = new HttpHandlerServlet(); + ServletHttpHandlerAdapter servlet = new ServletHttpHandlerAdapter(); servlet.setHandler(getHttpHandler()); ServletHolder servletHolder = new ServletHolder(servlet); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/support/ReactorHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java similarity index 87% rename from spring-web-reactive/src/main/java/org/springframework/http/server/support/ReactorHttpServer.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java index 91245e1ed9..c1bfe69d16 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/support/ReactorHttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java @@ -14,14 +14,14 @@ * limitations under the License. */ -package org.springframework.http.server.support; +package org.springframework.http.server.reactive.boot; import reactor.io.buffer.Buffer; import reactor.io.net.ReactiveNet; import org.springframework.beans.factory.InitializingBean; import org.springframework.util.Assert; -import org.springframework.http.server.reactor.HttpHandlerChannelHandler; +import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter; /** * @author Stephane Maldini @@ -29,7 +29,7 @@ import org.springframework.http.server.reactor.HttpHandlerChannelHandler; public class ReactorHttpServer extends HttpServerSupport implements InitializingBean, HttpServer { - private HttpHandlerChannelHandler reactorHandler; + private ReactorHttpHandlerAdapter reactorHandler; private reactor.io.net.http.HttpServer reactorServer; @@ -44,7 +44,7 @@ public class ReactorHttpServer extends HttpServerSupport public void afterPropertiesSet() throws Exception { Assert.notNull(getHttpHandler()); - this.reactorHandler = new HttpHandlerChannelHandler(getHttpHandler()); + this.reactorHandler = new ReactorHttpHandlerAdapter(getHttpHandler()); this.reactorServer = (getPort() != -1 ? ReactiveNet.httpServer(getPort()) : ReactiveNet.httpServer()); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/support/RxNettyHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/RxNettyHttpServer.java similarity index 87% rename from spring-web-reactive/src/main/java/org/springframework/http/server/support/RxNettyHttpServer.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/RxNettyHttpServer.java index f71859cfab..84525ac0d5 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/support/RxNettyHttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/RxNettyHttpServer.java @@ -14,13 +14,13 @@ * limitations under the License. */ -package org.springframework.http.server.support; +package org.springframework.http.server.reactive.boot; import io.netty.buffer.ByteBuf; import org.springframework.beans.factory.InitializingBean; import org.springframework.util.Assert; -import org.springframework.http.server.rxnetty.HttpHandlerRequestHandler; +import org.springframework.http.server.reactive.RxNettyHttpHandlerAdapter; /** @@ -28,7 +28,7 @@ import org.springframework.http.server.rxnetty.HttpHandlerRequestHandler; */ public class RxNettyHttpServer extends HttpServerSupport implements InitializingBean, HttpServer { - private HttpHandlerRequestHandler rxNettyHandler; + private RxNettyHttpHandlerAdapter rxNettyHandler; private io.reactivex.netty.protocol.http.server.HttpServer rxNettyServer; @@ -45,7 +45,7 @@ public class RxNettyHttpServer extends HttpServerSupport implements Initializing public void afterPropertiesSet() throws Exception { Assert.notNull(getHttpHandler()); - this.rxNettyHandler = new HttpHandlerRequestHandler(getHttpHandler()); + this.rxNettyHandler = new RxNettyHttpHandlerAdapter(getHttpHandler()); this.rxNettyServer = (getPort() != -1 ? io.reactivex.netty.protocol.http.server.HttpServer.newServer(getPort()) : diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/support/TomcatHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/TomcatHttpServer.java similarity index 91% rename from spring-web-reactive/src/main/java/org/springframework/http/server/support/TomcatHttpServer.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/TomcatHttpServer.java index ed73f3d31e..b91c3aa49a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/support/TomcatHttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/TomcatHttpServer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.http.server.support; +package org.springframework.http.server.reactive.boot; import java.io.File; @@ -25,7 +25,7 @@ import org.apache.catalina.startup.Tomcat; import org.springframework.beans.factory.InitializingBean; import org.springframework.util.Assert; import org.springframework.util.SocketUtils; -import org.springframework.http.server.servlet31.HttpHandlerServlet; +import org.springframework.http.server.reactive.ServletHttpHandlerAdapter; /** @@ -54,7 +54,7 @@ public class TomcatHttpServer extends HttpServerSupport implements InitializingB this.tomcatServer.setPort(getPort()); Assert.notNull(getHttpHandler()); - HttpHandlerServlet servlet = new HttpHandlerServlet(); + ServletHttpHandlerAdapter servlet = new ServletHttpHandlerAdapter(); servlet.setHandler(getHttpHandler()); File base = new File(System.getProperty("java.io.tmpdir")); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/support/UndertowHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/UndertowHttpServer.java similarity index 88% rename from spring-web-reactive/src/main/java/org/springframework/http/server/support/UndertowHttpServer.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/UndertowHttpServer.java index cc267f4c55..68af14ae4a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/support/UndertowHttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/UndertowHttpServer.java @@ -14,11 +14,11 @@ * limitations under the License. */ -package org.springframework.http.server.support; +package org.springframework.http.server.reactive.boot; import org.springframework.beans.factory.InitializingBean; import org.springframework.util.Assert; -import org.springframework.http.server.undertow.HttpHandlerHttpHandler; +import org.springframework.http.server.reactive.UndertowHttpHandlerAdapter; import io.undertow.Undertow; import io.undertow.server.HttpHandler; @@ -36,7 +36,7 @@ public class UndertowHttpServer extends HttpServerSupport implements Initializin @Override public void afterPropertiesSet() throws Exception { Assert.notNull(getHttpHandler()); - HttpHandler handler = new HttpHandlerHttpHandler(getHttpHandler()); + HttpHandler handler = new UndertowHttpHandlerAdapter(getHttpHandler()); int port = (getPort() != -1 ? getPort() : 8080); this.server = Undertow.builder().addHttpListener(port, "localhost") .setHandler(handler).build(); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/support/package-info.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/package-info.java similarity index 75% rename from spring-web-reactive/src/main/java/org/springframework/http/server/support/package-info.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/package-info.java index f404a34927..56e9fbd187 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/support/package-info.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/package-info.java @@ -2,4 +2,4 @@ * This package contains temporary interfaces and classes for running embedded servers. * They are expected to be replaced by an upcoming Spring Boot support. */ -package org.springframework.http.server.support; +package org.springframework.http.server.reactive.boot; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/servlet31/RequestBodyPublisher.java b/spring-web-reactive/src/main/java/org/springframework/http/server/servlet31/RequestBodyPublisher.java deleted file mode 100644 index 956c00e900..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/servlet31/RequestBodyPublisher.java +++ /dev/null @@ -1,218 +0,0 @@ -/* - * Copyright 2002-2015 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.server.servlet31; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.Arrays; -import java.util.concurrent.atomic.AtomicLong; -import javax.servlet.ReadListener; -import javax.servlet.ServletInputStream; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.reactivestreams.Publisher; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; - -import org.springframework.util.Assert; - -/** - * @author Arjen Poutsma - */ -public class RequestBodyPublisher implements ReadListener, Publisher { - - private static final Log logger = LogFactory.getLog(RequestBodyPublisher.class); - - private final AsyncContextSynchronizer synchronizer; - - private final byte[] buffer; - - private final DemandCounter demand = new DemandCounter(); - - private Subscriber subscriber; - - private boolean stalled; - - private boolean cancelled; - - public RequestBodyPublisher(AsyncContextSynchronizer synchronizer, int bufferSize) { - this.synchronizer = synchronizer; - this.buffer = new byte[bufferSize]; - } - - @Override - public void subscribe(Subscriber subscriber) { - if (subscriber == null) { - throw new NullPointerException(); - } - else if (this.subscriber != null) { - subscriber.onError(new IllegalStateException("Only one subscriber allowed")); - } - this.subscriber = subscriber; - this.subscriber.onSubscribe(new RequestBodySubscription()); - } - - @Override - public void onDataAvailable() throws IOException { - if (cancelled) { - return; - } - ServletInputStream input = this.synchronizer.getInputStream(); - logger.debug("onDataAvailable: " + input); - - while (true) { - logger.debug("Demand: " + this.demand); - - if (!demand.hasDemand()) { - stalled = true; - break; - } - - boolean ready = input.isReady(); - logger.debug("Input ready: " + ready + " finished: " + input.isFinished()); - - if (!ready) { - break; - } - - int read = input.read(buffer); - logger.debug("Input read:" + read); - - if (read == -1) { - break; - } - else if (read > 0) { - this.demand.decrement(); - byte[] copy = Arrays.copyOf(this.buffer, read); - -// logger.debug("Next: " + new String(copy, UTF_8)); - - this.subscriber.onNext(ByteBuffer.wrap(copy)); - - } - } - } - - @Override - public void onAllDataRead() throws IOException { - if (cancelled) { - return; - } - logger.debug("All data read"); - this.synchronizer.readComplete(); - if (this.subscriber != null) { - this.subscriber.onComplete(); - } - } - - @Override - public void onError(Throwable t) { - if (cancelled) { - return; - } - logger.error("RequestBodyPublisher Error", t); - this.synchronizer.readComplete(); - if (this.subscriber != null) { - this.subscriber.onError(t); - } - } - - private class RequestBodySubscription implements Subscription { - - @Override - public void request(long n) { - if (cancelled) { - return; - } - logger.debug("Updating demand " + demand + " by " + n); - - demand.increase(n); - - logger.debug("Stalled: " + stalled); - - if (stalled) { - stalled = false; - try { - onDataAvailable(); - } - catch (IOException ex) { - onError(ex); - } - } - } - - @Override - public void cancel() { - if (cancelled) { - return; - } - cancelled = true; - synchronizer.readComplete(); - demand.reset(); - } - } - - - /** - * Small utility class for keeping track of Reactive Streams demand. - */ - private static final class DemandCounter { - - private final AtomicLong demand = new AtomicLong(); - - /** - * Increases the demand by the given number - * @param n the positive number to increase demand by - * @return the increased demand - * @see org.reactivestreams.Subscription#request(long) - */ - public long increase(long n) { - Assert.isTrue(n > 0, "'n' must be higher than 0"); - return demand.updateAndGet(d -> d != Long.MAX_VALUE ? d + n : Long.MAX_VALUE); - } - - /** - * Decreases the demand by one. - * @return the decremented demand - */ - public long decrement() { - return demand.updateAndGet(d -> d != Long.MAX_VALUE ? d - 1 : Long.MAX_VALUE); - } - - /** - * Indicates whether this counter has demand, i.e. whether it is higher than 0. - * @return {@code true} if this counter has demand; {@code false} otherwise - */ - public boolean hasDemand() { - return this.demand.get() > 0; - } - - /** - * Resets this counter to 0. - * @see org.reactivestreams.Subscription#cancel() - */ - public void reset() { - this.demand.set(0); - } - - @Override - public String toString() { - return demand.toString(); - } - } -} diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/servlet31/ResponseBodySubscriber.java b/spring-web-reactive/src/main/java/org/springframework/http/server/servlet31/ResponseBodySubscriber.java deleted file mode 100644 index a2e82bc076..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/servlet31/ResponseBodySubscriber.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright 2002-2015 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.server.servlet31; - -import java.io.IOException; -import java.nio.ByteBuffer; -import javax.servlet.ServletOutputStream; -import javax.servlet.WriteListener; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; - -import org.springframework.util.Assert; - -/** - * @author Arjen Poutsma - */ -public class ResponseBodySubscriber implements WriteListener, Subscriber { - - private static final Log logger = LogFactory.getLog(ResponseBodySubscriber.class); - - private final AsyncContextSynchronizer synchronizer; - - private Subscription subscription; - - private ByteBuffer buffer; - - private volatile boolean subscriberComplete = false; - - public ResponseBodySubscriber(AsyncContextSynchronizer synchronizer) { - this.synchronizer = synchronizer; - } - - @Override - public void onSubscribe(Subscription subscription) { - this.subscription = subscription; - this.subscription.request(1); - } - - @Override - public void onNext(ByteBuffer bytes) { - - Assert.isNull(buffer); - - this.buffer = bytes; - try { - onWritePossible(); - } - catch (IOException e) { - onError(e); - } - } - - @Override - public void onComplete() { - logger.debug("Complete buffer: " + (buffer == null)); - - this.subscriberComplete = true; - - if (buffer == null) { - this.synchronizer.writeComplete(); - } - } - - @Override - public void onWritePossible() throws IOException { - ServletOutputStream output = this.synchronizer.getOutputStream(); - - boolean ready = output.isReady(); - logger.debug("Output: " + ready + " buffer: " + (buffer == null)); - - if (ready) { - if (this.buffer != null) { - byte[] bytes = new byte[this.buffer.remaining()]; - this.buffer.get(bytes); - this.buffer = null; - output.write(bytes); - if (!subscriberComplete) { - this.subscription.request(1); - } - else { - this.synchronizer.writeComplete(); - } - } - else { - this.subscription.request(1); - } - } - } - - @Override - public void onError(Throwable t) { - logger.error("ResponseBodySubscriber error", t); - } - -} diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/servlet31/Servlet31ServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/servlet31/Servlet31ServerHttpRequest.java deleted file mode 100644 index 5ad43ed444..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/servlet31/Servlet31ServerHttpRequest.java +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright 2002-2015 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.server.servlet31; - -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.util.Enumeration; -import java.util.Map; -import javax.servlet.http.HttpServletRequest; - -import org.reactivestreams.Publisher; - -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.MediaType; -import org.springframework.http.server.ReactiveServerHttpRequest; -import org.springframework.util.Assert; -import org.springframework.util.LinkedCaseInsensitiveMap; -import org.springframework.util.StringUtils; - -/** - * @author Rossen Stoyanchev - */ -public class Servlet31ServerHttpRequest implements ReactiveServerHttpRequest { - - private final HttpServletRequest servletRequest; - - private final Publisher requestBodyPublisher; - - private HttpHeaders headers; - - - public Servlet31ServerHttpRequest(HttpServletRequest servletRequest, - Publisher requestBodyPublisher) { - - Assert.notNull(servletRequest, "HttpServletRequest must not be null"); - this.servletRequest = servletRequest; - this.requestBodyPublisher = requestBodyPublisher; - } - - - @Override - public HttpMethod getMethod() { - return HttpMethod.valueOf(this.servletRequest.getMethod()); - } - - @Override - public URI getURI() { - try { - return new URI(this.servletRequest.getScheme(), null, this.servletRequest.getServerName(), - this.servletRequest.getServerPort(), this.servletRequest.getRequestURI(), - this.servletRequest.getQueryString(), null); - } - catch (URISyntaxException ex) { - throw new IllegalStateException("Could not get HttpServletRequest URI: " + ex.getMessage(), ex); - } - } - - @Override - public HttpHeaders getHeaders() { - if (this.headers == null) { - this.headers = new HttpHeaders(); - for (Enumeration names = this.servletRequest.getHeaderNames(); names.hasMoreElements(); ) { - String headerName = (String) names.nextElement(); - for (Enumeration headerValues = this.servletRequest.getHeaders(headerName); - headerValues.hasMoreElements(); ) { - String headerValue = (String) headerValues.nextElement(); - this.headers.add(headerName, headerValue); - } - } - // HttpServletRequest exposes some headers as properties: we should include those if not already present - MediaType contentType = this.headers.getContentType(); - if (contentType == null) { - String requestContentType = this.servletRequest.getContentType(); - if (StringUtils.hasLength(requestContentType)) { - contentType = MediaType.parseMediaType(requestContentType); - this.headers.setContentType(contentType); - } - } - if (contentType != null && contentType.getCharSet() == null) { - String requestEncoding = this.servletRequest.getCharacterEncoding(); - if (StringUtils.hasLength(requestEncoding)) { - Charset charSet = Charset.forName(requestEncoding); - Map params = new LinkedCaseInsensitiveMap<>(); - params.putAll(contentType.getParameters()); - params.put("charset", charSet.toString()); - MediaType newContentType = new MediaType(contentType.getType(), contentType.getSubtype(), params); - this.headers.setContentType(newContentType); - } - } - if (this.headers.getContentLength() == -1) { - int requestContentLength = this.servletRequest.getContentLength(); - if (requestContentLength != -1) { - this.headers.setContentLength(requestContentLength); - } - } - } - return this.headers; - } - - @Override - public Publisher getBody() { - return this.requestBodyPublisher; - } - -} diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/undertow/RequestBodyPublisher.java b/spring-web-reactive/src/main/java/org/springframework/http/server/undertow/RequestBodyPublisher.java deleted file mode 100644 index 6234e90642..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/undertow/RequestBodyPublisher.java +++ /dev/null @@ -1,248 +0,0 @@ -/* - * Copyright 2002-2015 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.server.undertow; - -import static org.xnio.IoUtils.safeClose; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.concurrent.atomic.AtomicLongFieldUpdater; - -import org.springframework.util.Assert; - -import io.undertow.connector.PooledByteBuffer; -import io.undertow.server.HttpServerExchange; -import io.undertow.util.SameThreadExecutor; -import org.reactivestreams.Publisher; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; -import org.xnio.ChannelListener; -import org.xnio.channels.StreamSourceChannel; -import reactor.core.error.SpecificationExceptions; -import reactor.core.support.BackpressureUtils; - -/** - * @author Marek Hawrylczak - */ -class RequestBodyPublisher implements Publisher { - - private static final AtomicLongFieldUpdater DEMAND = - AtomicLongFieldUpdater.newUpdater(RequestBodySubscription.class, "demand"); - - - private final HttpServerExchange exchange; - - private Subscriber subscriber; - - - public RequestBodyPublisher(HttpServerExchange exchange) { - Assert.notNull(exchange, "'exchange' is required."); - this.exchange = exchange; - } - - - @Override - public void subscribe(Subscriber subscriber) { - if (subscriber == null) { - throw SpecificationExceptions.spec_2_13_exception(); - } - if (this.subscriber != null) { - subscriber.onError(new IllegalStateException("Only one subscriber allowed")); - } - - this.subscriber = subscriber; - this.subscriber.onSubscribe(new RequestBodySubscription()); - } - - - private class RequestBodySubscription implements Subscription, Runnable, - ChannelListener { - - volatile long demand; - - private PooledByteBuffer pooledBuffer; - - private StreamSourceChannel channel; - - private boolean subscriptionClosed; - - private boolean draining; - - - @Override - public void request(long n) { - BackpressureUtils.checkRequest(n, subscriber); - if (this.subscriptionClosed) { - return; - } - BackpressureUtils.getAndAdd(DEMAND, this, n); - scheduleNextMessage(); - } - - private void scheduleNextMessage() { - exchange.dispatch(exchange.isInIoThread() ? SameThreadExecutor.INSTANCE : - exchange.getIoThread(), this); - } - - @Override - public void cancel() { - this.subscriptionClosed = true; - close(); - } - - private void close() { - if (this.pooledBuffer != null) { - safeClose(this.pooledBuffer); - this.pooledBuffer = null; - } - if (this.channel != null) { - safeClose(this.channel); - this.channel = null; - } - } - - @Override - public void run() { - if (this.subscriptionClosed || this.draining) { - return; - } - if (0 == BackpressureUtils.getAndSub(DEMAND, this, 1)) { - return; - } - - this.draining = true; - - if (this.channel == null) { - this.channel = exchange.getRequestChannel(); - - if (this.channel == null) { - if (exchange.isRequestComplete()) { - return; - } - else { - throw new IllegalStateException("Failed to acquire channel!"); - } - } - } - if (this.pooledBuffer == null) { - this.pooledBuffer = exchange.getConnection().getByteBufferPool().allocate(); - } - else { - this.pooledBuffer.getBuffer().clear(); - } - - try { - ByteBuffer buffer = this.pooledBuffer.getBuffer(); - int count; - do { - count = this.channel.read(buffer); - if (count == 0) { - this.channel.getReadSetter().set(this); - this.channel.resumeReads(); - } - else if (count == -1) { - if (buffer.position() > 0) { - doOnNext(buffer); - } - doOnComplete(); - } - else { - if (buffer.remaining() == 0) { - if (this.demand == 0) { - this.channel.suspendReads(); - } - doOnNext(buffer); - if (this.demand > 0) { - scheduleNextMessage(); - } - break; - } - } - } while (count > 0); - } - catch (IOException e) { - doOnError(e); - } - } - - private void doOnNext(ByteBuffer buffer) { - this.draining = false; - buffer.flip(); - subscriber.onNext(buffer); - } - - private void doOnComplete() { - this.subscriptionClosed = true; - try { - subscriber.onComplete(); - } - finally { - close(); - } - } - - private void doOnError(Throwable t) { - this.subscriptionClosed = true; - try { - subscriber.onError(t); - } - finally { - close(); - } - } - - @Override - public void handleEvent(StreamSourceChannel channel) { - if (this.subscriptionClosed) { - return; - } - - try { - ByteBuffer buffer = this.pooledBuffer.getBuffer(); - int count; - do { - count = channel.read(buffer); - if (count == 0) { - return; - } - else if (count == -1) { - if (buffer.position() > 0) { - doOnNext(buffer); - } - doOnComplete(); - } - else { - if (buffer.remaining() == 0) { - if (this.demand == 0) { - channel.suspendReads(); - } - doOnNext(buffer); - if (this.demand > 0) { - scheduleNextMessage(); - } - break; - } - } - } while (count > 0); - } - catch (IOException e) { - doOnError(e); - } - } - } - -} diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/undertow/ResponseBodySubscriber.java b/spring-web-reactive/src/main/java/org/springframework/http/server/undertow/ResponseBodySubscriber.java deleted file mode 100644 index b809e439ed..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/undertow/ResponseBodySubscriber.java +++ /dev/null @@ -1,204 +0,0 @@ -/* - * Copyright 2002-2015 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.server.undertow; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; - -import io.undertow.connector.PooledByteBuffer; -import io.undertow.server.HttpServerExchange; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.reactivestreams.Subscription; -import org.xnio.ChannelListener; -import org.xnio.channels.StreamSinkChannel; -import reactor.core.subscriber.BaseSubscriber; - -import static org.xnio.ChannelListeners.closingChannelExceptionHandler; -import static org.xnio.ChannelListeners.flushingChannelListener; -import static org.xnio.IoUtils.safeClose; - -/** - * @author Marek Hawrylczak - * @author Rossen Stoyanchev - */ -class ResponseBodySubscriber extends BaseSubscriber - implements ChannelListener { - - private static final Log logger = LogFactory.getLog(ResponseBodySubscriber.class); - - - private final HttpServerExchange exchange; - - private Subscription subscription; - - private final Queue buffers; - - private final AtomicInteger writing = new AtomicInteger(); - - private final AtomicBoolean closing = new AtomicBoolean(); - - private StreamSinkChannel responseChannel; - - - public ResponseBodySubscriber(HttpServerExchange exchange) { - this.exchange = exchange; - this.buffers = new ConcurrentLinkedQueue<>(); - } - - - @Override - public void onSubscribe(Subscription subscription) { - super.onSubscribe(subscription); - this.subscription = subscription; - this.subscription.request(1); - } - - @Override - public void onNext(ByteBuffer buffer) { - super.onNext(buffer); - - if (this.responseChannel == null) { - this.responseChannel = this.exchange.getResponseChannel(); - } - - this.writing.incrementAndGet(); - try { - int c; - do { - c = this.responseChannel.write(buffer); - } while (buffer.hasRemaining() && c > 0); - - if (buffer.hasRemaining()) { - this.writing.incrementAndGet(); - enqueue(buffer); - this.responseChannel.getWriteSetter().set(this); - this.responseChannel.resumeWrites(); - } - else { - this.subscription.request(1); - } - - } - catch (IOException ex) { - onError(ex); - } - finally { - this.writing.decrementAndGet(); - if (this.closing.get()) { - closeIfDone(); - } - } - } - - private void enqueue(ByteBuffer src) { - do { - PooledByteBuffer buffer = this.exchange.getConnection().getByteBufferPool().allocate(); - ByteBuffer dst = buffer.getBuffer(); - copy(dst, src); - dst.flip(); - this.buffers.add(buffer); - } while (src.remaining() > 0); - } - - private void copy(ByteBuffer dst, ByteBuffer src) { - int n = Math.min(dst.capacity(), src.remaining()); - for (int i = 0; i < n; i++) { - dst.put(src.get()); - } - } - - @Override - public void handleEvent(StreamSinkChannel channel) { - try { - int c; - do { - ByteBuffer buffer = this.buffers.peek().getBuffer(); - do { - c = channel.write(buffer); - } while (buffer.hasRemaining() && c > 0); - - if (!buffer.hasRemaining()) { - safeClose(this.buffers.remove()); - } - } while (!this.buffers.isEmpty() && c > 0); - - if (!this.buffers.isEmpty()) { - channel.resumeWrites(); - } - else { - this.writing.decrementAndGet(); - - if (this.closing.get()) { - closeIfDone(); - } - else { - this.subscription.request(1); - } - } - } - catch (IOException ex) { - onError(ex); - } - } - - @Override - public void onError(Throwable ex) { - super.onError(ex); - logger.error("ResponseBodySubscriber error", ex); - if (!this.exchange.isResponseStarted() && this.exchange.getStatusCode() < 500) { - this.exchange.setStatusCode(500); - } - } - - @Override - public void onComplete() { - super.onComplete(); - if (this.responseChannel != null) { - this.closing.set(true); - closeIfDone(); - } - } - - private void closeIfDone() { - if (this.writing.get() == 0) { - if (this.closing.compareAndSet(true, false)) { - closeChannel(); - } - } - } - - private void closeChannel() { - try { - this.responseChannel.shutdownWrites(); - - if (!this.responseChannel.flush()) { - this.responseChannel.getWriteSetter().set(flushingChannelListener( - o -> safeClose(this.responseChannel), closingChannelExceptionHandler())); - this.responseChannel.resumeWrites(); - } - this.responseChannel = null; - } - catch (IOException ex) { - onError(ex); - } - } -} diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/undertow/UndertowServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/undertow/UndertowServerHttpRequest.java deleted file mode 100644 index 25094d3f5f..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/undertow/UndertowServerHttpRequest.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2002-2015 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.server.undertow; - -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.ByteBuffer; - -import io.undertow.server.HttpServerExchange; -import io.undertow.util.HeaderValues; -import org.reactivestreams.Publisher; - -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.server.ReactiveServerHttpRequest; - -/** - * @author Marek Hawrylczak - * @author Rossen Stoyanchev - */ -class UndertowServerHttpRequest implements ReactiveServerHttpRequest { - - private final HttpServerExchange exchange; - - private final Publisher body; - - private HttpHeaders headers; - - - public UndertowServerHttpRequest(HttpServerExchange exchange, Publisher body) { - this.exchange = exchange; - this.body = body; - } - - - @Override - public HttpMethod getMethod() { - return HttpMethod.valueOf(this.exchange.getRequestMethod().toString()); - } - - @Override - public URI getURI() { - try { - return new URI(this.exchange.getRequestScheme(), null, this.exchange.getHostName(), - this.exchange.getHostPort(), this.exchange.getRequestURI(), - this.exchange.getQueryString(), null); - } - catch (URISyntaxException ex) { - throw new IllegalStateException("Could not get URI: " + ex.getMessage(), ex); - } - } - - @Override - public HttpHeaders getHeaders() { - if (this.headers == null) { - this.headers = new HttpHeaders(); - for (HeaderValues headerValues : this.exchange.getRequestHeaders()) { - for (String value : headerValues) { - this.headers.add(headerValues.getHeaderName().toString(), value); - } - } - } - return this.headers; - } - - @Override - public Publisher getBody() { - return this.body; - } - -} diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/undertow/UndertowServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/undertow/UndertowServerHttpResponse.java deleted file mode 100644 index 506b2ff181..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/undertow/UndertowServerHttpResponse.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright 2002-2015 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.server.undertow; - -import java.nio.ByteBuffer; -import java.util.List; -import java.util.Map; - -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.server.ReactiveServerHttpResponse; -import org.springframework.util.Assert; - -import io.undertow.server.HttpServerExchange; -import io.undertow.util.HttpString; -import org.reactivestreams.Publisher; -import org.reactivestreams.Subscription; - -/** - * @author Marek Hawrylczak - * @author Rossen Stoyanchev - */ -class UndertowServerHttpResponse implements ReactiveServerHttpResponse { - - private final HttpServerExchange exchange; - - private final ResponseBodySubscriber bodySubscriber; - - private final HttpHeaders headers = new HttpHeaders(); - - private boolean headersWritten = false; - - - public UndertowServerHttpResponse(HttpServerExchange exchange, ResponseBodySubscriber body) { - this.exchange = exchange; - this.bodySubscriber = body; - } - - - @Override - public void setStatusCode(HttpStatus status) { - Assert.notNull(status); - this.exchange.setStatusCode(status.value()); - } - - - @Override - public Publisher setBody(Publisher bodyPublisher) { - applyHeaders(); - return (subscriber -> bodyPublisher.subscribe(bodySubscriber)); - } - - @Override - public HttpHeaders getHeaders() { - return (this.headersWritten ? HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers); - } - - @Override - public Publisher writeHeaders() { - applyHeaders(); - return s -> s.onSubscribe(new Subscription() { - @Override - public void request(long n) { - s.onComplete(); - } - - @Override - public void cancel() { - } - }); - } - - private void applyHeaders() { - if (!this.headersWritten) { - for (Map.Entry> entry : this.headers.entrySet()) { - HttpString headerName = HttpString.tryFromString(entry.getKey()); - this.exchange.getResponseHeaders().addAll(headerName, entry.getValue()); - - } - this.headersWritten = true; - } - } - -} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java index 917a1adb15..56c950a5e8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java @@ -19,7 +19,6 @@ package org.springframework.web.reactive; import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -31,9 +30,9 @@ import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.core.annotation.AnnotationAwareOrderComparator; -import org.springframework.http.server.ReactiveHttpHandler; -import org.springframework.http.server.ReactiveServerHttpRequest; -import org.springframework.http.server.ReactiveServerHttpResponse; +import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; /** * Central dispatcher for HTTP request handlers/controllers. Dispatches to registered @@ -53,7 +52,7 @@ import org.springframework.http.server.ReactiveServerHttpResponse; * @author Rossen Stoyanchev * @author Sebastien Deleuze */ -public class DispatcherHandler implements ReactiveHttpHandler, ApplicationContextAware { +public class DispatcherHandler implements HttpHandler, ApplicationContextAware { private static final Log logger = LogFactory.getLog(DispatcherHandler.class); @@ -94,7 +93,7 @@ public class DispatcherHandler implements ReactiveHttpHandler, ApplicationContex @Override - public Publisher handle(ReactiveServerHttpRequest request, ReactiveServerHttpResponse response) { + public Publisher handle(ServerHttpRequest request, ServerHttpResponse response) { if (logger.isDebugEnabled()) { logger.debug("Processing " + request.getMethod() + " request for [" + request.getURI() + "]"); } @@ -143,7 +142,7 @@ public class DispatcherHandler implements ReactiveHttpHandler, ApplicationContex private static class NotFoundHandlerMapping implements HandlerMapping { @Override - public Publisher getHandler(ReactiveServerHttpRequest request) { + public Publisher getHandler(ServerHttpRequest request) { return Publishers.error(new HandlerNotFoundException(request.getMethod(), request.getURI().getPath(), request.getHeaders())); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerAdapter.java index da3284efee..40f0c7251f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerAdapter.java @@ -18,8 +18,8 @@ package org.springframework.web.reactive; import org.reactivestreams.Publisher; -import org.springframework.http.server.ReactiveServerHttpRequest; -import org.springframework.http.server.ReactiveServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; /** * Interface that must be implemented for each handler type to handle an HTTP request. @@ -54,7 +54,7 @@ public interface HandlerAdapter { * returned {@code true}. * @return A {@link Publisher} object that produces a single {@link HandlerResult} element */ - Publisher handle(ReactiveServerHttpRequest request, - ReactiveServerHttpResponse response, Object handler); + Publisher handle(ServerHttpRequest request, ServerHttpResponse response, + Object handler); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerMapping.java index 5b60cc86da..1f399035a1 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerMapping.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerMapping.java @@ -18,7 +18,7 @@ package org.springframework.web.reactive; import org.reactivestreams.Publisher; -import org.springframework.http.server.ReactiveServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpRequest; /** * Interface to be implemented by objects that define a mapping between @@ -34,6 +34,6 @@ public interface HandlerMapping { * @param request current HTTP request * @return A {@link Publisher} object that produces a single handler element */ - Publisher getHandler(ReactiveServerHttpRequest request); + Publisher getHandler(ServerHttpRequest request); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResultHandler.java index 91a3cb3535..0fc158c120 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResultHandler.java @@ -18,8 +18,8 @@ package org.springframework.web.reactive; import org.reactivestreams.Publisher; -import org.springframework.http.server.ReactiveServerHttpRequest; -import org.springframework.http.server.ReactiveServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; /** * Process the {@link HandlerResult}, usually returned by an {@link HandlerAdapter}. @@ -48,7 +48,7 @@ public interface HandlerResultHandler { * when the handling is complete (success or error) including the flush of the data on the * network. */ - Publisher handleResult(ReactiveServerHttpRequest request, ReactiveServerHttpResponse response, + Publisher handleResult(ServerHttpRequest request, ServerHttpResponse response, HandlerResult result); } \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/HttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/HttpHandlerAdapter.java index 7d98704956..34ae1fcf98 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/HttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/HttpHandlerAdapter.java @@ -20,15 +20,15 @@ import org.reactivestreams.Publisher; import reactor.Publishers; import org.springframework.core.ResolvableType; -import org.springframework.http.server.ReactiveServerHttpRequest; -import org.springframework.http.server.ReactiveServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.web.reactive.HandlerAdapter; import org.springframework.web.reactive.HandlerResult; -import org.springframework.http.server.ReactiveHttpHandler; +import org.springframework.http.server.reactive.HttpHandler; import org.springframework.web.reactive.DispatcherHandler; /** - * Support use of {@link ReactiveHttpHandler} with + * Support use of {@link HttpHandler} with * {@link DispatcherHandler * DispatcherHandler} (which implements the same contract). * The use of {@code DispatcherHandler} this way enables routing requests to @@ -46,14 +46,14 @@ public class HttpHandlerAdapter implements HandlerAdapter { @Override public boolean supports(Object handler) { - return ReactiveHttpHandler.class.isAssignableFrom(handler.getClass()); + return HttpHandler.class.isAssignableFrom(handler.getClass()); } @Override - public Publisher handle(ReactiveServerHttpRequest request, - ReactiveServerHttpResponse response, Object handler) { + public Publisher handle(ServerHttpRequest request, + ServerHttpResponse response, Object handler) { - ReactiveHttpHandler httpHandler = (ReactiveHttpHandler)handler; + HttpHandler httpHandler = (HttpHandler)handler; Publisher completion = httpHandler.handle(request, response); return Publishers.just(new HandlerResult(httpHandler, completion, PUBLISHER_VOID)); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java index 2b5c482eb9..0721f92d06 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java @@ -24,8 +24,8 @@ import reactor.Publishers; import org.springframework.core.Ordered; import org.springframework.core.ResolvableType; import org.springframework.core.convert.ConversionService; -import org.springframework.http.server.ReactiveServerHttpRequest; -import org.springframework.http.server.ReactiveServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.util.Assert; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.HandlerResultHandler; @@ -75,8 +75,8 @@ public class SimpleHandlerResultHandler implements Ordered, HandlerResultHandler } @Override - public Publisher handleResult(ReactiveServerHttpRequest request, - ReactiveServerHttpResponse response, HandlerResult result) { + public Publisher handleResult(ServerHttpRequest request, + ServerHttpResponse response, HandlerResult result) { Object value = result.getValue(); if (Void.TYPE.equals(result.getValueType().getRawClass())) { diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMapping.java index 986c7498ef..b57ede729a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMapping.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMapping.java @@ -20,10 +20,9 @@ import java.util.HashMap; import java.util.Map; import org.reactivestreams.Publisher; -import reactor.Publishers; import reactor.core.publisher.PublisherFactory; -import org.springframework.http.server.ReactiveServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.web.reactive.HandlerMapping; /** @@ -43,7 +42,7 @@ public class SimpleUrlHandlerMapping implements HandlerMapping { @Override - public Publisher getHandler(ReactiveServerHttpRequest request) { + public Publisher getHandler(ServerHttpRequest request) { return PublisherFactory.create(subscriber -> { String path = request.getURI().getPath(); Object handler = this.handlerMap.get(path); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/HandlerMethodArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/HandlerMethodArgumentResolver.java index b2b018904c..a9539b1ae3 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/HandlerMethodArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/HandlerMethodArgumentResolver.java @@ -19,7 +19,7 @@ package org.springframework.web.reactive.method; import org.reactivestreams.Publisher; import org.springframework.core.MethodParameter; -import org.springframework.http.server.ReactiveServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpRequest; /** @@ -34,7 +34,7 @@ public interface HandlerMethodArgumentResolver { * does not allow publishing null values, if the value may be {@code null} * use {@link java.util.Optional#ofNullable(Object)} to wrap it. */ - Publisher resolveArgument(MethodParameter parameter, ReactiveServerHttpRequest request); + Publisher resolveArgument(MethodParameter parameter, ServerHttpRequest request); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java index d7585a6619..491ac774ff 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java @@ -31,7 +31,7 @@ import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.GenericTypeResolver; import org.springframework.core.MethodParameter; import org.springframework.core.ParameterNameDiscoverer; -import org.springframework.http.server.ReactiveServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.util.ReflectionUtils; import org.springframework.web.method.HandlerMethod; @@ -57,7 +57,7 @@ public class InvocableHandlerMethod extends HandlerMethod { } - public Publisher invokeForRequest(ReactiveServerHttpRequest request, + public Publisher invokeForRequest(ServerHttpRequest request, Object... providedArgs) { List> argPublishers = getMethodArguments(request, providedArgs); @@ -88,7 +88,7 @@ public class InvocableHandlerMethod extends HandlerMethod { }); } - private List> getMethodArguments(ReactiveServerHttpRequest request, + private List> getMethodArguments(ServerHttpRequest request, Object... providedArgs) { MethodParameter[] parameters = getMethodParameters(); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestBodyArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestBodyArgumentResolver.java index de3700b47a..ec44088220 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestBodyArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestBodyArgumentResolver.java @@ -26,7 +26,7 @@ import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.core.convert.ConversionService; import org.springframework.http.MediaType; -import org.springframework.http.server.ReactiveServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.core.codec.Decoder; import org.springframework.web.reactive.method.HandlerMethodArgumentResolver; import org.springframework.util.Assert; @@ -57,7 +57,7 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve } @Override - public Publisher resolveArgument(MethodParameter parameter, ReactiveServerHttpRequest request) { + public Publisher resolveArgument(MethodParameter parameter, ServerHttpRequest request) { MediaType mediaType = request.getHeaders().getContentType(); if (mediaType == null) { mediaType = MediaType.APPLICATION_OCTET_STREAM; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java index a73e53a0b9..144bf93ac6 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java @@ -26,8 +26,8 @@ import reactor.Publishers; import org.springframework.beans.factory.InitializingBean; import org.springframework.core.ResolvableType; import org.springframework.core.convert.ConversionService; -import org.springframework.http.server.ReactiveServerHttpRequest; -import org.springframework.http.server.ReactiveServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.core.codec.support.ByteBufferDecoder; import org.springframework.core.codec.Decoder; import org.springframework.core.codec.support.JacksonJsonDecoder; @@ -87,8 +87,8 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Initializin } @Override - public Publisher handle(ReactiveServerHttpRequest request, - ReactiveServerHttpResponse response, Object handler) { + public Publisher handle(ServerHttpRequest request, + ServerHttpResponse response, Object handler) { InvocableHandlerMethod handlerMethod = new InvocableHandlerMethod((HandlerMethod) handler); handlerMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMapping.java index 86e87f7072..f19f7f96cf 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMapping.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMapping.java @@ -34,7 +34,7 @@ import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.http.server.ReactiveServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @@ -94,7 +94,7 @@ public class RequestMappingHandlerMapping implements HandlerMapping, } @Override - public Publisher getHandler(ReactiveServerHttpRequest request) { + public Publisher getHandler(ServerHttpRequest request) { return PublisherFactory.create(subscriber -> { for (Map.Entry entry : this.methodMap.entrySet()) { RequestMappingInfo info = entry.getKey(); @@ -143,7 +143,7 @@ public class RequestMappingHandlerMapping implements HandlerMapping, return this.methods; } - public boolean matchesRequest(ReactiveServerHttpRequest request) { + public boolean matchesRequest(ServerHttpRequest request) { String httpMethod = request.getMethod().name(); return request.getURI().getPath().equals(getPath()) && (getMethods().isEmpty() || getMethods().contains(RequestMethod.valueOf(httpMethod))); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestParamArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestParamArgumentResolver.java index 909adb598b..7c26ae4692 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestParamArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestParamArgumentResolver.java @@ -23,7 +23,7 @@ import org.reactivestreams.Publisher; import reactor.Publishers; import org.springframework.core.MethodParameter; -import org.springframework.http.server.ReactiveServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.web.reactive.method.HandlerMethodArgumentResolver; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.util.UriComponents; @@ -45,7 +45,7 @@ public class RequestParamArgumentResolver implements HandlerMethodArgumentResolv @Override - public Publisher resolveArgument(MethodParameter param, ReactiveServerHttpRequest request) { + public Publisher resolveArgument(MethodParameter param, ServerHttpRequest request) { RequestParam annotation = param.getParameterAnnotation(RequestParam.class); String name = (annotation.value().length() != 0 ? annotation.value() : param.getParameterName()); UriComponents uriComponents = UriComponentsBuilder.fromUri(request.getURI()).build(); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java index 47f2ff691e..d0061d35c8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java @@ -35,8 +35,8 @@ import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.convert.ConversionService; import org.springframework.http.MediaType; -import org.springframework.http.server.ReactiveServerHttpRequest; -import org.springframework.http.server.ReactiveServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.core.codec.Encoder; import org.springframework.util.Assert; import org.springframework.util.MimeType; @@ -127,8 +127,8 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered @Override @SuppressWarnings("unchecked") - public Publisher handleResult(ReactiveServerHttpRequest request, - ReactiveServerHttpResponse response, HandlerResult result) { + public Publisher handleResult(ServerHttpRequest request, + ServerHttpResponse response, HandlerResult result) { Object value = result.getValue(); if (value == null) { @@ -192,7 +192,7 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered return Publishers.error(new HttpMediaTypeNotAcceptableException(this.allMediaTypes)); } - private List getAcceptableMediaTypes(ReactiveServerHttpRequest request) { + private List getAcceptableMediaTypes(ServerHttpRequest request) { List mediaTypes = request.getHeaders().getAccept(); return (mediaTypes.isEmpty() ? Collections.singletonList(MediaType.ALL) : mediaTypes); } diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/AbstractHttpHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/AbstractHttpHandlerIntegrationTests.java index 0f75b552da..4b5b2edd52 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/AbstractHttpHandlerIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/AbstractHttpHandlerIntegrationTests.java @@ -21,12 +21,13 @@ import org.junit.Before; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; -import org.springframework.http.server.support.HttpServer; -import org.springframework.http.server.support.JettyHttpServer; -import org.springframework.http.server.support.ReactorHttpServer; -import org.springframework.http.server.support.RxNettyHttpServer; -import org.springframework.http.server.support.TomcatHttpServer; -import org.springframework.http.server.support.UndertowHttpServer; +import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.http.server.reactive.boot.HttpServer; +import org.springframework.http.server.reactive.boot.JettyHttpServer; +import org.springframework.http.server.reactive.boot.ReactorHttpServer; +import org.springframework.http.server.reactive.boot.RxNettyHttpServer; +import org.springframework.http.server.reactive.boot.TomcatHttpServer; +import org.springframework.http.server.reactive.boot.UndertowHttpServer; import org.springframework.util.SocketUtils; @@ -60,7 +61,7 @@ public abstract class AbstractHttpHandlerIntegrationTests { this.server.start(); } - protected abstract ReactiveHttpHandler createHttpHandler(); + protected abstract HttpHandler createHttpHandler(); @After public void tearDown() throws Exception { diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/EchoHandler.java b/spring-web-reactive/src/test/java/org/springframework/http/server/EchoHandler.java index c43d4ef919..97a7c8cd08 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/EchoHandler.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/EchoHandler.java @@ -18,13 +18,17 @@ package org.springframework.http.server; import org.reactivestreams.Publisher; +import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; + /** * @author Arjen Poutsma */ -public class EchoHandler implements ReactiveHttpHandler { +public class EchoHandler implements HttpHandler { @Override - public Publisher handle(ReactiveServerHttpRequest request, ReactiveServerHttpResponse response) { + public Publisher handle(ServerHttpRequest request, ServerHttpResponse response) { return response.setBody(request.getBody()); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/FilterChainHttpHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/FilterChainHttpHandlerTests.java index 3617318e53..63abca219a 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/FilterChainHttpHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/FilterChainHttpHandlerTests.java @@ -24,6 +24,13 @@ import org.reactivestreams.Publisher; import reactor.Publishers; import reactor.rx.Streams; +import org.springframework.http.server.reactive.FilterChainHttpHandler; +import org.springframework.http.server.reactive.HttpFilter; +import org.springframework.http.server.reactive.HttpFilterChain; +import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; + import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; @@ -33,15 +40,15 @@ import static org.mockito.Mockito.mock; */ public class FilterChainHttpHandlerTests { - private ReactiveServerHttpRequest request; + private ServerHttpRequest request; - private ReactiveServerHttpResponse response; + private ServerHttpResponse response; @Before public void setUp() throws Exception { - this.request = mock(ReactiveServerHttpRequest.class); - this.response = mock(ReactiveServerHttpResponse.class); + this.request = mock(ServerHttpRequest.class); + this.response = mock(ServerHttpResponse.class); } @Test @@ -90,7 +97,7 @@ public class FilterChainHttpHandlerTests { } - private static class TestFilter implements ReactiveHttpFilter { + private static class TestFilter implements HttpFilter { private boolean invoked; @@ -100,15 +107,15 @@ public class FilterChainHttpHandlerTests { } @Override - public Publisher filter(ReactiveServerHttpRequest req, ReactiveServerHttpResponse res, - ReactiveHttpFilterChain chain) { + public Publisher filter(ServerHttpRequest req, ServerHttpResponse res, + HttpFilterChain chain) { this.invoked = true; return doFilter(req, res, chain); } - public Publisher doFilter(ReactiveServerHttpRequest req, ReactiveServerHttpResponse res, - ReactiveHttpFilterChain chain) { + public Publisher doFilter(ServerHttpRequest req, ServerHttpResponse res, + HttpFilterChain chain) { return chain.filter(req, res); } @@ -117,14 +124,14 @@ public class FilterChainHttpHandlerTests { private static class ShortcircuitingFilter extends TestFilter { @Override - public Publisher doFilter(ReactiveServerHttpRequest req, ReactiveServerHttpResponse res, - ReactiveHttpFilterChain chain) { + public Publisher doFilter(ServerHttpRequest req, ServerHttpResponse res, + HttpFilterChain chain) { return Publishers.empty(); } } - private static class StubHandler implements ReactiveHttpHandler { + private static class StubHandler implements HttpHandler { private boolean invoked; @@ -133,7 +140,7 @@ public class FilterChainHttpHandlerTests { } @Override - public Publisher handle(ReactiveServerHttpRequest req, ReactiveServerHttpResponse res) { + public Publisher handle(ServerHttpRequest req, ServerHttpResponse res) { this.invoked = true; return Publishers.empty(); } diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/RandomHandler.java b/spring-web-reactive/src/test/java/org/springframework/http/server/RandomHandler.java index cc94febae4..ab21f071da 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/RandomHandler.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/RandomHandler.java @@ -27,12 +27,16 @@ import org.reactivestreams.Subscription; import reactor.io.buffer.Buffer; import reactor.rx.Streams; +import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; + import static org.junit.Assert.assertEquals; /** * @author Arjen Poutsma */ -public class RandomHandler implements ReactiveHttpHandler { +public class RandomHandler implements HttpHandler { private static final Log logger = LogFactory.getLog(RandomHandler.class); @@ -41,7 +45,7 @@ public class RandomHandler implements ReactiveHttpHandler { private final Random rnd = new Random(); @Override - public Publisher handle(ReactiveServerHttpRequest request, ReactiveServerHttpResponse response) { + public Publisher handle(ServerHttpRequest request, ServerHttpResponse response) { request.getBody().subscribe(new Subscriber() { private Subscription s; diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/XmlHandler.java b/spring-web-reactive/src/test/java/org/springframework/http/server/XmlHandler.java index 81a68d363f..d4cd4eab20 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/XmlHandler.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/XmlHandler.java @@ -27,6 +27,9 @@ import reactor.io.buffer.Buffer; import reactor.rx.Streams; import org.springframework.http.MediaType; +import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.util.BufferOutputStream; import org.springframework.util.ByteBufferPublisherInputStream; @@ -35,13 +38,13 @@ import static org.junit.Assert.fail; /** * @author Arjen Poutsma */ -public class XmlHandler implements ReactiveHttpHandler { +public class XmlHandler implements HttpHandler { private static final Log logger = LogFactory.getLog(XmlHandler.class); @Override - public Publisher handle(ReactiveServerHttpRequest request, - ReactiveServerHttpResponse response) { + public Publisher handle(ServerHttpRequest request, + ServerHttpResponse response) { try { JAXBContext jaxbContext = JAXBContext.newInstance(XmlHandlerIntegrationTests.Person.class); Unmarshaller unmarshaller = jaxbContext.createUnmarshaller(); diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/XmlHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/XmlHandlerIntegrationTests.java index 54777283ed..59bb92ebae 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/XmlHandlerIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/XmlHandlerIntegrationTests.java @@ -23,6 +23,7 @@ import org.junit.Test; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; +import org.springframework.http.server.reactive.HttpHandler; import org.springframework.web.client.RestTemplate; /** @@ -31,7 +32,7 @@ import org.springframework.web.client.RestTemplate; public class XmlHandlerIntegrationTests extends AbstractHttpHandlerIntegrationTests { @Override - protected ReactiveHttpHandler createHttpHandler() { + protected HttpHandler createHttpHandler() { return new XmlHandler(); } diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/servlet31/AsyncContextSynchronizerTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncContextSynchronizerTests.java similarity index 88% rename from spring-web-reactive/src/test/java/org/springframework/http/server/servlet31/AsyncContextSynchronizerTests.java rename to spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncContextSynchronizerTests.java index ea516bec6f..158c7e022e 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/servlet31/AsyncContextSynchronizerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncContextSynchronizerTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.http.server.servlet31; +package org.springframework.http.server.reactive; import javax.servlet.AsyncContext; @@ -31,12 +31,12 @@ public class AsyncContextSynchronizerTests { private AsyncContext asyncContext; - private AsyncContextSynchronizer synchronizer; + private ServletAsyncContextSynchronizer synchronizer; @Before public void setUp() throws Exception { asyncContext = mock(AsyncContext.class); - synchronizer = new AsyncContextSynchronizer(asyncContext); + synchronizer = new ServletAsyncContextSynchronizer(asyncContext); } @Test diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java index 73427f772a..21a8579e24 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java @@ -30,16 +30,14 @@ import reactor.rx.Streams; import org.springframework.http.HttpStatus; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; -import org.springframework.http.server.ReactiveServerHttpRequest; -import org.springframework.http.server.ReactiveServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.web.client.HttpClientErrorException; -import org.springframework.web.client.RestClientException; import org.springframework.web.reactive.DispatcherHandler; import org.springframework.http.server.AbstractHttpHandlerIntegrationTests; -import org.springframework.http.server.ReactiveHttpHandler; +import org.springframework.http.server.reactive.HttpHandler; import org.springframework.web.client.RestTemplate; import org.springframework.web.context.support.StaticWebApplicationContext; -import org.springframework.web.reactive.method.annotation.RequestMappingHandlerMapping; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; @@ -54,7 +52,7 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler @Override - protected ReactiveHttpHandler createHttpHandler() { + protected HttpHandler createHttpHandler() { StaticWebApplicationContext wac = new StaticWebApplicationContext(); wac.registerSingleton("hm", TestHandlerMapping.class); @@ -122,18 +120,18 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler } } - private static class FooHandler implements ReactiveHttpHandler { + private static class FooHandler implements HttpHandler { @Override - public Publisher handle(ReactiveServerHttpRequest request, ReactiveServerHttpResponse response) { + public Publisher handle(ServerHttpRequest request, ServerHttpResponse response) { return response.setBody(Streams.just(Buffer.wrap("foo").byteBuffer())); } } - private static class BarHandler implements ReactiveHttpHandler { + private static class BarHandler implements HttpHandler { @Override - public Publisher handle(ReactiveServerHttpRequest request, ReactiveServerHttpResponse response) { + public Publisher handle(ServerHttpRequest request, ServerHttpResponse response) { return response.setBody(Streams.just(Buffer.wrap("bar").byteBuffer())); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java index 5fda7eb163..62eb6bf69d 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java @@ -29,7 +29,7 @@ import reactor.rx.Streams; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; -import org.springframework.http.server.ReactiveServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @@ -59,7 +59,7 @@ public class RequestMappingHandlerMappingTests { @Test public void path() throws Exception { - ReactiveServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, "boo"); + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, "boo"); Publisher handlerPublisher = this.mapping.getHandler(request); HandlerMethod handlerMethod = toHandlerMethod(handlerPublisher); assertEquals(TestController.class.getMethod("boo"), handlerMethod.getMethod()); @@ -67,7 +67,7 @@ public class RequestMappingHandlerMappingTests { @Test public void method() throws Exception { - ReactiveServerHttpRequest request = new MockServerHttpRequest(HttpMethod.POST, "foo"); + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.POST, "foo"); Publisher handlerPublisher = this.mapping.getHandler(request); HandlerMethod handlerMethod = toHandlerMethod(handlerPublisher); assertEquals(TestController.class.getMethod("postFoo"), handlerMethod.getMethod()); @@ -116,7 +116,7 @@ public class RequestMappingHandlerMappingTests { /** * TODO: this is more widely needed. */ - private static class MockServerHttpRequest implements ReactiveServerHttpRequest { + private static class MockServerHttpRequest implements ServerHttpRequest { private HttpMethod method; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java index c5c4bd5eed..9d062cfb9f 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java @@ -53,7 +53,7 @@ import org.springframework.core.codec.support.StringEncoder; import org.springframework.web.reactive.DispatcherHandler; import org.springframework.web.reactive.handler.SimpleHandlerResultHandler; import org.springframework.http.server.AbstractHttpHandlerIntegrationTests; -import org.springframework.http.server.ReactiveHttpHandler; +import org.springframework.http.server.reactive.HttpHandler; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -75,7 +75,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @Override - protected ReactiveHttpHandler createHttpHandler() { + protected HttpHandler createHttpHandler() { this.wac = new AnnotationConfigWebApplicationContext(); this.wac.register(FrameworkConfig.class, ApplicationConfig.class); this.wac.refresh(); From ce0bd2efc2e4806136fe64426cac01bc44977b6b Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Mon, 7 Dec 2015 11:35:00 +0100 Subject: [PATCH 109/498] Improve Gradle dependencies version management This commit also updates to the following dependencies: - Spring Framework 4.2.3.RELEASE - RxJava 1.1.0 --- spring-web-reactive/build.gradle | 37 ++++++++++++++++---------------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index fee4846c00..5f6300258f 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -30,37 +30,38 @@ configurations.all { resolutionStrategy.cacheChangingModulesFor 0, 'seconds' } +ext { + springVersion = '4.2.3.RELEASE' + reactorVersion = '2.1.0.BUILD-SNAPSHOT' + tomcatVersion = '8.0.28' + jettyVersion = '9.3.5.v20151012' +} + dependencies { - compile "org.springframework:spring-core:4.2.2.RELEASE" - compile "org.springframework:spring-web:4.2.2.RELEASE" + compile "org.springframework:spring-core:${springVersion}" + compile "org.springframework:spring-web:${springVersion}" compile "org.reactivestreams:reactive-streams:1.0.0" - compile "io.projectreactor:reactor-core:2.1.0.BUILD-SNAPSHOT" + compile "io.projectreactor:reactor-core:${reactorVersion}" compile "commons-logging:commons-logging:1.2" - optional "com.fasterxml.jackson.core:jackson-databind:2.6.2" - + optional 'io.reactivex:rxjava:1.1.0' optional "io.reactivex:rxnetty:0.5.0-SNAPSHOT" - - optional "io.projectreactor:reactor-stream:2.1.0.BUILD-SNAPSHOT" - optional "io.projectreactor:reactor-net:2.1.0.BUILD-SNAPSHOT" - - optional 'org.apache.tomcat:tomcat-util:8.0.28' - optional 'org.apache.tomcat.embed:tomcat-embed-core:8.0.28' - + optional "com.fasterxml.jackson.core:jackson-databind:2.6.2" + optional "io.projectreactor:reactor-stream:${reactorVersion}" + optional "io.projectreactor:reactor-net:${reactorVersion}" + optional "org.apache.tomcat:tomcat-util:${tomcatVersion}" + optional "org.apache.tomcat.embed:tomcat-embed-core:${tomcatVersion}" optional 'io.undertow:undertow-core:1.3.5.Final' - - optional 'org.eclipse.jetty:jetty-server:9.3.5.v20151012' - optional 'org.eclipse.jetty:jetty-servlet:9.3.5.v20151012' + optional "org.eclipse.jetty:jetty-server:${jettyVersion}" + optional "org.eclipse.jetty:jetty-servlet:${jettyVersion}" provided "javax.servlet:javax.servlet-api:3.1.0" testCompile "junit:junit:4.12" - testCompile "org.springframework:spring-test:4.2.2.RELEASE" - + testCompile "org.springframework:spring-test:${springVersion}" testCompile "org.slf4j:slf4j-jcl:1.7.12" testCompile "org.slf4j:jul-to-slf4j:1.7.12" testCompile("log4j:log4j:1.2.16") - testCompile("org.mockito:mockito-core:1.10.19") { exclude group: 'org.hamcrest', module: 'hamcrest-core' } From 45706422ddae063b99c3751793eb41b2dd547c41 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Sat, 5 Dec 2015 16:53:00 -0500 Subject: [PATCH 110/498] InvocableHandlerMethod and arg resolution updates General improvements e.g. make use of Java 8 Stream. The main reason for the refactoring however to tighten error handling. To that extent: InvocableHandlerMethod turns all exceptions into Reactive Streams error signals, in effect never allowing any Exceptions to bubble up. HandlerMethodArgumentResolver may throw an Exception for sync resolution or produce an error signal via the returned Publisher. Either way the exception is consistently wrapped with helpful method argument details. For the latter case using a custom mapError operator. HandlerMethodArgumentResolver no longer needs to return Optional for nullable argument values. Instead (for now) the defaultIfEmpty operator of reactor.rx.Stream operator is used to ensure a default constant value (called "NO_VALUE") is produced. That way an argument resolver may produce 0..1 values where 0 means it did not resolve to any value and that results in null passed as the argument value. If a HandlerMethodArgumentResolver produces more than one value, all additional values beyond the first one will be ignored with the help of a custom "first" operator. As HandlerMethod is invoked within the map operator, checked exceptions are not allowed but instead of wrapping it in a runtime exception what we really need is to unwrap the target exception for exception resolution purposes. To this end concatMap is used to produce a nested Publisher or an error Publisher with the unwrapped target exception. Related to that InvocableHandlerMethod now returns Publisher instead of Publisher so that no longer needs to be externally mapped from Object to HandlerResult. InvocableHandlerMethodTests provides tests for the above scenarios and verifies the details of resulting error signals. --- .../method/HandlerMethodArgumentResolver.java | 12 +- .../method/InvocableHandlerMethod.java | 273 ++++++++---------- .../RequestMappingHandlerAdapter.java | 5 +- .../RequestParamArgumentResolver.java | 2 +- .../method/InvocableHandlerMethodTests.java | 236 +++++++++++++++ 5 files changed, 365 insertions(+), 163 deletions(-) create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/HandlerMethodArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/HandlerMethodArgumentResolver.java index a9539b1ae3..1df3064b1f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/HandlerMethodArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/HandlerMethodArgumentResolver.java @@ -27,14 +27,18 @@ import org.springframework.http.server.reactive.ServerHttpRequest; */ public interface HandlerMethodArgumentResolver { + boolean supportsParameter(MethodParameter parameter); /** - * The returned Publisher must produce a single value. As Reactive Streams - * does not allow publishing null values, if the value may be {@code null} - * use {@link java.util.Optional#ofNullable(Object)} to wrap it. + * The returned Publisher is expected to produce a single value -- i.e. the + * value to use to invoke the handler method. Any additional values will be + * ignored. + * + *

    The publisher may also produce zero values if the argument does not + * resolve to any value which will result in passing {@code null} as the + * argument value. */ Publisher resolveArgument(MethodParameter parameter, ServerHttpRequest request); - } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java index 491ac774ff..5b89c254e4 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java @@ -19,21 +19,28 @@ package org.springframework.web.reactive.method; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; -import java.util.Collections; +import java.util.Arrays; import java.util.List; -import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; import org.reactivestreams.Publisher; import reactor.Publishers; import reactor.fn.tuple.Tuple; +import reactor.rx.Streams; import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.GenericTypeResolver; import org.springframework.core.MethodParameter; import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.core.ResolvableType; import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; import org.springframework.web.method.HandlerMethod; +import org.springframework.web.reactive.HandlerResult; /** @@ -41,7 +48,12 @@ import org.springframework.web.method.HandlerMethod; */ public class InvocableHandlerMethod extends HandlerMethod { - private List argumentResolvers = new ArrayList<>(); + public static final Publisher NO_ARGS = Publishers.just(new Object[0]); + + private final static Object NO_VALUE = new Object(); + + + private List resolvers = new ArrayList<>(); private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); @@ -52,179 +64,132 @@ public class InvocableHandlerMethod extends HandlerMethod { public void setHandlerMethodArgumentResolvers(List resolvers) { - this.argumentResolvers.clear(); - this.argumentResolvers.addAll(resolvers); + this.resolvers.clear(); + this.resolvers.addAll(resolvers); + } + + @Override + protected Method getBridgedMethod() { + return super.getBridgedMethod(); } - public Publisher invokeForRequest(ServerHttpRequest request, - Object... providedArgs) { + /** + * + * @param request + * @param providedArgs + * @return Publisher that produces a single HandlerResult or an error signal; + * never throws an exception. + */ + public Publisher invokeForRequest(ServerHttpRequest request, Object... providedArgs) { - List> argPublishers = getMethodArguments(request, providedArgs); - - Publisher argValues = (!argPublishers.isEmpty() ? - Publishers.zip(argPublishers, this::unwrapOptionalArgValues) : - Publishers.just(new Object[0])); - - return Publishers.map(argValues, args -> { - if (logger.isTraceEnabled()) { - logger.trace("Invoking [" + getBeanType().getSimpleName() + "." + - getMethod().getName() + "] method with arguments " + - Collections.singletonList(argPublishers)); + Publisher argsPublisher = NO_ARGS; + try { + if (!ObjectUtils.isEmpty(getMethodParameters())) { + List> publishers = resolveArguments(request, providedArgs); + argsPublisher = Publishers.zip(publishers, this::initArgs); + argsPublisher = first(argsPublisher); } - Object returnValue = null; + } + catch (Throwable ex) { + return Publishers.error(ex); + } + + return Publishers.concatMap(argsPublisher, args -> { try { - returnValue = doInvoke(args); - if (logger.isTraceEnabled()) { - logger.trace("Method [" + getMethod().getName() + "] returned " + - "[" + returnValue + "]"); - } + Object value = doInvoke(args); + + HandlerMethod handlerMethod = InvocableHandlerMethod.this; + ResolvableType type = ResolvableType.forMethodParameter(handlerMethod.getReturnType()); + HandlerResult handlerResult = new HandlerResult(handlerMethod, value, type); + + return Publishers.just(handlerResult); } - catch (Exception ex) { - // TODO: how to best handle error inside map? (also wrapping hides original ex) - throw new IllegalStateException(ex); + catch (InvocationTargetException ex) { + return Publishers.error(ex.getTargetException()); + } + catch (Throwable ex) { + String s = getInvocationErrorMessage(args); + return Publishers.error(new IllegalStateException(s)); } - return returnValue; }); } - private List> getMethodArguments(ServerHttpRequest request, - Object... providedArgs) { - - MethodParameter[] parameters = getMethodParameters(); - List> valuePublishers = new ArrayList<>(parameters.length); - for (int i = 0; i < parameters.length; i++) { - MethodParameter parameter = parameters[i]; - parameter.initParameterNameDiscovery(this.parameterNameDiscoverer); - GenericTypeResolver.resolveParameterType(parameter, getBean().getClass()); - Object value = resolveProvidedArgument(parameter, providedArgs); - if (value != null) { - valuePublishers.add(Publishers.just(value)); - continue; - } - boolean resolved = false; - for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) { - if (resolver.supportsParameter(parameter)) { + private List> resolveArguments(ServerHttpRequest request, Object... providedArgs) { + return Stream.of(getMethodParameters()) + .map(parameter -> { + parameter.initParameterNameDiscovery(this.parameterNameDiscoverer); + GenericTypeResolver.resolveParameterType(parameter, getBean().getClass()); + if (!ObjectUtils.isEmpty(providedArgs)) { + for (Object providedArg : providedArgs) { + if (parameter.getParameterType().isInstance(providedArg)) { + return Publishers.just(providedArg); + } + } + } + HandlerMethodArgumentResolver resolver = this.resolvers.stream() + .filter(r -> r.supportsParameter(parameter)) + .findFirst() + .orElseThrow(() -> getArgError("No resolver for ", parameter, null)); try { - valuePublishers.add(resolver.resolveArgument(parameter, request)); - resolved = true; - break; + Publisher publisher = resolver.resolveArgument(parameter, request); + publisher = mapError(publisher, ex -> getArgError("Error resolving ", parameter, ex)); + return Streams.wrap(publisher).defaultIfEmpty(NO_VALUE); } catch (Exception ex) { - String msg = buildArgErrorMessage("Error resolving argument", i); - valuePublishers.add(Publishers.error(new IllegalStateException(msg, ex))); - break; + throw getArgError("Error resolving ", parameter, ex); } - } - } - if (!resolved) { - String msg = buildArgErrorMessage("No suitable resolver for argument", i); - valuePublishers.add(Publishers.error(new IllegalStateException(msg))); - break; - } + }) + .collect(Collectors.toList()); + } + + private IllegalStateException getArgError(String message, MethodParameter param, Throwable cause) { + return new IllegalStateException(message + + "argument [" + param.getParameterIndex() + "] " + + "of type [" + param.getParameterType().getName() + "] " + + "on method [" + getBridgedMethod().toGenericString() + "]", cause); + } + + private Object doInvoke(Object[] args) throws Exception { + if (logger.isTraceEnabled()) { + String target = getBeanType().getSimpleName() + "." + getMethod().getName(); + logger.trace("Invoking [" + target + "] method with arguments " + Arrays.toString(args)); } - return valuePublishers; - } - - private String buildArgErrorMessage(String message, int index) { - MethodParameter param = getMethodParameters()[index]; - message += " [" + index + "] [type=" + param.getParameterType().getName() + "]"; - return getDetailedErrorMessage(message); - } - - protected String getDetailedErrorMessage(String message) { - return message + "\n" + "HandlerMethod details: \n" + - "Controller [" + getBeanType().getName() + "]\n" + - "Method [" + getBridgedMethod().toGenericString() + "]\n"; - } - - private Object resolveProvidedArgument(MethodParameter parameter, Object... providedArgs) { - if (providedArgs == null) { - return null; - } - for (Object providedArg : providedArgs) { - if (parameter.getParameterType().isInstance(providedArg)) { - return providedArg; - } - } - return null; - } - - private void unwrapOptionalArgValues(Object[] args) { - for (int i = 0; i < args.length; i++) { - if (args[i] instanceof Optional) { - Optional optional = (Optional) args[i]; - args[i] = optional.isPresent() ? optional.get() : null; - } - } - } - - private Object[] unwrapOptionalArgValues(Tuple tuple) { - Object[] args = new Object[tuple.size()]; - for (int i = 0; i < tuple.size(); i++) { - args[i] = tuple.get(i); - if (args[i] instanceof Optional) { - Optional optional = (Optional) args[i]; - args[i] = optional.isPresent() ? optional.get() : null; - } - } - return args; - } - - protected Object doInvoke(Object... args) throws Exception { ReflectionUtils.makeAccessible(getBridgedMethod()); - try { - return getBridgedMethod().invoke(getBean(), args); - } - catch (IllegalArgumentException ex) { - assertTargetBean(getBridgedMethod(), getBean(), args); - throw new IllegalStateException(getInvocationErrorMessage(ex.getMessage(), args), ex); - } - catch (InvocationTargetException ex) { - // Unwrap for HandlerExceptionResolvers ... - Throwable targetException = ex.getTargetException(); - if (targetException instanceof RuntimeException) { - throw (RuntimeException) targetException; - } - else if (targetException instanceof Error) { - throw (Error) targetException; - } - else if (targetException instanceof Exception) { - throw (Exception) targetException; - } - else { - String msg = getInvocationErrorMessage("Failed to invoke controller method", args); - throw new IllegalStateException(msg, targetException); - } + Object returnValue = getBridgedMethod().invoke(getBean(), args); + if (logger.isTraceEnabled()) { + String target = getBeanType().getSimpleName() + "." + getMethod().getName(); + logger.trace("Method [" + target + "] returned [" + returnValue + "]"); } + return returnValue; } - private void assertTargetBean(Method method, Object targetBean, Object[] args) { - Class methodDeclaringClass = method.getDeclaringClass(); - Class targetBeanClass = targetBean.getClass(); - if (!methodDeclaringClass.isAssignableFrom(targetBeanClass)) { - String msg = "The mapped controller method class '" + methodDeclaringClass.getName() + - "' is not an instance of the actual controller bean instance '" + - targetBeanClass.getName() + "'. If the controller requires proxying " + - "(e.g. due to @Transactional), please use class-based proxying."; - throw new IllegalStateException(getInvocationErrorMessage(msg, args)); - } + private String getInvocationErrorMessage(Object[] args) { + String argumentDetails = IntStream.range(0, args.length) + .mapToObj(i -> (args[i] != null ? + "[" + i + "][type=" + args[i].getClass().getName() + "][value=" + args[i] + "]" : + "[" + i + "][null]")) + .collect(Collectors.joining(",", " ", " ")); + return "Failed to invoke controller with resolved arguments:" + argumentDetails + + "on method [" + getBridgedMethod().toGenericString() + "]"; } - private String getInvocationErrorMessage(String message, Object[] resolvedArgs) { - StringBuilder sb = new StringBuilder(getDetailedErrorMessage(message)); - sb.append("Resolved arguments: \n"); - for (int i=0; i < resolvedArgs.length; i++) { - sb.append("[").append(i).append("] "); - if (resolvedArgs[i] == null) { - sb.append("[null] \n"); - } - else { - sb.append("[type=").append(resolvedArgs[i].getClass().getName()).append("] "); - sb.append("[value=").append(resolvedArgs[i]).append("]\n"); - } - } - return sb.toString(); + private Object[] initArgs(Tuple tuple) { + return Stream.of(tuple.toArray()).map(o -> o != NO_VALUE ? o : null).toArray(); + } + + + private static Publisher first(Publisher source) { + return Publishers.lift(source, (e, subscriber) -> { + subscriber.onNext(e); + subscriber.onComplete(); + }); + } + + private static Publisher mapError(Publisher source, Function function) { + return Publishers.lift(source, null, (throwable, subscriber) -> { + subscriber.onError(function.apply(throwable)); + }, null); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java index 144bf93ac6..215718afa8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java @@ -92,10 +92,7 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Initializin InvocableHandlerMethod handlerMethod = new InvocableHandlerMethod((HandlerMethod) handler); handlerMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers); - ResolvableType type = ResolvableType.forMethodParameter(handlerMethod.getReturnType()); - - Publisher resultPublisher = handlerMethod.invokeForRequest(request); - return Publishers.map(resultPublisher, result -> new HandlerResult(handlerMethod, result, type)); + return handlerMethod.invokeForRequest(request); } } \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestParamArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestParamArgumentResolver.java index 7c26ae4692..b50c5ebe8c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestParamArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestParamArgumentResolver.java @@ -50,7 +50,7 @@ public class RequestParamArgumentResolver implements HandlerMethodArgumentResolv String name = (annotation.value().length() != 0 ? annotation.value() : param.getParameterName()); UriComponents uriComponents = UriComponentsBuilder.fromUri(request.getURI()).build(); String value = uriComponents.getQueryParams().getFirst(name); - return Publishers.just(Optional.ofNullable(value)); + return (value != null ? Publishers.just(value) : Publishers.empty()); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java new file mode 100644 index 0000000000..8f27b173c3 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java @@ -0,0 +1,236 @@ +/* + * Copyright 2002-2015 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.web.reactive.method; + + +import java.lang.reflect.Method; +import java.net.URI; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.Before; +import org.junit.Test; +import org.reactivestreams.Publisher; +import reactor.Publishers; +import reactor.rx.Streams; +import reactor.rx.action.Signal; + +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.reactive.HandlerResult; +import org.springframework.web.reactive.method.annotation.RequestParamArgumentResolver; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * @author Rossen Stoyanchev + */ +@SuppressWarnings("ThrowableResultOfMethodCallIgnored") +public class InvocableHandlerMethodTests { + + private static Log logger = LogFactory.getLog(InvocableHandlerMethodTests.class); + + + private ServerHttpRequest request; + + + @Before + public void setUp() throws Exception { + this.request = mock(ServerHttpRequest.class); + } + + + @Test + public void noArgsMethod() throws Exception { + InvocableHandlerMethod hm = createHandlerMethod("noArgs"); + + Publisher publisher = hm.invokeForRequest(this.request); + Object value = awaitValue(publisher); + + assertEquals("success", value); + } + + @Test + public void resolveArgToZeroValues() throws Exception { + when(this.request.getURI()).thenReturn(new URI("http://localhost:8080/path")); + InvocableHandlerMethod hm = createHandlerMethod("singleArg", String.class); + hm.setHandlerMethodArgumentResolvers(Collections.singletonList(new RequestParamArgumentResolver())); + + Publisher publisher = hm.invokeForRequest(this.request); + Object value = awaitValue(publisher); + + assertEquals("success:null", value); + } + + @Test + public void resolveArgToOneValue() throws Exception { + InvocableHandlerMethod hm = createHandlerMethod("singleArg", String.class); + addResolver(hm, Publishers.just("value1")); + + Publisher publisher = hm.invokeForRequest(this.request); + Object value = awaitValue(publisher); + + assertEquals("success:value1", value); + } + + @Test + public void resolveArgToMultipleValues() throws Exception { + InvocableHandlerMethod hm = createHandlerMethod("singleArg", String.class); + addResolver(hm, Publishers.from(Arrays.asList("value1", "value2", "value3"))); + + Publisher publisher = hm.invokeForRequest(this.request); + List> signals = awaitSignals(publisher); + + assertEquals("Expected only one value: " + signals.toString(), 2, signals.size()); + assertEquals(Signal.Type.NEXT, signals.get(0).getType()); + assertEquals(Signal.Type.COMPLETE, signals.get(1).getType()); + assertEquals("success:value1", signals.get(0).get().getValue()); + } + + @Test + public void noResolverForArg() throws Exception { + InvocableHandlerMethod hm = createHandlerMethod("singleArg", String.class); + + Publisher publisher = hm.invokeForRequest(this.request); + Throwable ex = awaitErrorSignal(publisher); + + assertEquals(IllegalStateException.class, ex.getClass()); + assertEquals("No resolver for argument [0] of type [java.lang.String] on method " + + "[" + hm.getMethod().toGenericString() + "]", ex.getMessage()); + } + + @Test + public void resolveArgumentWithThrownException() throws Exception { + HandlerMethodArgumentResolver resolver = mock(HandlerMethodArgumentResolver.class); + when(resolver.supportsParameter(any())).thenReturn(true); + when(resolver.resolveArgument(any(), any())).thenThrow(new IllegalStateException("boo")); + + InvocableHandlerMethod hm = createHandlerMethod("singleArg", String.class); + hm.setHandlerMethodArgumentResolvers(Collections.singletonList(resolver)); + + Publisher publisher = hm.invokeForRequest(this.request); + Throwable ex = awaitErrorSignal(publisher); + + assertEquals(IllegalStateException.class, ex.getClass()); + assertEquals("Exception not wrapped with helpful argument details", + "Error resolving argument [0] of type [java.lang.String] on method " + + "[" + hm.getMethod().toGenericString() + "]", ex.getMessage()); + } + + @Test + public void resolveArgumentWithErrorSignal() throws Exception { + InvocableHandlerMethod hm = createHandlerMethod("singleArg", String.class); + addResolver(hm, Publishers.error(new IllegalStateException("boo"))); + + Publisher publisher = hm.invokeForRequest(this.request); + Throwable ex = awaitErrorSignal(publisher); + + assertEquals(IllegalStateException.class, ex.getClass()); + assertEquals("Exception not wrapped with helpful argument details", + "Error resolving argument [0] of type [java.lang.String] on method " + + "[" + hm.getMethod().toGenericString() + "]", ex.getMessage()); + } + + @Test + public void illegalArgumentExceptionIsWrappedWithHelpfulDetails() throws Exception { + InvocableHandlerMethod hm = createHandlerMethod("singleArg", String.class); + addResolver(hm, Publishers.just(1)); + + Publisher publisher = hm.invokeForRequest(this.request); + Throwable ex = awaitErrorSignal(publisher); + + assertEquals(IllegalStateException.class, ex.getClass()); + assertEquals("Failed to invoke controller with resolved arguments: " + + "[0][type=java.lang.Integer][value=1] " + + "on method [" + hm.getMethod().toGenericString() + "]", ex.getMessage()); + } + + @Test + public void invocationTargetExceptionIsUnwrapped() throws Exception { + InvocableHandlerMethod hm = createHandlerMethod("exceptionMethod"); + + Publisher publisher = hm.invokeForRequest(this.request); + Throwable ex = awaitErrorSignal(publisher); + + assertEquals(IllegalStateException.class, ex.getClass()); + assertEquals("boo", ex.getMessage()); + } + + + private InvocableHandlerMethod createHandlerMethod(String methodName, Class... argTypes) throws Exception { + Object controller = new TestController(); + Method method = controller.getClass().getMethod(methodName, argTypes); + return new InvocableHandlerMethod(new HandlerMethod(controller, method)); + } + + private void addResolver(InvocableHandlerMethod handlerMethod, Publisher resolvedValue) { + HandlerMethodArgumentResolver resolver = mock(HandlerMethodArgumentResolver.class); + when(resolver.supportsParameter(any())).thenReturn(true); + when(resolver.resolveArgument(any(), any())).thenReturn(resolvedValue); + handlerMethod.setHandlerMethodArgumentResolvers(Collections.singletonList(resolver)); + } + + private Object awaitValue(Publisher publisher) throws Exception { + Object object = awaitSignal(publisher, Signal.Type.NEXT).get(); + assertEquals(HandlerResult.class, object.getClass()); + return ((HandlerResult) object).getValue(); + } + + private Throwable awaitErrorSignal(Publisher publisher) throws Exception { + return awaitSignal(publisher, Signal.Type.ERROR).getThrowable(); + } + + @SuppressWarnings("unchecked") + private Signal awaitSignal(Publisher publisher, Signal.Type type) throws Exception { + Signal signal = awaitSignals(publisher).get(0); + if (!type.equals(signal.getType()) && signal.isOnError()) { + logger.error("Unexpected error: ", signal.getThrowable()); + } + assertEquals("Unexpected signal: " + signal, type, signal.getType()); + return signal; + } + + private List> awaitSignals(Publisher publisher) throws InterruptedException { + return Streams.wrap(publisher).materialize().toList().await(5, TimeUnit.SECONDS); + } + + + @SuppressWarnings("unused") + private static class TestController { + + public String noArgs() { + return "success"; + } + + public String singleArg(@RequestParam(required=false) String q) { + return "success:" + q; + } + + public void exceptionMethod() { + throw new IllegalStateException("boo"); + } + } + + +} From 4616938525a8385f3c7da786f37867c0854e9a8a Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Tue, 8 Dec 2015 13:21:35 +0000 Subject: [PATCH 111/498] Prepare for a future change to downstream() inner method in reactor-core --- .../springframework/core/codec/support/JsonObjectEncoder.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectEncoder.java index d3079ebe2f..ffe5a85d8c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectEncoder.java @@ -112,7 +112,7 @@ public class JsonObjectEncoder extends AbstractEncoder { buffer.flip(); BackpressureUtils.getAndSub(REQUESTED, this, 1L); - downstream().onNext(buffer.byteBuffer()); + subscriber.onNext(buffer.byteBuffer()); } protected void drainLast(){ @@ -123,7 +123,7 @@ public class JsonObjectEncoder extends AbstractEncoder { buffer.append("]"); } buffer.flip(); - downstream().onNext(buffer.byteBuffer()); + subscriber.onNext(buffer.byteBuffer()); super.doComplete(); } } From ad4be9462bf575ebcb9eacfd03d071a9669049cb Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 8 Dec 2015 12:10:49 -0500 Subject: [PATCH 112/498] Drop use of WebApplicationContext in tests It brings along Servlet imports. --- ...mpleUrlHandlerMappingIntegrationTests.java | 10 +++++----- .../RequestMappingHandlerMappingTests.java | 4 ++-- .../RequestMappingIntegrationTests.java | 20 +++++++++---------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java index 21a8579e24..4ffb123a28 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java @@ -27,17 +27,17 @@ import org.reactivestreams.Publisher; import reactor.io.buffer.Buffer; import reactor.rx.Streams; +import org.springframework.context.support.StaticApplicationContext; import org.springframework.http.HttpStatus; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; +import org.springframework.http.server.AbstractHttpHandlerIntegrationTests; +import org.springframework.http.server.reactive.HttpHandler; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.web.client.HttpClientErrorException; -import org.springframework.web.reactive.DispatcherHandler; -import org.springframework.http.server.AbstractHttpHandlerIntegrationTests; -import org.springframework.http.server.reactive.HttpHandler; import org.springframework.web.client.RestTemplate; -import org.springframework.web.context.support.StaticWebApplicationContext; +import org.springframework.web.reactive.DispatcherHandler; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; @@ -54,7 +54,7 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler @Override protected HttpHandler createHttpHandler() { - StaticWebApplicationContext wac = new StaticWebApplicationContext(); + StaticApplicationContext wac = new StaticApplicationContext(); wac.registerSingleton("hm", TestHandlerMapping.class); wac.registerSingleton("ha", HttpHandlerAdapter.class); wac.registerSingleton("rh", SimpleHandlerResultHandler.class); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java index 62eb6bf69d..f3eac33527 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java @@ -27,13 +27,13 @@ import org.junit.Test; import org.reactivestreams.Publisher; import reactor.rx.Streams; +import org.springframework.context.support.StaticApplicationContext; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.context.support.StaticWebApplicationContext; import org.springframework.web.method.HandlerMethod; import static org.junit.Assert.assertEquals; @@ -49,7 +49,7 @@ public class RequestMappingHandlerMappingTests { @Before public void setup() { - StaticWebApplicationContext wac = new StaticWebApplicationContext(); + StaticApplicationContext wac = new StaticApplicationContext(); wac.registerSingleton("handlerMapping", RequestMappingHandlerMapping.class); wac.registerSingleton("controller", TestController.class); wac.refresh(); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java index 9d062cfb9f..e10e22fd31 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java @@ -33,10 +33,15 @@ import reactor.rx.Streams; import rx.Observable; import rx.Single; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ResolvableType; +import org.springframework.core.codec.support.ByteBufferEncoder; +import org.springframework.core.codec.support.JacksonJsonEncoder; +import org.springframework.core.codec.support.JsonObjectEncoder; +import org.springframework.core.codec.support.StringEncoder; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter; @@ -46,12 +51,6 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; -import org.springframework.core.codec.support.ByteBufferEncoder; -import org.springframework.core.codec.support.JacksonJsonEncoder; -import org.springframework.core.codec.support.JsonObjectEncoder; -import org.springframework.core.codec.support.StringEncoder; -import org.springframework.web.reactive.DispatcherHandler; -import org.springframework.web.reactive.handler.SimpleHandlerResultHandler; import org.springframework.http.server.AbstractHttpHandlerIntegrationTests; import org.springframework.http.server.reactive.HttpHandler; import org.springframework.stereotype.Controller; @@ -60,7 +59,8 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.client.RestTemplate; -import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; +import org.springframework.web.reactive.DispatcherHandler; +import org.springframework.web.reactive.handler.SimpleHandlerResultHandler; import static org.junit.Assert.assertEquals; @@ -71,12 +71,12 @@ import static org.junit.Assert.assertEquals; */ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrationTests { - private AnnotationConfigWebApplicationContext wac; + private AnnotationConfigApplicationContext wac; @Override protected HttpHandler createHttpHandler() { - this.wac = new AnnotationConfigWebApplicationContext(); + this.wac = new AnnotationConfigApplicationContext(); this.wac.register(FrameworkConfig.class, ApplicationConfig.class); this.wac.refresh(); @@ -474,7 +474,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @RequestMapping("/observable-create") public Observable observableCreate(@RequestBody Observable personStream) { - return personStream.toList().doOnNext(p -> persons.addAll(p)).flatMap(document -> Observable.empty()); + return personStream.toList().doOnNext(persons::addAll).flatMap(document -> Observable.empty()); } //TODO add mixed and T request mappings tests From fbb0c702c98920f38a7d96ceb254e5c8dbb2556e Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Wed, 9 Dec 2015 01:58:26 +0000 Subject: [PATCH 113/498] minor reactor server impl to fulfill core introspection --- .../reactive/boot/ReactorHttpServer.java | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java index c1bfe69d16..c335796d0c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java @@ -16,6 +16,7 @@ package org.springframework.http.server.reactive.boot; +import reactor.core.support.ReactiveState; import reactor.io.buffer.Buffer; import reactor.io.net.ReactiveNet; @@ -27,7 +28,7 @@ import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter; * @author Stephane Maldini */ public class ReactorHttpServer extends HttpServerSupport - implements InitializingBean, HttpServer { + implements InitializingBean, HttpServer, ReactiveState.FeedbackLoop, ReactiveState.ActiveUpstream { private ReactorHttpHandlerAdapter reactorHandler; @@ -50,6 +51,26 @@ public class ReactorHttpServer extends HttpServerSupport ReactiveNet.httpServer()); } + @Override + public Object delegateInput() { + return reactorServer; + } + + @Override + public Object delegateOutput() { + return reactorServer; + } + + @Override + public boolean isStarted() { + return running; + } + + @Override + public boolean isTerminated() { + return !running; + } + @Override public void start() { if (!this.running) { From 1dcaff8a5cefa16f82118dc81812ce58685c51d3 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 8 Dec 2015 15:13:30 -0500 Subject: [PATCH 114/498] Updates to Javadoc --- .../reactive/FilterChainHttpHandler.java | 4 ++-- .../http/server/reactive/HttpFilter.java | 18 ++++++++++++++++- .../http/server/reactive/HttpFilterChain.java | 9 +++++++++ .../http/server/reactive/HttpHandler.java | 20 ++++++------------- 4 files changed, 34 insertions(+), 17 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/FilterChainHttpHandler.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/FilterChainHttpHandler.java index 746807b9a7..729f5dd36b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/FilterChainHttpHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/FilterChainHttpHandler.java @@ -24,8 +24,8 @@ import org.reactivestreams.Publisher; import org.springframework.util.Assert; /** - * An {@link HttpHandler} decorator that delegates to a list of - * {@link HttpFilter}s and the target {@link HttpHandler}. + * {@link HttpHandler} that delegates to a chain of {@link HttpFilter}s followed + * by a target {@link HttpHandler}. * * @author Rossen Stoyanchev */ diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpFilter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpFilter.java index 9e9d5fd500..525fb4d625 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpFilter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpFilter.java @@ -19,11 +19,27 @@ package org.springframework.http.server.reactive; import org.reactivestreams.Publisher; /** + * Contract for interception-style, chained processing of HTTP requests. + * + *

    Filters may be used to implement cross-cutting, application-agnostic + * requirements such as security, timeouts, and others. + * + *

    {@link FilterChainHttpHandler} provides a way of constructing a chain of + * {@link HttpFilter}s followed by a target {@link HttpHandler}. + * * @author Rossen Stoyanchev + * @see FilterChainHttpHandler */ public interface HttpFilter { - + /** + * Process the given request and optionally delegate to the next HttpFilter. + * + * @param request current HTTP request. + * @param response current HTTP response. + * @param chain provides a way to delegate to the next HttpFilter. + * @return Publisher to indicate when request processing is complete. + */ Publisher filter(ServerHttpRequest request, ServerHttpResponse response, HttpFilterChain chain); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpFilterChain.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpFilterChain.java index 1c15d90791..a18a644f2d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpFilterChain.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpFilterChain.java @@ -19,10 +19,19 @@ import org.reactivestreams.Publisher; /** + * Represents a chain of {@link HttpFilter}s allowing each {@link HttpFilter} to + * delegate to the next in the chain. + * * @author Rossen Stoyanchev */ public interface HttpFilterChain { + /** + * + * @param request current HTTP request. + * @param response current HTTP response. + * @return Publisher to indicate when request handling is complete. + */ Publisher filter(ServerHttpRequest request, ServerHttpResponse response); } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpHandler.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpHandler.java index f11cc13a2d..ab0e93197a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpHandler.java @@ -19,29 +19,21 @@ package org.springframework.http.server.reactive; import org.reactivestreams.Publisher; /** - * Interface for handlers that process HTTP requests and generate an HTTP response. - * This handler is designed to be called when the HTTP headers have been received, making - * the HTTP request body available as stream. The HTTP response body can also be written - * as a stream. + * Contract for handling HTTP requests in a non-blocking way. * * @author Arjen Poutsma * @author Rossen Stoyanchev * @author Sebastien Deleuze - * @see ServerHttpRequest#getBody() - * @see ServerHttpResponse#setBody(Publisher) + * @see HttpFilter */ public interface HttpHandler { /** - * Process the given request, generating a response in an asynchronous non blocking way. - * Implementations should not throw exceptions but signal them via the returned - * {@code Publisher}. + * Handle the given request and generate a response. * - * @param request current HTTP request, the body can be processed as a data stream. - * @param response current HTTP response, the body can be provided as a data stream. - * @return A {@code Publisher} used to signal the demand, and receive a notification - * when the handling is complete (success or error) including the flush of the data on the - * network. + * @param request current HTTP request. + * @param response current HTTP response. + * @return Publisher to indicate when request handling is complete. */ Publisher handle(ServerHttpRequest request, ServerHttpResponse response); From ef3560a55a0925a0be099803f039b2ab95be201f Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 8 Dec 2015 15:29:54 -0500 Subject: [PATCH 115/498] Add HttpHandlerDecorator and fix test package --- .../reactive/FilterChainHttpHandler.java | 10 ++-- .../server/reactive/HttpHandlerDecorator.java | 52 +++++++++++++++++++ .../AbstractHttpHandlerIntegrationTests.java | 2 +- .../server/{ => reactive}/EchoHandler.java | 2 +- .../EchoHandlerIntegrationTests.java | 2 +- .../FilterChainHttpHandlerTests.java | 9 +--- .../server/{ => reactive}/RandomHandler.java | 6 +-- .../RandomHandlerIntegrationTests.java | 2 +- .../server/{ => reactive}/XmlHandler.java | 5 +- .../XmlHandlerIntegrationTests.java | 3 +- ...mpleUrlHandlerMappingIntegrationTests.java | 2 +- .../RequestMappingIntegrationTests.java | 2 +- 12 files changed, 65 insertions(+), 32 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpHandlerDecorator.java rename spring-web-reactive/src/test/java/org/springframework/http/server/{ => reactive}/AbstractHttpHandlerIntegrationTests.java (97%) rename spring-web-reactive/src/test/java/org/springframework/http/server/{ => reactive}/EchoHandler.java (95%) rename spring-web-reactive/src/test/java/org/springframework/http/server/{ => reactive}/EchoHandlerIntegrationTests.java (98%) rename spring-web-reactive/src/test/java/org/springframework/http/server/{ => reactive}/FilterChainHttpHandlerTests.java (89%) rename spring-web-reactive/src/test/java/org/springframework/http/server/{ => reactive}/RandomHandler.java (90%) rename spring-web-reactive/src/test/java/org/springframework/http/server/{ => reactive}/RandomHandlerIntegrationTests.java (97%) rename spring-web-reactive/src/test/java/org/springframework/http/server/{ => reactive}/XmlHandler.java (91%) rename spring-web-reactive/src/test/java/org/springframework/http/server/{ => reactive}/XmlHandlerIntegrationTests.java (95%) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/FilterChainHttpHandler.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/FilterChainHttpHandler.java index 729f5dd36b..224e0fcf9d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/FilterChainHttpHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/FilterChainHttpHandler.java @@ -21,7 +21,6 @@ import java.util.List; import org.reactivestreams.Publisher; -import org.springframework.util.Assert; /** * {@link HttpHandler} that delegates to a chain of {@link HttpFilter}s followed @@ -29,17 +28,14 @@ import org.springframework.util.Assert; * * @author Rossen Stoyanchev */ -public class FilterChainHttpHandler implements HttpHandler { +public class FilterChainHttpHandler extends HttpHandlerDecorator { private final List filters; - private final HttpHandler targetHandler; - public FilterChainHttpHandler(HttpHandler targetHandler, HttpFilter... filters) { - Assert.notNull(targetHandler, "'targetHandler' is required."); + super(targetHandler); this.filters = (filters != null ? Arrays.asList(filters) : Collections.emptyList()); - this.targetHandler = targetHandler; } @@ -60,7 +56,7 @@ public class FilterChainHttpHandler implements HttpHandler { return filter.filter(request, response, this); } else { - return targetHandler.handle(request, response); + return getDelegate().handle(request, response); } } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpHandlerDecorator.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpHandlerDecorator.java new file mode 100644 index 0000000000..e8ba2baf77 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpHandlerDecorator.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2015 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.server.reactive; + +import org.reactivestreams.Publisher; + +import org.springframework.util.Assert; + +/** + * + * @author Rossen Stoyanchev + */ +public class HttpHandlerDecorator implements HttpHandler { + + private final HttpHandler delegate; + + + public HttpHandlerDecorator(HttpHandler delegate) { + Assert.notNull(delegate, "'delegate' must not be null"); + this.delegate = delegate; + } + + + public HttpHandler getDelegate() { + return this.delegate; + } + + + @Override + public Publisher handle(ServerHttpRequest request, ServerHttpResponse response) { + return this.delegate.handle(request, response); + } + + @Override + public String toString() { + return getClass().getSimpleName() + " [delegate=" + this.delegate + "]"; + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/AbstractHttpHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AbstractHttpHandlerIntegrationTests.java similarity index 97% rename from spring-web-reactive/src/test/java/org/springframework/http/server/AbstractHttpHandlerIntegrationTests.java rename to spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AbstractHttpHandlerIntegrationTests.java index 4b5b2edd52..fb11e7b151 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/AbstractHttpHandlerIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AbstractHttpHandlerIntegrationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.http.server; +package org.springframework.http.server.reactive; import org.junit.After; import org.junit.Before; diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/EchoHandler.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/EchoHandler.java similarity index 95% rename from spring-web-reactive/src/test/java/org/springframework/http/server/EchoHandler.java rename to spring-web-reactive/src/test/java/org/springframework/http/server/reactive/EchoHandler.java index 97a7c8cd08..684f1f52be 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/EchoHandler.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/EchoHandler.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.http.server; +package org.springframework.http.server.reactive; import org.reactivestreams.Publisher; diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/EchoHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/EchoHandlerIntegrationTests.java similarity index 98% rename from spring-web-reactive/src/test/java/org/springframework/http/server/EchoHandlerIntegrationTests.java rename to spring-web-reactive/src/test/java/org/springframework/http/server/reactive/EchoHandlerIntegrationTests.java index 8d4b114843..5a27960114 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/EchoHandlerIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/EchoHandlerIntegrationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.http.server; +package org.springframework.http.server.reactive; import java.net.URI; import java.util.Random; diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/FilterChainHttpHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/FilterChainHttpHandlerTests.java similarity index 89% rename from spring-web-reactive/src/test/java/org/springframework/http/server/FilterChainHttpHandlerTests.java rename to spring-web-reactive/src/test/java/org/springframework/http/server/reactive/FilterChainHttpHandlerTests.java index 63abca219a..c53e01395c 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/FilterChainHttpHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/FilterChainHttpHandlerTests.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.http.server; +package org.springframework.http.server.reactive; import java.util.concurrent.TimeUnit; @@ -24,13 +24,6 @@ import org.reactivestreams.Publisher; import reactor.Publishers; import reactor.rx.Streams; -import org.springframework.http.server.reactive.FilterChainHttpHandler; -import org.springframework.http.server.reactive.HttpFilter; -import org.springframework.http.server.reactive.HttpFilterChain; -import org.springframework.http.server.reactive.HttpHandler; -import org.springframework.http.server.reactive.ServerHttpRequest; -import org.springframework.http.server.reactive.ServerHttpResponse; - import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/RandomHandler.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/RandomHandler.java similarity index 90% rename from spring-web-reactive/src/test/java/org/springframework/http/server/RandomHandler.java rename to spring-web-reactive/src/test/java/org/springframework/http/server/reactive/RandomHandler.java index ab21f071da..97af86dc70 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/RandomHandler.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/RandomHandler.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.http.server; +package org.springframework.http.server.reactive; import java.nio.ByteBuffer; import java.util.Random; @@ -27,10 +27,6 @@ import org.reactivestreams.Subscription; import reactor.io.buffer.Buffer; import reactor.rx.Streams; -import org.springframework.http.server.reactive.HttpHandler; -import org.springframework.http.server.reactive.ServerHttpRequest; -import org.springframework.http.server.reactive.ServerHttpResponse; - import static org.junit.Assert.assertEquals; /** diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/RandomHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/RandomHandlerIntegrationTests.java similarity index 97% rename from spring-web-reactive/src/test/java/org/springframework/http/server/RandomHandlerIntegrationTests.java rename to spring-web-reactive/src/test/java/org/springframework/http/server/reactive/RandomHandlerIntegrationTests.java index ba3f76a5d0..d7925d3ca0 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/RandomHandlerIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/RandomHandlerIntegrationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.http.server; +package org.springframework.http.server.reactive; import java.net.URI; import java.util.Random; diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/XmlHandler.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/XmlHandler.java similarity index 91% rename from spring-web-reactive/src/test/java/org/springframework/http/server/XmlHandler.java rename to spring-web-reactive/src/test/java/org/springframework/http/server/reactive/XmlHandler.java index d4cd4eab20..838d1ad877 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/XmlHandler.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/XmlHandler.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.http.server; +package org.springframework.http.server.reactive; import javax.xml.bind.JAXBContext; import javax.xml.bind.Marshaller; @@ -27,9 +27,6 @@ import reactor.io.buffer.Buffer; import reactor.rx.Streams; import org.springframework.http.MediaType; -import org.springframework.http.server.reactive.HttpHandler; -import org.springframework.http.server.reactive.ServerHttpRequest; -import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.util.BufferOutputStream; import org.springframework.util.ByteBufferPublisherInputStream; diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/XmlHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/XmlHandlerIntegrationTests.java similarity index 95% rename from spring-web-reactive/src/test/java/org/springframework/http/server/XmlHandlerIntegrationTests.java rename to spring-web-reactive/src/test/java/org/springframework/http/server/reactive/XmlHandlerIntegrationTests.java index 59bb92ebae..c805886bf1 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/XmlHandlerIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/XmlHandlerIntegrationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.http.server; +package org.springframework.http.server.reactive; import java.net.URI; import javax.xml.bind.annotation.XmlRootElement; @@ -23,7 +23,6 @@ import org.junit.Test; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; -import org.springframework.http.server.reactive.HttpHandler; import org.springframework.web.client.RestTemplate; /** diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java index 4ffb123a28..0cc303e0c9 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java @@ -31,7 +31,7 @@ import org.springframework.context.support.StaticApplicationContext; import org.springframework.http.HttpStatus; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; -import org.springframework.http.server.AbstractHttpHandlerIntegrationTests; +import org.springframework.http.server.reactive.AbstractHttpHandlerIntegrationTests; import org.springframework.http.server.reactive.HttpHandler; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java index e10e22fd31..dbb016a39f 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java @@ -51,7 +51,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; -import org.springframework.http.server.AbstractHttpHandlerIntegrationTests; +import org.springframework.http.server.reactive.AbstractHttpHandlerIntegrationTests; import org.springframework.http.server.reactive.HttpHandler; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestBody; From 78454f5a2e0f4a07a3a9d2cc01f1b2c909fb11fc Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Wed, 9 Dec 2015 11:18:50 +0100 Subject: [PATCH 116/498] Add a link to Spring Reactive Playground --- spring-web-reactive/README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/spring-web-reactive/README.md b/spring-web-reactive/README.md index 8b017c24b0..4bddcd39ce 100644 --- a/spring-web-reactive/README.md +++ b/spring-web-reactive/README.md @@ -8,6 +8,10 @@ Spring Reactive JAR dependency is available from Spring snapshot repository: - GroupId: `org.springframework.reactive` - ArtifactId: `spring-reactive` - Version: `0.1.0.BUILD-SNAPSHOT` + +## Sample application +[Spring Reactive Playground] is a sample application based on Spring Reactive and on MongoDB, +Couchbase and PostgreSQL Reactive database drivers. ## Building from Source Spring Reactive uses a [Gradle][]-based build system. In the instructions @@ -36,6 +40,7 @@ Feel free to send us your feedback on the [issue tracker][]; [Pull requests][] a The Spring Reactive is released under version 2.0 of the [Apache License][]. +[Spring Reactive Playground]: https://github.com/sdeleuze/spring-reactive-playground [Gradle]: http://gradle.org [Bamboo Spring Reactive build]: https://build.spring.io/browse/SR-PUB [Git]: http://help.github.com/set-up-git-redirect From 448aac813a24a434e9deb51995ba12e6f00d892f Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 9 Dec 2015 14:43:22 -0500 Subject: [PATCH 117/498] Add MockServerHttpRequest/Response --- .../reactive/MockServerHttpRequest.java | 87 +++++++++++++++++++ .../reactive/MockServerHttpResponse.java | 67 ++++++++++++++ .../RequestMappingHandlerMappingTests.java | 50 +---------- 3 files changed, 158 insertions(+), 46 deletions(-) create mode 100644 spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpRequest.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpRequest.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpRequest.java new file mode 100644 index 0000000000..c5ffbc83fa --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpRequest.java @@ -0,0 +1,87 @@ +/* + * Copyright 2002-2015 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.server.reactive; + +import java.net.URI; +import java.nio.ByteBuffer; + +import org.reactivestreams.Publisher; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; + +/** + * @author Rossen Stoyanchev + */ +public class MockServerHttpRequest implements ServerHttpRequest { + + private HttpMethod httpMethod; + + private URI uri; + + private HttpHeaders headers = new HttpHeaders(); + + private Publisher body; + + + public MockServerHttpRequest(HttpMethod httpMethod, URI uri) { + this.httpMethod = httpMethod; + this.uri = uri; + } + + public MockServerHttpRequest(Publisher body, HttpMethod httpMethod, URI uri) { + this.body = body; + this.httpMethod = httpMethod; + this.uri = uri; + } + + + @Override + public HttpMethod getMethod() { + return this.httpMethod; + } + + public void setHttpMethod(HttpMethod httpMethod) { + this.httpMethod = httpMethod; + } + + @Override + public URI getURI() { + return this.uri; + } + + public void setUri(URI uri) { + this.uri = uri; + } + + @Override + public HttpHeaders getHeaders() { + return this.headers; + } + + public void setHeaders(HttpHeaders headers) { + this.headers = headers; + } + + @Override + public Publisher getBody() { + return this.body; + } + + public void setBody(Publisher body) { + this.body = body; + } +} diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java new file mode 100644 index 0000000000..694951ff38 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2015 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.server.reactive; + +import java.nio.ByteBuffer; + +import org.reactivestreams.Publisher; +import reactor.Publishers; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; + +/** + * @author Rossen Stoyanchev + */ +public class MockServerHttpResponse implements ServerHttpResponse { + + private HttpStatus status; + + private HttpHeaders headers = new HttpHeaders(); + + private Publisher body; + + + @Override + public void setStatusCode(HttpStatus status) { + this.status = status; + } + + public HttpStatus getStatus() { + return this.status; + } + + @Override + public HttpHeaders getHeaders() { + return this.headers; + } + + @Override + public Publisher writeHeaders() { + return Publishers.empty(); + } + + @Override + public Publisher setBody(Publisher body) { + this.body = body; + return Publishers.empty(); + } + + public Publisher getBody() { + return this.body; + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java index f3eac33527..4bbe2cd443 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java @@ -17,8 +17,6 @@ package org.springframework.web.reactive.method.annotation; import java.net.URI; -import java.net.URISyntaxException; -import java.nio.ByteBuffer; import java.util.List; import java.util.concurrent.TimeUnit; @@ -28,8 +26,8 @@ import org.reactivestreams.Publisher; import reactor.rx.Streams; import org.springframework.context.support.StaticApplicationContext; -import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; +import org.springframework.http.server.reactive.MockServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @@ -59,7 +57,7 @@ public class RequestMappingHandlerMappingTests { @Test public void path() throws Exception { - ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, "boo"); + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("boo")); Publisher handlerPublisher = this.mapping.getHandler(request); HandlerMethod handlerMethod = toHandlerMethod(handlerPublisher); assertEquals(TestController.class.getMethod("boo"), handlerMethod.getMethod()); @@ -67,12 +65,12 @@ public class RequestMappingHandlerMappingTests { @Test public void method() throws Exception { - ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.POST, "foo"); + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.POST, new URI("foo")); Publisher handlerPublisher = this.mapping.getHandler(request); HandlerMethod handlerMethod = toHandlerMethod(handlerPublisher); assertEquals(TestController.class.getMethod("postFoo"), handlerMethod.getMethod()); - request = new MockServerHttpRequest(HttpMethod.GET, "foo"); + request = new MockServerHttpRequest(HttpMethod.GET, new URI("foo")); handlerPublisher = this.mapping.getHandler(request); handlerMethod = toHandlerMethod(handlerPublisher); assertEquals(TestController.class.getMethod("getFoo"), handlerMethod.getMethod()); @@ -112,44 +110,4 @@ public class RequestMappingHandlerMappingTests { } - - /** - * TODO: this is more widely needed. - */ - private static class MockServerHttpRequest implements ServerHttpRequest { - - private HttpMethod method; - - private URI uri; - - public MockServerHttpRequest(HttpMethod method, String path) { - this.method = method; - try { - this.uri = new URI(path); - } catch (URISyntaxException ex) { - throw new IllegalStateException("Could not get URI: " + ex.getMessage(), ex); - } - } - - @Override - public Publisher getBody() { - return null; - } - - @Override - public HttpMethod getMethod() { - return this.method; - } - - @Override - public URI getURI() { - return this.uri; - } - - @Override - public HttpHeaders getHeaders() { - return null; - } - } - } From 4ba3d0736f6f14ba780845ca4095559c8c756aad Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 9 Dec 2015 14:44:05 -0500 Subject: [PATCH 118/498] Add HttpExceptionHandler --- .../reactive/ErrorHandlingHttpHandler.java | 73 ++++++++ .../server/reactive/HttpExceptionHandler.java | 43 +++++ .../ErrorHandlingHttpHandlerTests.java | 168 ++++++++++++++++++ .../method/InvocableHandlerMethodTests.java | 54 ++---- 4 files changed, 300 insertions(+), 38 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ErrorHandlingHttpHandler.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpExceptionHandler.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ErrorHandlingHttpHandlerTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ErrorHandlingHttpHandler.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ErrorHandlingHttpHandler.java new file mode 100644 index 0000000000..755e83bb53 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ErrorHandlingHttpHandler.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2015 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.server.reactive; + +import java.util.Arrays; +import java.util.List; + +import org.reactivestreams.Publisher; +import reactor.Publishers; +import reactor.core.publisher.convert.RxJava1Converter; +import rx.Observable; + +import org.springframework.util.Assert; + +/** + * {@link HttpHandler} that delegates to a target {@link HttpHandler} and handles + * any errors from it by invoking one or more {@link HttpExceptionHandler}s + * sequentially until one of them completes successfully. + * + * @author Rossen Stoyanchev + */ +public class ErrorHandlingHttpHandler extends HttpHandlerDecorator { + + private final List exceptionHandlers; + + + public ErrorHandlingHttpHandler(HttpHandler targetHandler, HttpExceptionHandler... exceptionHandlers) { + super(targetHandler); + Assert.notEmpty(exceptionHandlers, "At least one exception handler is required"); + this.exceptionHandlers = Arrays.asList(exceptionHandlers); + } + + + @Override + public Publisher handle(ServerHttpRequest request, ServerHttpResponse response) { + Publisher publisher; + try { + publisher = getDelegate().handle(request, response); + } + catch (Throwable ex) { + publisher = Publishers.error(ex); + } + for (HttpExceptionHandler handler : this.exceptionHandlers) { + publisher = applyExceptionHandler(publisher, handler, request, response); + } + return publisher; + } + + private static Publisher applyExceptionHandler(Publisher publisher, + HttpExceptionHandler handler, ServerHttpRequest request, ServerHttpResponse response) { + + // see https://github.com/reactor/reactor/issues/580 + + Observable observable = RxJava1Converter.from(publisher).onErrorResumeNext(ex -> { + return RxJava1Converter.from(handler.handle(request, response, ex)); + }); + return RxJava1Converter.from(observable); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpExceptionHandler.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpExceptionHandler.java new file mode 100644 index 0000000000..4fb95c20b6 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpExceptionHandler.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2015 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.server.reactive; + +import org.reactivestreams.Publisher; + +/** + * A contract for resolving exceptions from HTTP request handling. + * + *

    {@link ErrorHandlingHttpHandler} provides a way of applying a list + * {@link HttpExceptionHandler}s to a target {@link HttpHandler}. + * + * @author Rossen Stoyanchev + * @see ErrorHandlingHttpHandler + */ +public interface HttpExceptionHandler { + + /** + * Handle the given exception and return a completion Publisher to indicate + * when error handling is complete, or send an error signal if the exception + * was not handled. + * + * @param request the current request + * @param response the current response + * @param ex the exception to handle + * @return Publisher to indicate when exception handling is complete. + */ + Publisher handle(ServerHttpRequest request, ServerHttpResponse response, Throwable ex); + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ErrorHandlingHttpHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ErrorHandlingHttpHandlerTests.java new file mode 100644 index 0000000000..f59220ac5a --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ErrorHandlingHttpHandlerTests.java @@ -0,0 +1,168 @@ +/* + * Copyright 2002-2015 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.server.reactive; + + +import java.net.URI; +import java.util.concurrent.TimeUnit; + +import org.junit.Before; +import org.junit.Test; +import org.reactivestreams.Publisher; +import reactor.Publishers; +import reactor.rx.Streams; +import reactor.rx.action.Signal; + +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +/** + * @author Rossen Stoyanchev + */ +@SuppressWarnings("ThrowableResultOfMethodCallIgnored") +public class ErrorHandlingHttpHandlerTests { + + private MockServerHttpRequest request; + + private MockServerHttpResponse response; + + + @Before + public void setUp() throws Exception { + this.request = new MockServerHttpRequest(HttpMethod.GET, new URI("http://localhost:8080")); + this.response = new MockServerHttpResponse(); + } + + + @Test + public void handleErrorSignal() throws Exception { + HttpExceptionHandler exceptionHandler = new UnresolvedExceptionHandler(); + HttpHandler targetHandler = new TestHttpHandler(new IllegalStateException("boo")); + HttpHandler handler = new ErrorHandlingHttpHandler(targetHandler, exceptionHandler); + + Publisher publisher = handler.handle(this.request, this.response); + Streams.wrap(publisher).toList().await(5, TimeUnit.SECONDS); + + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, this.response.getStatus()); + } + + @Test + public void handleErrorSignalWithMultipleHttpErrorHandlers() throws Exception { + HttpExceptionHandler[] exceptionHandlers = new HttpExceptionHandler[] { + new UnresolvedExceptionHandler(), + new UnresolvedExceptionHandler(), + new HttpStatusExceptionHandler(HttpStatus.INTERNAL_SERVER_ERROR), + new UnresolvedExceptionHandler() + }; + HttpHandler targetHandler = new TestHttpHandler(new IllegalStateException("boo")); + HttpHandler httpHandler = new ErrorHandlingHttpHandler(targetHandler, exceptionHandlers); + + Publisher publisher = httpHandler.handle(this.request, this.response); + Streams.wrap(publisher).toList().await(5, TimeUnit.SECONDS); + + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, this.response.getStatus()); + } + + @Test + public void unresolvedException() throws Exception { + HttpExceptionHandler exceptionHandler = new UnresolvedExceptionHandler(); + HttpHandler targetHandler = new TestHttpHandler(new IllegalStateException("boo")); + HttpHandler httpHandler = new ErrorHandlingHttpHandler(targetHandler, exceptionHandler); + + Publisher publisher = httpHandler.handle(this.request, this.response); + Throwable ex = awaitErrorSignal(publisher); + + assertEquals("boo", ex.getMessage()); + assertNull(this.response.getStatus()); + } + + @Test + public void thrownExceptionBecomesErrorSignal() throws Exception { + HttpExceptionHandler exceptionHandler = new HttpStatusExceptionHandler(HttpStatus.INTERNAL_SERVER_ERROR); + HttpHandler targetHandler = new TestHttpHandler(new IllegalStateException("boo"), true); + HttpHandler handler = new ErrorHandlingHttpHandler(targetHandler, exceptionHandler); + + Publisher publisher = handler.handle(this.request, this.response); + Streams.wrap(publisher).toList().await(5, TimeUnit.SECONDS); + + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, this.response.getStatus()); + } + + + private Throwable awaitErrorSignal(Publisher publisher) throws Exception { + Signal signal = Streams.wrap(publisher).materialize().toList().await(5, TimeUnit.SECONDS).get(0); + assertEquals("Unexpected signal: " + signal, Signal.Type.ERROR, signal.getType()); + return signal.getThrowable(); + } + + + private static class TestHttpHandler implements HttpHandler { + + private final Throwable exception; + + private final boolean raise; + + + public TestHttpHandler(Throwable exception) { + this(exception, false); + } + + public TestHttpHandler(Throwable exception, boolean raise) { + this.exception = exception; + this.raise = raise; + assertTrue(exception instanceof RuntimeException || !this.raise); + } + + @Override + public Publisher handle(ServerHttpRequest request, ServerHttpResponse response) { + if (this.raise) { + throw (RuntimeException) exception; + } + return Publishers.error(this.exception); + } + } + + + /** Leave the exception unresolved. */ + private static class UnresolvedExceptionHandler implements HttpExceptionHandler { + + @Override + public Publisher handle(ServerHttpRequest request, ServerHttpResponse response, Throwable ex) { + return Publishers.error(ex); + } + } + + /** Set the response status to the given HttpStatus. */ + private static class HttpStatusExceptionHandler implements HttpExceptionHandler { + + private final HttpStatus status; + + public HttpStatusExceptionHandler(HttpStatus status) { + this.status = status; + } + + @Override + public Publisher handle(ServerHttpRequest request, ServerHttpResponse response, Throwable ex) { + response.setStatusCode(this.status); + return Publishers.empty(); + } + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java index 8f27b173c3..fbd669e514 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java @@ -23,8 +23,6 @@ import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; import org.junit.Before; import org.junit.Test; import org.reactivestreams.Publisher; @@ -49,9 +47,6 @@ import static org.mockito.Mockito.when; @SuppressWarnings("ThrowableResultOfMethodCallIgnored") public class InvocableHandlerMethodTests { - private static Log logger = LogFactory.getLog(InvocableHandlerMethodTests.class); - - private ServerHttpRequest request; @@ -66,9 +61,10 @@ public class InvocableHandlerMethodTests { InvocableHandlerMethod hm = createHandlerMethod("noArgs"); Publisher publisher = hm.invokeForRequest(this.request); - Object value = awaitValue(publisher); + List values = Streams.wrap(publisher).toList().await(5, TimeUnit.SECONDS); - assertEquals("success", value); + assertEquals(1, values.size()); + assertEquals("success", values.get(0).getValue()); } @Test @@ -78,9 +74,10 @@ public class InvocableHandlerMethodTests { hm.setHandlerMethodArgumentResolvers(Collections.singletonList(new RequestParamArgumentResolver())); Publisher publisher = hm.invokeForRequest(this.request); - Object value = awaitValue(publisher); + List values = Streams.wrap(publisher).toList().await(5, TimeUnit.SECONDS); - assertEquals("success:null", value); + assertEquals(1, values.size()); + assertEquals("success:null", values.get(0).getValue()); } @Test @@ -89,9 +86,10 @@ public class InvocableHandlerMethodTests { addResolver(hm, Publishers.just("value1")); Publisher publisher = hm.invokeForRequest(this.request); - Object value = awaitValue(publisher); + List values = Streams.wrap(publisher).toList().await(5, TimeUnit.SECONDS); - assertEquals("success:value1", value); + assertEquals(1, values.size()); + assertEquals("success:value1", values.get(0).getValue()); } @Test @@ -100,12 +98,10 @@ public class InvocableHandlerMethodTests { addResolver(hm, Publishers.from(Arrays.asList("value1", "value2", "value3"))); Publisher publisher = hm.invokeForRequest(this.request); - List> signals = awaitSignals(publisher); + List values = Streams.wrap(publisher).toList().await(5, TimeUnit.SECONDS); - assertEquals("Expected only one value: " + signals.toString(), 2, signals.size()); - assertEquals(Signal.Type.NEXT, signals.get(0).getType()); - assertEquals(Signal.Type.COMPLETE, signals.get(1).getType()); - assertEquals("success:value1", signals.get(0).get().getValue()); + assertEquals(1, values.size()); + assertEquals("success:value1", values.get(0).getValue()); } @Test @@ -191,28 +187,10 @@ public class InvocableHandlerMethodTests { handlerMethod.setHandlerMethodArgumentResolvers(Collections.singletonList(resolver)); } - private Object awaitValue(Publisher publisher) throws Exception { - Object object = awaitSignal(publisher, Signal.Type.NEXT).get(); - assertEquals(HandlerResult.class, object.getClass()); - return ((HandlerResult) object).getValue(); - } - - private Throwable awaitErrorSignal(Publisher publisher) throws Exception { - return awaitSignal(publisher, Signal.Type.ERROR).getThrowable(); - } - - @SuppressWarnings("unchecked") - private Signal awaitSignal(Publisher publisher, Signal.Type type) throws Exception { - Signal signal = awaitSignals(publisher).get(0); - if (!type.equals(signal.getType()) && signal.isOnError()) { - logger.error("Unexpected error: ", signal.getThrowable()); - } - assertEquals("Unexpected signal: " + signal, type, signal.getType()); - return signal; - } - - private List> awaitSignals(Publisher publisher) throws InterruptedException { - return Streams.wrap(publisher).materialize().toList().await(5, TimeUnit.SECONDS); + private Throwable awaitErrorSignal(Publisher publisher) throws Exception { + Signal signal = Streams.wrap(publisher).materialize().toList().await(5, TimeUnit.SECONDS).get(0); + assertEquals("Unexpected signal: " + signal, Signal.Type.ERROR, signal.getType()); + return signal.getThrowable(); } From a0018d13e19f7fe7ef7a98979476d9cd2851a10c Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 10 Dec 2015 16:15:11 -0500 Subject: [PATCH 119/498] Add DispatcherHandlerErrorTests The tests demonstrate failures at various phases of request processing and the resulting error signals. --- .../core/codec/support/AbstractDecoder.java | 3 + .../core/codec/support/AbstractEncoder.java | 3 + .../web/reactive/DispatcherHandler.java | 2 +- .../RequestMappingHandlerAdapter.java | 9 +- .../annotation/ResponseBodyResultHandler.java | 22 +- .../src/main/resources/log4j.properties | 2 +- .../ErrorHandlingHttpHandlerTests.java | 9 +- .../reactive/FilterChainHttpHandlerTests.java | 50 +++- .../reactive/MockServerHttpResponse.java | 2 +- .../reactive/DispatcherHandlerErrorTests.java | 282 ++++++++++++++++++ 10 files changed, 359 insertions(+), 25 deletions(-) create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractDecoder.java index 9f4b8b5705..fb00faab0c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractDecoder.java @@ -44,6 +44,9 @@ public abstract class AbstractDecoder implements Decoder { @Override public boolean canDecode(ResolvableType type, MimeType mimeType, Object... hints) { + if (mimeType == null) { + return true; + } for (MimeType supportedMimeType : this.supportedMimeTypes) { if (supportedMimeType.isCompatibleWith(mimeType)) { return true; diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractEncoder.java index 8010042158..d0d1920194 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractEncoder.java @@ -44,6 +44,9 @@ public abstract class AbstractEncoder implements Encoder { @Override public boolean canEncode(ResolvableType type, MimeType mimeType, Object... hints) { + if (mimeType == null) { + return true; + } for (MimeType supportedMimeType : this.supportedMimeTypes) { if (supportedMimeType.isCompatibleWith(mimeType)) { return true; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java index 56c950a5e8..444f14a7c9 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java @@ -128,7 +128,7 @@ public class DispatcherHandler implements HttpHandler, ApplicationContextAware { return resultHandler; } } - throw new IllegalStateException("No HandlerResultHandler: " + handlerResult.getValue()); + throw new IllegalStateException("No HandlerResultHandler for " + handlerResult.getValue()); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java index 215718afa8..d997913e7d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java @@ -26,6 +26,7 @@ import reactor.Publishers; import org.springframework.beans.factory.InitializingBean; import org.springframework.core.ResolvableType; import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.core.codec.support.ByteBufferDecoder; @@ -33,6 +34,7 @@ import org.springframework.core.codec.Decoder; import org.springframework.core.codec.support.JacksonJsonDecoder; import org.springframework.core.codec.support.JsonObjectDecoder; import org.springframework.core.codec.support.StringDecoder; +import org.springframework.util.ObjectUtils; import org.springframework.web.reactive.HandlerAdapter; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.method.HandlerMethodArgumentResolver; @@ -45,9 +47,9 @@ import org.springframework.web.method.HandlerMethod; */ public class RequestMappingHandlerAdapter implements HandlerAdapter, InitializingBean { - private List argumentResolvers; + private final List argumentResolvers = new ArrayList<>(); - private ConversionService conversionService; + private ConversionService conversionService = new DefaultConversionService(); public void setArgumentResolvers(List resolvers) { @@ -70,12 +72,11 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Initializin @Override public void afterPropertiesSet() throws Exception { - if (this.argumentResolvers == null) { + if (ObjectUtils.isEmpty(this.argumentResolvers)) { List> decoders = Arrays.asList(new ByteBufferDecoder(), new StringDecoder(), new JacksonJsonDecoder(new JsonObjectDecoder())); - this.argumentResolvers = new ArrayList<>(); this.argumentResolvers.add(new RequestParamArgumentResolver()); this.argumentResolvers.add(new RequestBodyArgumentResolver(decoders, this.conversionService)); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java index d0061d35c8..d0d4ab6340 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java @@ -135,10 +135,20 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered return Publishers.empty(); } + Publisher publisher; + ResolvableType elementType; ResolvableType returnType = result.getValueType(); + if (this.conversionService.canConvert(returnType.getRawClass(), Publisher.class)) { + publisher = this.conversionService.convert(value, Publisher.class); + elementType = returnType.getGeneric(0); + } + else { + publisher = Publishers.just(value); + elementType = returnType; + } List requestedMediaTypes = getAcceptableMediaTypes(request); - List producibleMediaTypes = getProducibleMediaTypes(returnType); + List producibleMediaTypes = getProducibleMediaTypes(elementType); if (producibleMediaTypes.isEmpty()) { producibleMediaTypes.add(MediaType.ALL); @@ -172,16 +182,6 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered } if (selectedMediaType != null) { - Publisher publisher; - ResolvableType elementType; - if (this.conversionService.canConvert(returnType.getRawClass(), Publisher.class)) { - publisher = this.conversionService.convert(value, Publisher.class); - elementType = returnType.getGeneric(0); - } - else { - publisher = Publishers.just(value); - elementType = returnType; - } Encoder encoder = resolveEncoder(elementType, selectedMediaType); if (encoder != null) { response.getHeaders().setContentType(selectedMediaType); diff --git a/spring-web-reactive/src/main/resources/log4j.properties b/spring-web-reactive/src/main/resources/log4j.properties index b5bf0d4dfb..34659ab78b 100644 --- a/spring-web-reactive/src/main/resources/log4j.properties +++ b/spring-web-reactive/src/main/resources/log4j.properties @@ -1,6 +1,6 @@ log4j.rootCategory=WARN, stdout -log4j.logger.org.springframework.reactive=DEBUG +log4j.logger.org.springframework.http=DEBUG log4j.logger.org.springframework.web=DEBUG log4j.logger.reactor=INFO diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ErrorHandlingHttpHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ErrorHandlingHttpHandlerTests.java index f59220ac5a..fe80a41bcc 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ErrorHandlingHttpHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ErrorHandlingHttpHandlerTests.java @@ -115,25 +115,24 @@ public class ErrorHandlingHttpHandlerTests { private static class TestHttpHandler implements HttpHandler { - private final Throwable exception; + private final RuntimeException exception; private final boolean raise; - public TestHttpHandler(Throwable exception) { + public TestHttpHandler(RuntimeException exception) { this(exception, false); } - public TestHttpHandler(Throwable exception, boolean raise) { + public TestHttpHandler(RuntimeException exception, boolean raise) { this.exception = exception; this.raise = raise; - assertTrue(exception instanceof RuntimeException || !this.raise); } @Override public Publisher handle(ServerHttpRequest request, ServerHttpResponse response) { if (this.raise) { - throw (RuntimeException) exception; + throw this.exception; } return Publishers.error(this.exception); } diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/FilterChainHttpHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/FilterChainHttpHandlerTests.java index c53e01395c..cd3bad68ab 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/FilterChainHttpHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/FilterChainHttpHandlerTests.java @@ -16,14 +16,26 @@ package org.springframework.http.server.reactive; +import java.util.Arrays; +import java.util.concurrent.Callable; +import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.log4j.spi.LoggerFactory; import org.junit.Before; import org.junit.Test; import org.reactivestreams.Publisher; import reactor.Publishers; +import reactor.core.publisher.PublisherFactory; +import reactor.fn.timer.Timer; import reactor.rx.Streams; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; +import org.springframework.util.ObjectUtils; + import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; @@ -33,6 +45,9 @@ import static org.mockito.Mockito.mock; */ public class FilterChainHttpHandlerTests { + private static Log logger = LogFactory.getLog(FilterChainHttpHandlerTests.class); + + private ServerHttpRequest request; private ServerHttpResponse response; @@ -89,10 +104,24 @@ public class FilterChainHttpHandlerTests { assertFalse(handler.invoked()); } + @Test + public void asyncFilter() throws Exception { + StubHandler handler = new StubHandler(); + AsyncFilter filter = new AsyncFilter(); + FilterChainHttpHandler filterHandler = new FilterChainHttpHandler(handler, filter); + + Publisher voidPublisher = filterHandler.handle(this.request, this.response); + Streams.wrap(voidPublisher).toList().await(10, TimeUnit.SECONDS); + + assertTrue(filter.invoked()); + assertTrue(handler.invoked()); + } + + private static class TestFilter implements HttpFilter { - private boolean invoked; + private volatile boolean invoked; public boolean invoked() { @@ -124,9 +153,25 @@ public class FilterChainHttpHandlerTests { } } + private static class AsyncFilter extends TestFilter { + + @Override + public Publisher doFilter(ServerHttpRequest req, ServerHttpResponse res, HttpFilterChain chain) { + return Publishers.concatMap(doAsyncWork(), asyncResult -> { + logger.debug("Async result: " + asyncResult); + return chain.filter(req, res); + }); + } + + private Publisher doAsyncWork() { + return Publishers.just("123"); + } + } + + private static class StubHandler implements HttpHandler { - private boolean invoked; + private volatile boolean invoked; public boolean invoked() { return this.invoked; @@ -134,6 +179,7 @@ public class FilterChainHttpHandlerTests { @Override public Publisher handle(ServerHttpRequest req, ServerHttpResponse res) { + logger.trace("StubHandler invoked."); this.invoked = true; return Publishers.empty(); } diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java index 694951ff38..ba12b8d76d 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java @@ -57,7 +57,7 @@ public class MockServerHttpResponse implements ServerHttpResponse { @Override public Publisher setBody(Publisher body) { this.body = body; - return Publishers.empty(); + return Publishers.concatMap(body, b -> Publishers.empty()); } public Publisher getBody() { diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java new file mode 100644 index 0000000000..0027fd31d6 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java @@ -0,0 +1,282 @@ +/* + * Copyright 2002-2015 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.web.reactive; + +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.junit.Before; +import org.junit.Test; +import org.reactivestreams.Publisher; +import reactor.Publishers; +import reactor.rx.Streams; +import reactor.rx.action.Signal; + +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.codec.Encoder; +import org.springframework.core.codec.support.StringEncoder; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.server.reactive.ErrorHandlingHttpHandler; +import org.springframework.http.server.reactive.FilterChainHttpHandler; +import org.springframework.http.server.reactive.HttpExceptionHandler; +import org.springframework.http.server.reactive.HttpFilter; +import org.springframework.http.server.reactive.HttpFilterChain; +import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.stereotype.Controller; +import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.reactive.method.annotation.RequestMappingHandlerAdapter; +import org.springframework.web.reactive.method.annotation.RequestMappingHandlerMapping; +import org.springframework.web.reactive.method.annotation.ResponseBodyResultHandler; + +import static org.hamcrest.CoreMatchers.startsWith; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThat; + +/** + * Test the effect of exceptions at different stages of request processing by + * checking the error signals on the completion publisher. + * + * @author Rossen Stoyanchev + */ +@SuppressWarnings({"ThrowableResultOfMethodCallIgnored", "ThrowableInstanceNeverThrown"}) +public class DispatcherHandlerErrorTests { + + public static final IllegalStateException EXCEPTION = new IllegalStateException("boo"); + + + private DispatcherHandler dispatcherHandler; + + private MockServerHttpRequest request; + + private MockServerHttpResponse response; + + + @Before + public void setUp() throws Exception { + AnnotationConfigApplicationContext appContext = new AnnotationConfigApplicationContext(); + appContext.register(TestConfig.class); + appContext.refresh(); + + this.dispatcherHandler = new DispatcherHandler(); + this.dispatcherHandler.setApplicationContext(appContext); + + this.request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); + this.response = new MockServerHttpResponse(); + } + + + @Test + public void noHandler() throws Exception { + this.request.setUri(new URI("/does-not-exist")); + + Publisher publisher = this.dispatcherHandler.handle(this.request, this.response); + Throwable ex = awaitErrorSignal(publisher); + + assertEquals(HandlerNotFoundException.class, ex.getClass()); + } + + @Test + public void noResolverForArgument() throws Exception { + this.request.setUri(new URI("/uknown-argument-type")); + + Publisher publisher = this.dispatcherHandler.handle(this.request, this.response); + Throwable ex = awaitErrorSignal(publisher); + + assertEquals(IllegalStateException.class, ex.getClass()); + assertThat(ex.getMessage(), startsWith("No resolver for argument [0]")); + } + + @Test + public void controllerMethodError() throws Exception { + this.request.setUri(new URI("/error-signal")); + + Publisher publisher = this.dispatcherHandler.handle(this.request, this.response); + Throwable ex = awaitErrorSignal(publisher); + + assertSame(EXCEPTION, ex); + } + + @Test + public void controllerMethodWithThrownException() throws Exception { + this.request.setUri(new URI("/raise-exception")); + + Publisher publisher = this.dispatcherHandler.handle(this.request, this.response); + Throwable ex = awaitErrorSignal(publisher); + + assertSame(EXCEPTION, ex); + } + + @Test + public void noHandlerResultHandler() throws Exception { + this.request.setUri(new URI("/unknown-return-type")); + + Publisher publisher = this.dispatcherHandler.handle(this.request, this.response); + Throwable ex = awaitErrorSignal(publisher); + + assertEquals(IllegalStateException.class, ex.getClass()); + assertThat(ex.getMessage(), startsWith("No HandlerResultHandler")); + } + + @Test + public void notAcceptable() throws Exception { + this.request.setUri(new URI("/request-body")); + this.request.getHeaders().setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); + this.request.setBody(Publishers.just(ByteBuffer.wrap("body".getBytes("UTF-8")))); + + Publisher publisher = this.dispatcherHandler.handle(this.request, this.response); + Throwable ex = awaitErrorSignal(publisher); + + assertEquals(HttpMediaTypeNotAcceptableException.class, ex.getClass()); + } + + @Test + public void requestBodyError() throws Exception { + this.request.setUri(new URI("/request-body")); + this.request.setBody(Publishers.error(EXCEPTION)); + + Publisher publisher = this.dispatcherHandler.handle(this.request, this.response); + Throwable ex = awaitErrorSignal(publisher); + + assertSame(EXCEPTION, ex); + } + + @Test + public void dispatcherHandlerWithHttpExceptionHandler() throws Exception { + this.request.setUri(new URI("/uknown-argument-type")); + + HttpExceptionHandler exceptionHandler = new ServerError500ExceptionHandler(); + HttpHandler httpHandler = new ErrorHandlingHttpHandler(this.dispatcherHandler, exceptionHandler); + Publisher publisher = httpHandler.handle(this.request, this.response); + + Streams.wrap(publisher).toList().await(5, TimeUnit.SECONDS); + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, this.response.getStatus()); + } + + @Test + public void filterChainWithHttpExceptionHandler() throws Exception { + this.request.setUri(new URI("/uknown-argument-type")); + + HttpHandler httpHandler; + httpHandler = new FilterChainHttpHandler(this.dispatcherHandler, new TestHttpFilter()); + httpHandler = new ErrorHandlingHttpHandler(httpHandler, new ServerError500ExceptionHandler()); + Publisher publisher = httpHandler.handle(this.request, this.response); + + Streams.wrap(publisher).toList().await(5, TimeUnit.SECONDS); + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, this.response.getStatus()); + } + + + private Throwable awaitErrorSignal(Publisher publisher) throws Exception { + Signal signal = Streams.wrap(publisher).materialize().toList().await(5, TimeUnit.SECONDS).get(0); + assertEquals("Unexpected signal: " + signal, Signal.Type.ERROR, signal.getType()); + return signal.getThrowable(); + } + + + @Configuration + @SuppressWarnings("unused") + static class TestConfig { + + @Bean + public RequestMappingHandlerMapping handlerMapping() { + return new RequestMappingHandlerMapping(); + } + + @Bean + public RequestMappingHandlerAdapter handlerAdapter() { + return new RequestMappingHandlerAdapter(); + } + + @Bean + public ResponseBodyResultHandler resultHandler() { + List> encoders = Collections.singletonList(new StringEncoder()); + return new ResponseBodyResultHandler(encoders, new DefaultConversionService()); + } + + @Bean + public TestController testController() { + return new TestController(); + } + } + + @Controller + @SuppressWarnings("unused") + private static class TestController { + + @RequestMapping("/uknown-argument-type") + public void uknownArgumentType(Foo arg) { + } + + @RequestMapping("/error-signal") + @ResponseBody + public Publisher errorSignal() { + return Publishers.error(EXCEPTION); + } + + @RequestMapping("/raise-exception") + public void raiseException() throws Exception { + throw EXCEPTION; + } + + @RequestMapping("/unknown-return-type") + public Foo unknownReturnType() throws Exception { + return new Foo(); + } + + @RequestMapping("/request-body") + @ResponseBody + public Publisher requestBody(@RequestBody Publisher body) { + return Publishers.map(body, s -> "hello " + s); + } + } + + private static class Foo { + } + + private static class ServerError500ExceptionHandler implements HttpExceptionHandler { + + @Override + public Publisher handle(ServerHttpRequest request, ServerHttpResponse response, Throwable ex) { + response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR); + return Publishers.empty(); + } + } + + private static class TestHttpFilter implements HttpFilter { + + @Override + public Publisher filter(ServerHttpRequest req, ServerHttpResponse res, HttpFilterChain chain) { + return chain.filter(req, res); + } + } + +} From 5231e7da7bee9301ca29eecbbf00738eefd44ad3 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 10 Dec 2015 16:50:28 -0500 Subject: [PATCH 120/498] Fix failing test --- .../http/server/reactive/ErrorHandlingHttpHandlerTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ErrorHandlingHttpHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ErrorHandlingHttpHandlerTests.java index fe80a41bcc..4b93f15cc6 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ErrorHandlingHttpHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ErrorHandlingHttpHandlerTests.java @@ -53,7 +53,7 @@ public class ErrorHandlingHttpHandlerTests { @Test public void handleErrorSignal() throws Exception { - HttpExceptionHandler exceptionHandler = new UnresolvedExceptionHandler(); + HttpExceptionHandler exceptionHandler = new HttpStatusExceptionHandler(HttpStatus.INTERNAL_SERVER_ERROR); HttpHandler targetHandler = new TestHttpHandler(new IllegalStateException("boo")); HttpHandler handler = new ErrorHandlingHttpHandler(targetHandler, exceptionHandler); From 1f15b7e0741b7a57fb9bb02173abb4dc5ac0749a Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 15 Dec 2015 11:52:01 -0500 Subject: [PATCH 121/498] Add ResponseStatusException This change adds a ResponseStatusException to associate an exception with a status code at runtime. Along with that is an ResponseStatusExceptionHandler that handles ResponseStatusException by setting the response status. --- .../web/ResponseStatusException.java | 45 +++++++++++ .../ResponseStatusExceptionHandler.java | 43 ++++++++++ .../ResponseStatusExceptionHandlerTests.java | 78 +++++++++++++++++++ 3 files changed, 166 insertions(+) create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/ResponseStatusException.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/ResponseStatusExceptionHandler.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/web/ResponseStatusException.java b/spring-web-reactive/src/main/java/org/springframework/web/ResponseStatusException.java new file mode 100644 index 0000000000..3aa9eac84f --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/ResponseStatusException.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2015 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.web; + +import org.springframework.core.NestedRuntimeException; +import org.springframework.http.HttpStatus; + +/** + * Exception wrapper to associate an exception with a status code at runtime. + * + * @author Rossen Stoyanchev + */ +public class ResponseStatusException extends NestedRuntimeException { + + private final HttpStatus httpStatus; + + + public ResponseStatusException(HttpStatus status) { + this(status, null); + } + + public ResponseStatusException(HttpStatus status, Throwable cause) { + super("Request processing failure with status code: " + status, cause); + this.httpStatus = status; + } + + + public HttpStatus getHttpStatus() { + return this.httpStatus; + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/ResponseStatusExceptionHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/ResponseStatusExceptionHandler.java new file mode 100644 index 0000000000..828c9a52c5 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/ResponseStatusExceptionHandler.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2015 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.web.reactive; + +import org.reactivestreams.Publisher; +import reactor.Publishers; + +import org.springframework.http.server.reactive.HttpExceptionHandler; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.web.ResponseStatusException; + +/** + * Handle {@link ResponseStatusException} by setting the response status. + * + * @author Rossen Stoyanchev + */ +public class ResponseStatusExceptionHandler implements HttpExceptionHandler { + + + @Override + public Publisher handle(ServerHttpRequest request, ServerHttpResponse response, Throwable ex) { + if (ex instanceof ResponseStatusException) { + response.setStatusCode(((ResponseStatusException) ex).getHttpStatus()); + return response.writeHeaders(); + } + return Publishers.error(ex); + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java new file mode 100644 index 0000000000..f7162f5fa8 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2015 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.web.reactive; + +import java.net.URI; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.junit.Before; +import org.junit.Test; +import org.reactivestreams.Publisher; +import reactor.rx.Streams; +import reactor.rx.action.Signal; + +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.web.ResponseStatusException; + +import static junit.framework.TestCase.assertSame; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author Rossen Stoyanchev + */ +public class ResponseStatusExceptionHandlerTests { + + private ResponseStatusExceptionHandler handler; + + private MockServerHttpRequest request; + + private MockServerHttpResponse response; + + + @Before + public void setUp() throws Exception { + this.handler = new ResponseStatusExceptionHandler(); + this.request = new MockServerHttpRequest(HttpMethod.GET, new URI("/path")); + this.response = new MockServerHttpResponse(); + } + + + @Test + public void handleException() throws Exception { + Throwable ex = new ResponseStatusException(HttpStatus.BAD_REQUEST); + Publisher publisher = this.handler.handle(this.request, this.response, ex); + + Streams.wrap(publisher).toList().await(5, TimeUnit.SECONDS); + assertEquals(HttpStatus.BAD_REQUEST, this.response.getStatus()); + } + + @Test + public void unresolvedException() throws Exception { + Throwable ex = new IllegalStateException(); + Publisher publisher = this.handler.handle(this.request, this.response, ex); + + List> signals = Streams.wrap(publisher).materialize().toList().await(5, TimeUnit.SECONDS); + assertEquals(1, signals.size()); + assertTrue(signals.get(0).hasError()); + assertSame(ex, signals.get(0).getThrowable()); + } + +} From c13a8c9bb4a643da271e43712c57bda65238b53c Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 15 Dec 2015 11:55:42 -0500 Subject: [PATCH 122/498] DispatcherHandler maps errors to ResponseStatusException The DispatcherHandler now has an errorMapper property that is a function for transforming errors. By default this property is set to an instance of DispatcherHandlerExceptionMapper which wraps "standard" framework exceptions and @ResponseStatus-annotated exceptions as ResponseStatusException. This makes it easy to handle the exceptions downstream uniformly. --- .../web/reactive/DispatcherHandler.java | 34 ++++++- .../DispatcherHandlerExceptionMapper.java | 65 ++++++++++++++ .../reactive/DispatcherHandlerErrorTests.java | 10 ++- ...DispatcherHandlerExceptionMapperTests.java | 88 +++++++++++++++++++ 4 files changed, 194 insertions(+), 3 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandlerExceptionMapper.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerExceptionMapperTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java index 444f14a7c9..1e68b4890d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java @@ -19,11 +19,14 @@ package org.springframework.web.reactive; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.function.Function; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; import reactor.Publishers; +import reactor.fn.BiConsumer; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactoryUtils; @@ -63,12 +66,30 @@ public class DispatcherHandler implements HttpHandler, ApplicationContextAware { private List resultHandlers; + private Function errorMapper = new DispatcherHandlerExceptionMapper(); + @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { initStrategies(applicationContext); } + /** + * Configure a function to map error signals from the {@code DispatcherHandler}. + *

    By default this is set to {@link DispatcherHandlerExceptionMapper}. + * @param errorMapper the function + */ + public void setErrorMapper(Function errorMapper) { + this.errorMapper = errorMapper; + } + + /** + * Return the configured function for mapping exceptions. + */ + public Function getErrorMapper() { + return this.errorMapper; + } + protected void initStrategies(ApplicationContext context) { Map mappingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors( @@ -107,10 +128,12 @@ public class DispatcherHandler implements HttpHandler, ApplicationContextAware { return handlerAdapter.handle(request, response, handler); }); - return Publishers.concatMap(resultPublisher, result -> { + Publisher completionPublisher = Publishers.concatMap(resultPublisher, result -> { HandlerResultHandler handler = getResultHandler(result); return handler.handleResult(request, response, result); }); + + return mapError(completionPublisher, this.errorMapper); } protected HandlerAdapter getHandlerAdapter(Object handler) { @@ -139,6 +162,15 @@ public class DispatcherHandler implements HttpHandler, ApplicationContextAware { }); } + private static Publisher mapError(Publisher source, Function function) { + return Publishers.lift(source, null, new BiConsumer>() { + @Override + public void accept(Throwable throwable, Subscriber subscriber) { + subscriber.onError(function.apply(throwable)); + } + }, null); + } + private static class NotFoundHandlerMapping implements HandlerMapping { @Override diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandlerExceptionMapper.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandlerExceptionMapper.java new file mode 100644 index 0000000000..9884aa122d --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandlerExceptionMapper.java @@ -0,0 +1,65 @@ +/* + * Copyright 2002-2015 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.web.reactive; + +import java.util.function.Function; + +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.http.HttpStatus; +import org.springframework.web.ResponseStatusException; +import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.bind.annotation.ResponseStatus; + +/** + * Map "standard" framework exceptions and + * {@link ResponseStatus @ResponseStatus}-annotated exceptions to a + * {@link ResponseStatusException}. + * + * @author Rossen Stoyanchev + */ +public class DispatcherHandlerExceptionMapper implements Function { + + + @Override + public Throwable apply(Throwable ex) { + if (ex instanceof HandlerNotFoundException) { + ex = new ResponseStatusException(HttpStatus.NOT_FOUND, ex); + } + else if (ex instanceof HttpMediaTypeNotAcceptableException) { + ex = new ResponseStatusException(HttpStatus.NOT_ACCEPTABLE, ex); + } + else { + ResponseStatus status = findStatus(ex); + if (status != null) { + ex = new ResponseStatusException(status.code(), ex); + } + } + return ex; + } + + private ResponseStatus findStatus(Throwable ex) { + Class type = ex.getClass(); + ResponseStatus status = AnnotatedElementUtils.findMergedAnnotation(type, ResponseStatus.class); + if (status != null) { + return status; + } + else if (ex.getCause() != null) { + return findStatus(ex.getCause()); + } + return null; + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java index 0027fd31d6..00f378c0f6 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java @@ -37,6 +37,7 @@ import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.web.ResponseStatusException; import org.springframework.http.server.reactive.ErrorHandlingHttpHandler; import org.springframework.http.server.reactive.FilterChainHttpHandler; import org.springframework.http.server.reactive.HttpExceptionHandler; @@ -58,6 +59,7 @@ import org.springframework.web.reactive.method.annotation.ResponseBodyResultHand import static org.hamcrest.CoreMatchers.startsWith; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertThat; @@ -101,7 +103,9 @@ public class DispatcherHandlerErrorTests { Publisher publisher = this.dispatcherHandler.handle(this.request, this.response); Throwable ex = awaitErrorSignal(publisher); - assertEquals(HandlerNotFoundException.class, ex.getClass()); + assertEquals(ResponseStatusException.class, ex.getClass()); + assertNotNull(ex.getCause()); + assertEquals(HandlerNotFoundException.class, ex.getCause().getClass()); } @Test @@ -155,7 +159,9 @@ public class DispatcherHandlerErrorTests { Publisher publisher = this.dispatcherHandler.handle(this.request, this.response); Throwable ex = awaitErrorSignal(publisher); - assertEquals(HttpMediaTypeNotAcceptableException.class, ex.getClass()); + assertEquals(ResponseStatusException.class, ex.getClass()); + assertNotNull(ex.getCause()); + assertEquals(HttpMediaTypeNotAcceptableException.class, ex.getCause().getClass()); } @Test diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerExceptionMapperTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerExceptionMapperTests.java new file mode 100644 index 0000000000..afa5744009 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerExceptionMapperTests.java @@ -0,0 +1,88 @@ +/* + * Copyright 2002-2015 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.web.reactive; + +import java.util.Collections; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.ResponseStatusException; +import org.springframework.web.bind.annotation.ResponseStatus; + +import static org.junit.Assert.assertEquals; + +/** + * @author Rossen Stoyanchev + */ +public class DispatcherHandlerExceptionMapperTests { + + private DispatcherHandlerExceptionMapper mapper; + + + @Before + public void setUp() throws Exception { + this.mapper = new DispatcherHandlerExceptionMapper(); + } + + + @Test + public void handlerNotFound() throws Exception { + Throwable ex = new HandlerNotFoundException(HttpMethod.GET, "/path", new HttpHeaders()); + ex = this.mapper.apply(ex); + + assertEquals(ResponseStatusException.class, ex.getClass()); + assertEquals(HttpStatus.NOT_FOUND, ((ResponseStatusException) ex).getHttpStatus()); + } + + + @Test + public void httpMediaTypeNotAcceptable() throws Exception { + Throwable ex = new HttpMediaTypeNotAcceptableException(Collections.emptyList()); + ex = this.mapper.apply(ex); + + assertEquals(ResponseStatusException.class, ex.getClass()); + assertEquals(HttpStatus.NOT_ACCEPTABLE, ((ResponseStatusException) ex).getHttpStatus()); + } + + @Test + public void responseStatusAnnotation() throws Exception { + Throwable ex = new ResponseStatusAnnotatedException(); + ex = this.mapper.apply(ex); + + assertEquals(ResponseStatusException.class, ex.getClass()); + assertEquals(HttpStatus.BAD_REQUEST, ((ResponseStatusException) ex).getHttpStatus()); + } + + @Test + public void responseStatusAnnotationOnRootCause() throws Exception { + Throwable ex = new Exception(new ResponseStatusAnnotatedException()); + ex = this.mapper.apply(ex); + + assertEquals(ResponseStatusException.class, ex.getClass()); + assertEquals(HttpStatus.BAD_REQUEST, ((ResponseStatusException) ex).getHttpStatus()); + } + + + @ResponseStatus(code = HttpStatus.BAD_REQUEST) + private static class ResponseStatusAnnotatedException extends Exception { + } + +} From f2cd92a2433bbed9d60fe269c8ba190eecf5f718 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 15 Dec 2015 12:02:08 -0500 Subject: [PATCH 123/498] Fix imports --- .../server/reactive/FilterChainHttpHandlerTests.java | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/FilterChainHttpHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/FilterChainHttpHandlerTests.java index cd3bad68ab..dcf1fff42d 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/FilterChainHttpHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/FilterChainHttpHandlerTests.java @@ -16,26 +16,16 @@ package org.springframework.http.server.reactive; -import java.util.Arrays; -import java.util.concurrent.Callable; -import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.apache.log4j.spi.LoggerFactory; import org.junit.Before; import org.junit.Test; import org.reactivestreams.Publisher; import reactor.Publishers; -import reactor.core.publisher.PublisherFactory; -import reactor.fn.timer.Timer; import reactor.rx.Streams; -import org.springframework.scheduling.TaskScheduler; -import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; -import org.springframework.util.ObjectUtils; - import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; From 1b3289d0d521e66da67fadf75a62752470772ec0 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 15 Dec 2015 17:04:53 -0500 Subject: [PATCH 124/498] Add InternalServerErrorExceptionHandler --- .../InternalServerErrorExceptionHandler.java | 36 +++++++++++++++++++ .../ErrorHandlingHttpHandlerTests.java | 23 ++---------- 2 files changed, 39 insertions(+), 20 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/http/server/reactive/InternalServerErrorExceptionHandler.java diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/InternalServerErrorExceptionHandler.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/InternalServerErrorExceptionHandler.java new file mode 100644 index 0000000000..b9a69eac64 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/InternalServerErrorExceptionHandler.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2015 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.server.reactive; + +import org.reactivestreams.Publisher; + +import org.springframework.http.HttpStatus; + +/** + * Handle any exception by setting the response status to 500. + * + * @author Rossen Stoyanchev + */ +public class InternalServerErrorExceptionHandler implements HttpExceptionHandler { + + + @Override + public Publisher handle(ServerHttpRequest request, ServerHttpResponse response, Throwable ex) { + response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR); + return response.writeHeaders(); + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ErrorHandlingHttpHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ErrorHandlingHttpHandlerTests.java index 4b93f15cc6..6a744ecb81 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ErrorHandlingHttpHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ErrorHandlingHttpHandlerTests.java @@ -31,7 +31,6 @@ import org.springframework.http.HttpStatus; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; /** * @author Rossen Stoyanchev @@ -53,7 +52,7 @@ public class ErrorHandlingHttpHandlerTests { @Test public void handleErrorSignal() throws Exception { - HttpExceptionHandler exceptionHandler = new HttpStatusExceptionHandler(HttpStatus.INTERNAL_SERVER_ERROR); + HttpExceptionHandler exceptionHandler = new InternalServerErrorExceptionHandler(); HttpHandler targetHandler = new TestHttpHandler(new IllegalStateException("boo")); HttpHandler handler = new ErrorHandlingHttpHandler(targetHandler, exceptionHandler); @@ -68,7 +67,7 @@ public class ErrorHandlingHttpHandlerTests { HttpExceptionHandler[] exceptionHandlers = new HttpExceptionHandler[] { new UnresolvedExceptionHandler(), new UnresolvedExceptionHandler(), - new HttpStatusExceptionHandler(HttpStatus.INTERNAL_SERVER_ERROR), + new InternalServerErrorExceptionHandler(), new UnresolvedExceptionHandler() }; HttpHandler targetHandler = new TestHttpHandler(new IllegalStateException("boo")); @@ -95,7 +94,7 @@ public class ErrorHandlingHttpHandlerTests { @Test public void thrownExceptionBecomesErrorSignal() throws Exception { - HttpExceptionHandler exceptionHandler = new HttpStatusExceptionHandler(HttpStatus.INTERNAL_SERVER_ERROR); + HttpExceptionHandler exceptionHandler = new InternalServerErrorExceptionHandler(); HttpHandler targetHandler = new TestHttpHandler(new IllegalStateException("boo"), true); HttpHandler handler = new ErrorHandlingHttpHandler(targetHandler, exceptionHandler); @@ -148,20 +147,4 @@ public class ErrorHandlingHttpHandlerTests { } } - /** Set the response status to the given HttpStatus. */ - private static class HttpStatusExceptionHandler implements HttpExceptionHandler { - - private final HttpStatus status; - - public HttpStatusExceptionHandler(HttpStatus status) { - this.status = status; - } - - @Override - public Publisher handle(ServerHttpRequest request, ServerHttpResponse response, Throwable ex) { - response.setStatusCode(this.status); - return Publishers.empty(); - } - } - } From 623874b4d0e56770cc3574968ec008e1cc4f831b Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 16 Dec 2015 11:08:41 -0500 Subject: [PATCH 125/498] Constant HandlerNotFoundException --- .../web/reactive/DispatcherHandler.java | 7 ++-- .../reactive/HandlerNotFoundException.java | 33 ++----------------- 2 files changed, 7 insertions(+), 33 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java index 1e68b4890d..51cefb1eca 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java @@ -173,10 +173,13 @@ public class DispatcherHandler implements HttpHandler, ApplicationContextAware { private static class NotFoundHandlerMapping implements HandlerMapping { + @SuppressWarnings("ThrowableInstanceNeverThrown") + private static final Exception HANDLER_NOT_FOUND_EXCEPTION = new HandlerNotFoundException(); + + @Override public Publisher getHandler(ServerHttpRequest request) { - return Publishers.error(new HandlerNotFoundException(request.getMethod(), - request.getURI().getPath(), request.getHeaders())); + return Publishers.error(HANDLER_NOT_FOUND_EXCEPTION); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerNotFoundException.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerNotFoundException.java index d8bc40f933..f9d8f074af 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerNotFoundException.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerNotFoundException.java @@ -16,44 +16,15 @@ package org.springframework.web.reactive; import org.springframework.core.NestedRuntimeException; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; /** * @author Rossen Stoyanchev */ public class HandlerNotFoundException extends NestedRuntimeException { - private final HttpMethod method; - private final String requestURL; - - private final HttpHeaders headers; - - - /** - * Constructor for NoHandlerFoundException. - * @param method the HTTP method - * @param requestURL the HTTP request URL - * @param headers the HTTP request headers - */ - public HandlerNotFoundException(HttpMethod method, String requestURL, HttpHeaders headers) { - super("No handler found for " + method + " " + requestURL); - this.method = method; - this.requestURL = requestURL; - this.headers = headers; + public HandlerNotFoundException() { + super("No handler found."); } - - public HttpMethod getMethod() { - return this.method; - } - - public String getRequestURL() { - return this.requestURL; - } - - public HttpHeaders getHeaders() { - return this.headers; - } } From 69973d984dfff27837927da99cfe08047aae7c0d Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 16 Dec 2015 12:30:15 -0500 Subject: [PATCH 126/498] Fix test failure --- .../web/reactive/DispatcherHandlerExceptionMapperTests.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerExceptionMapperTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerExceptionMapperTests.java index afa5744009..4fd4b152b4 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerExceptionMapperTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerExceptionMapperTests.java @@ -20,8 +20,6 @@ import java.util.Collections; import org.junit.Before; import org.junit.Test; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.web.HttpMediaTypeNotAcceptableException; import org.springframework.web.ResponseStatusException; @@ -45,7 +43,7 @@ public class DispatcherHandlerExceptionMapperTests { @Test public void handlerNotFound() throws Exception { - Throwable ex = new HandlerNotFoundException(HttpMethod.GET, "/path", new HttpHeaders()); + Throwable ex = new HandlerNotFoundException(); ex = this.mapper.apply(ex); assertEquals(ResponseStatusException.class, ex.getClass()); From 15185c75a395a42b1fd6db4c7f48e178d6f14a90 Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Thu, 17 Dec 2015 13:34:09 +0000 Subject: [PATCH 127/498] use Publishers.onErrorResumeNext --- .../http/server/reactive/ErrorHandlingHttpHandler.java | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ErrorHandlingHttpHandler.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ErrorHandlingHttpHandler.java index 755e83bb53..0050a4c010 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ErrorHandlingHttpHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ErrorHandlingHttpHandler.java @@ -20,8 +20,6 @@ import java.util.List; import org.reactivestreams.Publisher; import reactor.Publishers; -import reactor.core.publisher.convert.RxJava1Converter; -import rx.Observable; import org.springframework.util.Assert; @@ -31,6 +29,7 @@ import org.springframework.util.Assert; * sequentially until one of them completes successfully. * * @author Rossen Stoyanchev + * @author Stephane Maldini */ public class ErrorHandlingHttpHandler extends HttpHandlerDecorator { @@ -62,12 +61,9 @@ public class ErrorHandlingHttpHandler extends HttpHandlerDecorator { private static Publisher applyExceptionHandler(Publisher publisher, HttpExceptionHandler handler, ServerHttpRequest request, ServerHttpResponse response) { - // see https://github.com/reactor/reactor/issues/580 - - Observable observable = RxJava1Converter.from(publisher).onErrorResumeNext(ex -> { - return RxJava1Converter.from(handler.handle(request, response, ex)); + return Publishers.onErrorResumeNext(publisher, ex -> { + return handler.handle(request, response, ex); }); - return RxJava1Converter.from(observable); } } From da4e4aab39391e11637b8e405f76c917149b340e Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Fri, 18 Dec 2015 16:56:17 +0000 Subject: [PATCH 128/498] test operator log --- .../method/annotation/RequestMappingIntegrationTests.java | 1 + 1 file changed, 1 insertion(+) diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java index dbb016a39f..15d6d8feba 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java @@ -25,6 +25,7 @@ import java.util.concurrent.CompletableFuture; import org.junit.Test; import org.reactivestreams.Publisher; +import reactor.Publishers; import reactor.io.buffer.Buffer; import reactor.rx.Promise; import reactor.rx.Promises; From a12c172ceba7d2403f3de3684300cbadfba4c240 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 29 Dec 2015 17:11:01 -0500 Subject: [PATCH 129/498] Switch to rxnetty-http dependency The "rxnetty" artifact is old (October 2015). There is now rxnetty-http and other modules that have more recent snapshots (December 2015). --- spring-web-reactive/build.gradle | 2 +- .../http/server/reactive/RxNettyHttpHandlerAdapter.java | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index 5f6300258f..99c780e6d6 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -45,7 +45,7 @@ dependencies { compile "commons-logging:commons-logging:1.2" optional 'io.reactivex:rxjava:1.1.0' - optional "io.reactivex:rxnetty:0.5.0-SNAPSHOT" + optional "io.reactivex:rxnetty-http:0.5.0-SNAPSHOT" optional "com.fasterxml.jackson.core:jackson-databind:2.6.2" optional "io.projectreactor:reactor-stream:${reactorVersion}" optional "io.projectreactor:reactor-net:${reactorVersion}" diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyHttpHandlerAdapter.java index 254fcb093f..a3190c8f7c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyHttpHandlerAdapter.java @@ -24,9 +24,6 @@ import org.reactivestreams.Publisher; import reactor.core.publisher.convert.RxJava1Converter; import rx.Observable; -import org.springframework.http.server.reactive.HttpHandler; -import org.springframework.http.server.reactive.RxNettyServerHttpRequest; -import org.springframework.http.server.reactive.RxNettyServerHttpResponse; import org.springframework.util.Assert; /** From 9eb07b77806af2e57ebb7e63814d948c0c86671d Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 29 Dec 2015 17:14:38 -0500 Subject: [PATCH 130/498] Update package for reactor Signal --- .../http/server/reactive/ErrorHandlingHttpHandlerTests.java | 2 +- .../web/reactive/DispatcherHandlerErrorTests.java | 2 +- .../web/reactive/ResponseStatusExceptionHandlerTests.java | 2 +- .../web/reactive/method/InvocableHandlerMethodTests.java | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ErrorHandlingHttpHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ErrorHandlingHttpHandlerTests.java index 6a744ecb81..ce13fa5694 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ErrorHandlingHttpHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ErrorHandlingHttpHandlerTests.java @@ -24,7 +24,7 @@ import org.junit.Test; import org.reactivestreams.Publisher; import reactor.Publishers; import reactor.rx.Streams; -import reactor.rx.action.Signal; +import reactor.rx.stream.Signal; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java index 00f378c0f6..6ca079a6fc 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java @@ -26,7 +26,7 @@ import org.junit.Test; import org.reactivestreams.Publisher; import reactor.Publishers; import reactor.rx.Streams; -import reactor.rx.action.Signal; +import reactor.rx.stream.Signal; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java index f7162f5fa8..076a589230 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java @@ -23,7 +23,7 @@ import org.junit.Before; import org.junit.Test; import org.reactivestreams.Publisher; import reactor.rx.Streams; -import reactor.rx.action.Signal; +import reactor.rx.stream.Signal; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java index fbd669e514..5da1f99568 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java @@ -28,7 +28,7 @@ import org.junit.Test; import org.reactivestreams.Publisher; import reactor.Publishers; import reactor.rx.Streams; -import reactor.rx.action.Signal; +import reactor.rx.stream.Signal; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.web.bind.annotation.RequestParam; From 84e3ad342b37afbaf9fa88d6116737830f1a857a Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 29 Dec 2015 17:15:18 -0500 Subject: [PATCH 131/498] Remove @Ignore from 404 status code tests --- .../handler/SimpleUrlHandlerMappingIntegrationTests.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java index 0cc303e0c9..d0de9544d0 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java @@ -21,7 +21,6 @@ import java.nio.charset.Charset; import java.util.HashMap; import java.util.Map; -import org.junit.Ignore; import org.junit.Test; import org.reactivestreams.Publisher; import reactor.io.buffer.Buffer; @@ -32,12 +31,14 @@ import org.springframework.http.HttpStatus; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; import org.springframework.http.server.reactive.AbstractHttpHandlerIntegrationTests; +import org.springframework.http.server.reactive.ErrorHandlingHttpHandler; import org.springframework.http.server.reactive.HttpHandler; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestTemplate; import org.springframework.web.reactive.DispatcherHandler; +import org.springframework.web.reactive.ResponseStatusExceptionHandler; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; @@ -62,7 +63,7 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler DispatcherHandler dispatcherHandler = new DispatcherHandler(); dispatcherHandler.setApplicationContext(wac); - return dispatcherHandler; + return new ErrorHandlingHttpHandler(dispatcherHandler, new ResponseStatusExceptionHandler()); } @Test @@ -91,10 +92,7 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler assertArrayEquals("bar".getBytes(UTF_8), response.getBody()); } - // TODO: remove @Ignore after 404 default handling - @Test - @Ignore public void testNotFound() throws Exception { RestTemplate restTemplate = new RestTemplate(); From 9789db23d2973d8f0156d2b6c6027d9fb67ab844 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 28 Dec 2015 10:11:55 -0500 Subject: [PATCH 132/498] Rename HandlerResult fields from value to result --- .../web/reactive/DispatcherHandler.java | 2 +- .../web/reactive/HandlerResult.java | 20 +++++++++---------- .../handler/SimpleHandlerResultHandler.java | 6 +++--- .../annotation/ResponseBodyResultHandler.java | 4 ++-- .../method/InvocableHandlerMethodTests.java | 8 ++++---- 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java index 51cefb1eca..2b654e1564 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java @@ -151,7 +151,7 @@ public class DispatcherHandler implements HttpHandler, ApplicationContextAware { return resultHandler; } } - throw new IllegalStateException("No HandlerResultHandler for " + handlerResult.getValue()); + throw new IllegalStateException("No HandlerResultHandler for " + handlerResult.getResult()); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java index b2e921e143..0dacf869e7 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java @@ -28,17 +28,17 @@ public class HandlerResult { private final Object handler; - private final Object value; + private final Object result; - private final ResolvableType valueType; + private final ResolvableType resultType; - public HandlerResult(Object handler, Object value, ResolvableType valueType) { + public HandlerResult(Object handler, Object result, ResolvableType resultType) { Assert.notNull(handler, "'handler' is required"); - Assert.notNull(handler, "'valueType' is required"); + Assert.notNull(handler, "'resultType' is required"); this.handler = handler; - this.value = value; - this.valueType = valueType; + this.result = result; + this.resultType = resultType; } @@ -46,12 +46,12 @@ public class HandlerResult { return this.handler; } - public Object getValue() { - return this.value; + public Object getResult() { + return this.result; } - public ResolvableType getValueType() { - return this.valueType; + public ResolvableType getResultType() { + return this.resultType; } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java index 0721f92d06..6c7bd02930 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java @@ -64,7 +64,7 @@ public class SimpleHandlerResultHandler implements Ordered, HandlerResultHandler @Override public boolean supports(HandlerResult result) { - ResolvableType type = result.getValueType(); + ResolvableType type = result.getResultType(); return type != null && Void.TYPE.equals(type.getRawClass()) || (Void.class.isAssignableFrom(type.getGeneric(0).getRawClass()) && isConvertibleToPublisher(type)); } @@ -78,8 +78,8 @@ public class SimpleHandlerResultHandler implements Ordered, HandlerResultHandler public Publisher handleResult(ServerHttpRequest request, ServerHttpResponse response, HandlerResult result) { - Object value = result.getValue(); - if (Void.TYPE.equals(result.getValueType().getRawClass())) { + Object value = result.getResult(); + if (Void.TYPE.equals(result.getResultType().getRawClass())) { return response.writeHeaders(); } Publisher completion = (value instanceof Publisher ? (Publisher)value : this.conversionService.convert(value, Publisher.class)); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java index d0d4ab6340..e52099f27d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java @@ -130,14 +130,14 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered public Publisher handleResult(ServerHttpRequest request, ServerHttpResponse response, HandlerResult result) { - Object value = result.getValue(); + Object value = result.getResult(); if (value == null) { return Publishers.empty(); } Publisher publisher; ResolvableType elementType; - ResolvableType returnType = result.getValueType(); + ResolvableType returnType = result.getResultType(); if (this.conversionService.canConvert(returnType.getRawClass(), Publisher.class)) { publisher = this.conversionService.convert(value, Publisher.class); elementType = returnType.getGeneric(0); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java index 5da1f99568..c7b0bee5fd 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java @@ -64,7 +64,7 @@ public class InvocableHandlerMethodTests { List values = Streams.wrap(publisher).toList().await(5, TimeUnit.SECONDS); assertEquals(1, values.size()); - assertEquals("success", values.get(0).getValue()); + assertEquals("success", values.get(0).getResult()); } @Test @@ -77,7 +77,7 @@ public class InvocableHandlerMethodTests { List values = Streams.wrap(publisher).toList().await(5, TimeUnit.SECONDS); assertEquals(1, values.size()); - assertEquals("success:null", values.get(0).getValue()); + assertEquals("success:null", values.get(0).getResult()); } @Test @@ -89,7 +89,7 @@ public class InvocableHandlerMethodTests { List values = Streams.wrap(publisher).toList().await(5, TimeUnit.SECONDS); assertEquals(1, values.size()); - assertEquals("success:value1", values.get(0).getValue()); + assertEquals("success:value1", values.get(0).getResult()); } @Test @@ -101,7 +101,7 @@ public class InvocableHandlerMethodTests { List values = Streams.wrap(publisher).toList().await(5, TimeUnit.SECONDS); assertEquals(1, values.size()); - assertEquals("success:value1", values.get(0).getValue()); + assertEquals("success:value1", values.get(0).getResult()); } @Test From 8c89b478d94f5c290c82ede2c302b1740b5d08e4 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 23 Dec 2015 16:38:40 -0500 Subject: [PATCH 133/498] Add operator for deferred writes --- .../server/reactive/WriteWithOperator.java | 218 ++++++++++++++++++ .../reactive/WriteWithOperatorTests.java | 193 ++++++++++++++++ 2 files changed, 411 insertions(+) create mode 100644 spring-web-reactive/src/main/java/org/springframework/http/server/reactive/WriteWithOperator.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/http/server/reactive/WriteWithOperatorTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/WriteWithOperator.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/WriteWithOperator.java new file mode 100644 index 0000000000..5aa3371b70 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/WriteWithOperator.java @@ -0,0 +1,218 @@ +/* + * Copyright 2002-2015 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.server.reactive; + +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import reactor.core.subscriber.SubscriberBarrier; +import reactor.core.support.Assert; +import reactor.fn.Function; + + +/** + * Given a write function that accepts a source {@code Publisher} to write + * with and returns {@code Publisher} for the result, this operator helps + * to defer the invocation of the write function, until we know if the source + * publisher will begin publishing without an error. If the first emission is + * an error, the write function is bypassed, and the error is sent directly + * through the result publisher. Otherwise the write function is invoked. + * + * @author Rossen Stoyanchev + */ +public class WriteWithOperator implements Function, Subscriber> { + + private final java.util.function.Function, Publisher> writeFunction; + + + public WriteWithOperator(java.util.function.Function, Publisher> writeFunction) { + this.writeFunction = writeFunction; + } + + @Override + public Subscriber apply(Subscriber subscriber) { + return new WriteWithBarrier(subscriber); + } + + + private class WriteWithBarrier extends SubscriberBarrier implements Publisher { + + /** + * We've at at least one emission, we've called the write function, the write + * subscriber has subscribed and cached signals have been emitted to it. + * We're now simply passing data through to the write subscriber. + **/ + private boolean readyToWrite = false; + + /** No emission from upstream yet */ + private boolean beforeFirstEmission = true; + + /** Cached signal before readyToWrite */ + private T item; + + /** Cached 1st/2nd signal before readyToWrite */ + private Throwable error; + + /** Cached 1st/2nd signal before readyToWrite */ + private boolean completed = false; + + /** The actual writeSubscriber vs the downstream completion subscriber */ + private Subscriber writeSubscriber; + + + public WriteWithBarrier(Subscriber subscriber) { + super(subscriber); + } + + + @Override + protected void doOnSubscribe(Subscription subscription) { + super.doOnSubscribe(subscription); + ((Subscription) super.upstream()).request(1); // bypass doRequest + } + + @Override + public void doNext(T item) { + if (this.readyToWrite) { + this.writeSubscriber.onNext(item); + return; + } + synchronized (this) { + if (this.readyToWrite) { + this.writeSubscriber.onNext(item); + } + else if (this.beforeFirstEmission) { + this.item = item; + this.beforeFirstEmission = false; + writeFunction.apply(this).subscribe(downstream()); + } + else { + subscription.cancel(); + downstream().onError(new IllegalStateException("Unexpected item.")); + } + } + } + + @Override + public void doError(Throwable ex) { + if (this.readyToWrite) { + this.writeSubscriber.onError(ex); + return; + } + synchronized (this) { + if (this.readyToWrite) { + this.writeSubscriber.onError(ex); + } + else if (this.beforeFirstEmission) { + this.beforeFirstEmission = false; + downstream().onError(ex); + } + else { + this.error = ex; + } + } + } + + @Override + public void doComplete() { + if (this.readyToWrite) { + this.writeSubscriber.onComplete(); + return; + } + synchronized (this) { + if (this.readyToWrite) { + this.writeSubscriber.onComplete(); + } + else if (this.beforeFirstEmission) { + this.completed = true; + this.beforeFirstEmission = false; + writeFunction.apply(this).subscribe(downstream()); + } + else { + this.completed = true; + } + } + } + + @Override + public void subscribe(Subscriber subscriber) { + synchronized (this) { + Assert.isNull(this.writeSubscriber, "Only one writeSubscriber supported."); + this.writeSubscriber = subscriber; + + if (this.error != null || this.completed) { + this.writeSubscriber.onSubscribe(NO_OP_SUBSCRIPTION); + emitCachedSignals(); + } + else { + this.writeSubscriber.onSubscribe(this); + } + } + } + + /** + * Emit cached signals to the write subscriber. + * @return true if no more signals expected + */ + private boolean emitCachedSignals() { + if (this.item != null) { + this.writeSubscriber.onNext(this.item); + } + if (this.error != null) { + this.writeSubscriber.onError(this.error); + return true; + } + if (this.completed) { + this.writeSubscriber.onComplete(); + return true; + } + return false; + } + + @Override + protected void doRequest(long n) { + if (this.readyToWrite) { + super.doRequest(n); + return; + } + synchronized (this) { + if (this.writeSubscriber != null) { + readyToWrite = true; + if (emitCachedSignals()) { + return; + } + n--; + if (n == 0) { + return; + } + super.doRequest(n); + } + } + } + } + + private final static Subscription NO_OP_SUBSCRIPTION = new Subscription() { + + @Override + public void request(long n) { + } + + @Override + public void cancel() { + } + }; + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/WriteWithOperatorTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/WriteWithOperatorTests.java new file mode 100644 index 0000000000..ac847a7f99 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/WriteWithOperatorTests.java @@ -0,0 +1,193 @@ +/* + * Copyright 2002-2015 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.server.reactive; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Before; +import org.junit.Test; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import reactor.Publishers; +import reactor.core.publisher.PublisherFactory; +import reactor.core.subscriber.SubscriberBarrier; +import reactor.rx.Streams; +import reactor.rx.action.Signal; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +/** + * @author Rossen Stoyanchev + */ +@SuppressWarnings("ThrowableResultOfMethodCallIgnored") +public class WriteWithOperatorTests { + + private OneByOneAsyncWriter writer; + + private WriteWithOperator operator; + + + @Before + public void setUp() throws Exception { + this.writer = new OneByOneAsyncWriter(); + this.operator = new WriteWithOperator<>(this.writer::writeWith); + } + + @Test + public void errorBeforeFirstItem() throws Exception { + IllegalStateException error = new IllegalStateException("boo"); + Publisher completion = Publishers.lift(Publishers.error(error), this.operator); + List> signals = Streams.wrap(completion).materialize().toList().await(5, TimeUnit.SECONDS); + + assertEquals(1, signals.size()); + assertSame("Unexpected signal: " + signals.get(0), error, signals.get(0).getThrowable()); + } + + @Test + public void completionBeforeFirstItem() throws Exception { + Publisher completion = Publishers.lift(Publishers.empty(), this.operator); + List> signals = Streams.wrap(completion).materialize().toList().await(5, TimeUnit.SECONDS); + + assertEquals(1, signals.size()); + assertTrue("Unexpected signal: " + signals.get(0), signals.get(0).isOnComplete()); + + assertEquals(0, this.writer.items.size()); + assertTrue(this.writer.completed); + } + + @Test + public void writeOneItem() throws Exception { + Publisher completion = Publishers.lift(Publishers.just("one"), this.operator); + List> signals = Streams.wrap(completion).materialize().toList().await(5, TimeUnit.SECONDS); + + assertEquals(1, signals.size()); + assertTrue("Unexpected signal: " + signals.get(0), signals.get(0).isOnComplete()); + + assertEquals(1, this.writer.items.size()); + assertEquals("one", this.writer.items.get(0)); + assertTrue(this.writer.completed); + } + + + @Test + public void writeMultipleItems() throws Exception { + List items = Arrays.asList("one", "two", "three"); + Publisher completion = Publishers.lift(Publishers.from(items), this.operator); + List> signals = Streams.wrap(completion).materialize().consumeAsList().await(5, TimeUnit.SECONDS); + + assertEquals(1, signals.size()); + assertTrue("Unexpected signal: " + signals.get(0), signals.get(0).isOnComplete()); + + assertEquals(3, this.writer.items.size()); + assertEquals("one", this.writer.items.get(0)); + assertEquals("two", this.writer.items.get(1)); + assertEquals("three", this.writer.items.get(2)); + assertTrue(this.writer.completed); + } + + @Test + public void errorAfterMultipleItems() throws Exception { + IllegalStateException error = new IllegalStateException("boo"); + Publisher publisher = PublisherFactory.create(subscriber -> { + int i = subscriber.context().incrementAndGet(); + subscriber.onNext(String.valueOf(i)); + if (i == 3) { + subscriber.onError(error); + } + }, subscriber -> new AtomicInteger()); + Publisher completion = Publishers.lift(publisher, this.operator); + List> signals = Streams.wrap(completion).materialize().toList().await(5, TimeUnit.SECONDS); + + assertEquals(1, signals.size()); + assertSame("Unexpected signal: " + signals.get(0), error, signals.get(0).getThrowable()); + + assertEquals(3, this.writer.items.size()); + assertEquals("1", this.writer.items.get(0)); + assertEquals("2", this.writer.items.get(1)); + assertEquals("3", this.writer.items.get(2)); + assertSame(error, this.writer.error); + } + + + private static class OneByOneAsyncWriter { + + private List items = new ArrayList<>(); + + private boolean completed = false; + + private Throwable error; + + + public Publisher writeWith(Publisher publisher) { + return subscriber -> { + Executors.newSingleThreadScheduledExecutor().schedule( + (Runnable) () -> publisher.subscribe(new WriteSubscriber(subscriber)), + 50, TimeUnit.MILLISECONDS); + }; + } + + private class WriteSubscriber extends SubscriberBarrier { + + public WriteSubscriber(Subscriber subscriber) { + super(subscriber); + } + + @Override + protected void doOnSubscribe(Subscription subscription) { + subscription.request(1); + } + + @Override + public void doNext(String item) { + items.add(item); + this.subscription.request(1); + } + + @Override + public void doError(Throwable ex) { + error = ex; + this.subscriber.onError(ex); + } + + @Override + public void doComplete() { + completed = true; + this.subscriber.onComplete(); + } + } + } + + private final static Subscription NO_OP_SUBSCRIPTION = new Subscription() { + + @Override + public void request(long n) { + } + + @Override + public void cancel() { + } + }; + +} From 34eb6d54263cd0ae426f021f98092e6daacbcd70 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 29 Dec 2015 17:36:32 -0500 Subject: [PATCH 134/498] Add support for @ExceptionHandler methods --- .../reactive/ReactorServerHttpResponse.java | 9 +- .../reactive/RxNettyServerHttpRequest.java | 1 - .../reactive/RxNettyServerHttpResponse.java | 11 +-- .../reactive/ServletServerHttpResponse.java | 8 +- .../reactive/UndertowServerHttpResponse.java | 16 ++-- .../server/reactive/WriteWithOperator.java | 38 ++++++-- .../web/reactive/DispatcherHandler.java | 20 ++++- .../web/reactive/HandlerResult.java | 56 +++++++++++- .../method/InvocableHandlerMethod.java | 23 ++--- .../RequestMappingHandlerAdapter.java | 88 ++++++++++++++++--- .../AbstractHttpHandlerIntegrationTests.java | 1 - .../reactive/WriteWithOperatorTests.java | 2 +- .../RequestMappingIntegrationTests.java | 43 +++++++++ 13 files changed, 265 insertions(+), 51 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java index 17ab111345..3233d38daa 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java @@ -25,7 +25,6 @@ import reactor.io.net.http.model.Status; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; -import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.util.Assert; /** @@ -67,9 +66,11 @@ public class ReactorServerHttpResponse implements ServerHttpResponse { } @Override - public Publisher setBody(Publisher contentPublisher) { - applyHeaders(); - return this.channel.writeWith(Publishers.map(contentPublisher, Buffer::new)); + public Publisher setBody(Publisher publisher) { + return Publishers.lift(publisher, new WriteWithOperator<>(writePublisher -> { + applyHeaders(); + return this.channel.writeWith(Publishers.map(writePublisher, Buffer::new)); + })); } private void applyHeaders() { diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java index 1fa794ca37..cf075d9f3f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java @@ -28,7 +28,6 @@ import rx.Observable; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; -import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.util.Assert; /** diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java index 73ac4864ce..78f7141589 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java @@ -28,7 +28,6 @@ import rx.Observable; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; -import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.util.Assert; /** @@ -72,10 +71,12 @@ public class RxNettyServerHttpResponse implements ServerHttpResponse { @Override public Publisher setBody(Publisher publisher) { - applyHeaders(); - Observable observable = RxJava1Converter.from(publisher).map( - content -> new Buffer(content).asBytes()); - return RxJava1Converter.from(this.response.writeBytes(observable)); + return Publishers.lift(publisher, new WriteWithOperator<>(writePublisher -> { + applyHeaders(); + Observable observable = RxJava1Converter.from(writePublisher) + .map(buffer -> new Buffer(buffer).asBytes()); + return RxJava1Converter.from(this.response.writeBytes(observable)); + })); } private void applyHeaders() { diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java index b992cc6463..984f9dd9ca 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java @@ -83,9 +83,11 @@ public class ServletServerHttpResponse implements ServerHttpResponse { } @Override - public Publisher setBody(final Publisher contentPublisher) { - applyHeaders(); - return (s -> contentPublisher.subscribe(subscriber)); + public Publisher setBody(final Publisher publisher) { + return Publishers.lift(publisher, new WriteWithOperator<>(writePublisher -> { + applyHeaders(); + return (s -> writePublisher.subscribe(subscriber)); + })); } private void applyHeaders() { diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java index d96b85bdf3..01f1d9ee53 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java @@ -38,6 +38,7 @@ import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; import org.xnio.ChannelListener; import org.xnio.channels.StreamSinkChannel; +import reactor.Publishers; import reactor.core.subscriber.BaseSubscriber; import static org.xnio.ChannelListeners.closingChannelExceptionHandler; @@ -74,13 +75,6 @@ public class UndertowServerHttpResponse implements ServerHttpResponse { this.exchange.setStatusCode(status.value()); } - - @Override - public Publisher setBody(Publisher bodyPublisher) { - applyHeaders(); - return (subscriber -> bodyPublisher.subscribe(bodySubscriber)); - } - @Override public HttpHeaders getHeaders() { return (this.headersWritten ? HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers); @@ -112,6 +106,14 @@ public class UndertowServerHttpResponse implements ServerHttpResponse { } } + @Override + public Publisher setBody(Publisher publisher) { + return Publishers.lift(publisher, new WriteWithOperator<>(writePublisher -> { + applyHeaders(); + return (subscriber -> writePublisher.subscribe(bodySubscriber)); + })); + } + private class ResponseBodySubscriber extends BaseSubscriber implements ChannelListener { diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/WriteWithOperator.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/WriteWithOperator.java index 5aa3371b70..2f23abf73c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/WriteWithOperator.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/WriteWithOperator.java @@ -97,7 +97,7 @@ public class WriteWithOperator implements Function, else if (this.beforeFirstEmission) { this.item = item; this.beforeFirstEmission = false; - writeFunction.apply(this).subscribe(downstream()); + writeFunction.apply(this).subscribe(new DownstreamBridge(downstream())); } else { subscription.cancel(); @@ -139,7 +139,7 @@ public class WriteWithOperator implements Function, else if (this.beforeFirstEmission) { this.completed = true; this.beforeFirstEmission = false; - writeFunction.apply(this).subscribe(downstream()); + writeFunction.apply(this).subscribe(new DownstreamBridge(downstream())); } else { this.completed = true; @@ -148,10 +148,10 @@ public class WriteWithOperator implements Function, } @Override - public void subscribe(Subscriber subscriber) { + public void subscribe(Subscriber writeSubscriber) { synchronized (this) { Assert.isNull(this.writeSubscriber, "Only one writeSubscriber supported."); - this.writeSubscriber = subscriber; + this.writeSubscriber = writeSubscriber; if (this.error != null || this.completed) { this.writeSubscriber.onSubscribe(NO_OP_SUBSCRIPTION); @@ -184,7 +184,7 @@ public class WriteWithOperator implements Function, @Override protected void doRequest(long n) { - if (this.readyToWrite) { + if (readyToWrite) { super.doRequest(n); return; } @@ -204,6 +204,34 @@ public class WriteWithOperator implements Function, } } + private class DownstreamBridge implements Subscriber { + + private final Subscriber downstream; + + public DownstreamBridge(Subscriber downstream) { + this.downstream = downstream; + } + + @Override + public void onSubscribe(Subscription subscription) { + subscription.request(Long.MAX_VALUE); + } + + @Override + public void onNext(Void aVoid) { + } + + @Override + public void onError(Throwable ex) { + this.downstream.onError(ex); + } + + @Override + public void onComplete() { + this.downstream.onComplete(); + } + } + private final static Subscription NO_OP_SUBSCRIPTION = new Subscription() { @Override diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java index 2b654e1564..ee3bfe70e3 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java @@ -129,8 +129,24 @@ public class DispatcherHandler implements HttpHandler, ApplicationContextAware { }); Publisher completionPublisher = Publishers.concatMap(resultPublisher, result -> { - HandlerResultHandler handler = getResultHandler(result); - return handler.handleResult(request, response, result); + Publisher publisher; + if (result.hasError()) { + publisher = Publishers.error(result.getError()); + } + else { + HandlerResultHandler handler = getResultHandler(result); + publisher = handler.handleResult(request, response, result); + } + if (result.hasExceptionMapper()) { + return Publishers.onErrorResumeNext(publisher, ex -> { + return Publishers.concatMap(result.getExceptionMapper().apply(ex), + errorResult -> { + HandlerResultHandler handler = getResultHandler(errorResult); + return handler.handleResult(request, response, errorResult); + }); + }); + } + return publisher; }); return mapError(completionPublisher, this.errorMapper); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java index 0dacf869e7..4a6bae92ab 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java @@ -16,11 +16,17 @@ package org.springframework.web.reactive; +import java.util.function.Function; +import java.util.logging.Handler; + +import org.reactivestreams.Publisher; +import reactor.Publishers; + import org.springframework.core.ResolvableType; import org.springframework.util.Assert; /** - * Represent the result of the invocation of an handler. + * Represent the result of the invocation of a handler. * * @author Rossen Stoyanchev */ @@ -32,6 +38,10 @@ public class HandlerResult { private final ResolvableType resultType; + private final Throwable error; + + private Function> exceptionMapper; + public HandlerResult(Object handler, Object result, ResolvableType resultType) { Assert.notNull(handler, "'handler' is required"); @@ -39,6 +49,16 @@ public class HandlerResult { this.handler = handler; this.result = result; this.resultType = resultType; + this.error = null; + } + + public HandlerResult(Object handler, Throwable error) { + Assert.notNull(handler, "'handler' is required"); + Assert.notNull(error, "'error' is required"); + this.handler = handler; + this.result = null; + this.resultType = null; + this.error = error; } @@ -54,4 +74,38 @@ public class HandlerResult { return this.resultType; } + public Throwable getError() { + return this.error; + } + + /** + * Whether handler invocation produced a result or failed with an error. + *

    If {@code true} the {@link #getError()} returns the error while + * {@link #getResult()} and {@link #getResultType()} return {@code null} + * and vice versa. + * @return whether this instance contains a result or an error. + */ + public boolean hasError() { + return (this.error != null); + } + + /** + * Configure a function for selecting an alternate {@code HandlerResult} in + * case of an {@link #hasError() error result} or in case of an async result + * that results in an error. + * @param function the exception resolving function + */ + public HandlerResult setExceptionMapper(Function> function) { + this.exceptionMapper = function; + return this; + } + + public Function> getExceptionMapper() { + return this.exceptionMapper; + } + + public boolean hasExceptionMapper() { + return (this.exceptionMapper != null); + } + } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java index 5b89c254e4..5cebab6252 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java @@ -62,6 +62,10 @@ public class InvocableHandlerMethod extends HandlerMethod { super(handlerMethod); } + public InvocableHandlerMethod(Object bean, Method method) { + super(bean, method); + } + public void setHandlerMethodArgumentResolvers(List resolvers) { this.resolvers.clear(); @@ -75,9 +79,10 @@ public class InvocableHandlerMethod extends HandlerMethod { /** - * - * @param request - * @param providedArgs + * Invoke the method and return a Publisher for the return value. + * @param request the current request + * @param providedArgs optional list of argument values to check by type + * (via {@code instanceof}) for resolving method arguments. * @return Publisher that produces a single HandlerResult or an error signal; * never throws an exception. */ @@ -98,11 +103,8 @@ public class InvocableHandlerMethod extends HandlerMethod { return Publishers.concatMap(argsPublisher, args -> { try { Object value = doInvoke(args); - - HandlerMethod handlerMethod = InvocableHandlerMethod.this; - ResolvableType type = ResolvableType.forMethodParameter(handlerMethod.getReturnType()); - HandlerResult handlerResult = new HandlerResult(handlerMethod, value, type); - + ResolvableType type = ResolvableType.forMethodParameter(getReturnType()); + HandlerResult handlerResult = new HandlerResult(this, value, type); return Publishers.just(handlerResult); } catch (InvocationTargetException ex) { @@ -187,9 +189,8 @@ public class InvocableHandlerMethod extends HandlerMethod { } private static Publisher mapError(Publisher source, Function function) { - return Publishers.lift(source, null, (throwable, subscriber) -> { - subscriber.onError(function.apply(throwable)); - }, null); + return Publishers.lift(source, null, + (throwable, subscriber) -> subscriber.onError(function.apply(throwable)), null); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java index d997913e7d..de6106177d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java @@ -16,30 +16,35 @@ package org.springframework.web.reactive.method.annotation; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.reactivestreams.Publisher; import reactor.Publishers; import org.springframework.beans.factory.InitializingBean; -import org.springframework.core.ResolvableType; +import org.springframework.core.codec.Decoder; +import org.springframework.core.codec.support.ByteBufferDecoder; +import org.springframework.core.codec.support.JacksonJsonDecoder; +import org.springframework.core.codec.support.JsonObjectDecoder; +import org.springframework.core.codec.support.StringDecoder; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; -import org.springframework.core.codec.support.ByteBufferDecoder; -import org.springframework.core.codec.Decoder; -import org.springframework.core.codec.support.JacksonJsonDecoder; -import org.springframework.core.codec.support.JsonObjectDecoder; -import org.springframework.core.codec.support.StringDecoder; import org.springframework.util.ObjectUtils; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.method.annotation.ExceptionHandlerMethodResolver; import org.springframework.web.reactive.HandlerAdapter; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.method.HandlerMethodArgumentResolver; import org.springframework.web.reactive.method.InvocableHandlerMethod; -import org.springframework.web.method.HandlerMethod; /** @@ -47,16 +52,29 @@ import org.springframework.web.method.HandlerMethod; */ public class RequestMappingHandlerAdapter implements HandlerAdapter, InitializingBean { + private static Log logger = LogFactory.getLog(RequestMappingHandlerAdapter.class); + + private final List argumentResolvers = new ArrayList<>(); private ConversionService conversionService = new DefaultConversionService(); + private final Map, ExceptionHandlerMethodResolver> exceptionHandlerCache = + new ConcurrentHashMap, ExceptionHandlerMethodResolver>(64); + + /** + * Configure the complete list of supported argument types thus overriding + * the resolvers that would otherwise be configured by default. + */ public void setArgumentResolvers(List resolvers) { this.argumentResolvers.clear(); this.argumentResolvers.addAll(resolvers); } + /** + * Return the configured argument resolvers. + */ public List getArgumentResolvers() { return this.argumentResolvers; } @@ -91,9 +109,59 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Initializin public Publisher handle(ServerHttpRequest request, ServerHttpResponse response, Object handler) { - InvocableHandlerMethod handlerMethod = new InvocableHandlerMethod((HandlerMethod) handler); - handlerMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers); - return handlerMethod.invokeForRequest(request); + HandlerMethod handlerMethod = (HandlerMethod) handler; + + InvocableHandlerMethod invocable = new InvocableHandlerMethod(handlerMethod); + invocable.setHandlerMethodArgumentResolvers(this.argumentResolvers); + + Publisher publisher = invocable.invokeForRequest(request); + + publisher = Publishers.onErrorResumeNext(publisher, ex -> { + return Publishers.just(new HandlerResult(handler, ex)); + }); + + publisher = Publishers.map(publisher, + result -> result.setExceptionMapper( + ex -> mapException((Exception) ex, handlerMethod, request, response))); + + return publisher; + } + + private Publisher mapException(Throwable ex, HandlerMethod handlerMethod, + ServerHttpRequest request, ServerHttpResponse response) { + + if (ex instanceof Exception) { + InvocableHandlerMethod invocable = findExceptionHandler(handlerMethod, (Exception) ex); + if (invocable != null) { + try { + if (logger.isDebugEnabled()) { + logger.debug("Invoking @ExceptionHandler method: " + invocable); + } + invocable.setHandlerMethodArgumentResolvers(getArgumentResolvers()); + return invocable.invokeForRequest(request, response, ex); + } + catch (Exception invocationEx) { + if (logger.isErrorEnabled()) { + logger.error("Failed to invoke @ExceptionHandler method: " + invocable, invocationEx); + } + } + } + } + return Publishers.error(ex); + } + + protected InvocableHandlerMethod findExceptionHandler(HandlerMethod handlerMethod, Exception exception) { + if (handlerMethod == null) { + return null; + } + Class handlerType = handlerMethod.getBeanType(); + ExceptionHandlerMethodResolver resolver = this.exceptionHandlerCache.get(handlerType); + if (resolver == null) { + resolver = new ExceptionHandlerMethodResolver(handlerType); + this.exceptionHandlerCache.put(handlerType, resolver); + } + Method method = resolver.resolveMethod(exception); + return (method != null ? new InvocableHandlerMethod(handlerMethod.getBean(), method) : null); } } \ No newline at end of file diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AbstractHttpHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AbstractHttpHandlerIntegrationTests.java index fb11e7b151..83781e29f9 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AbstractHttpHandlerIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AbstractHttpHandlerIntegrationTests.java @@ -21,7 +21,6 @@ import org.junit.Before; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; -import org.springframework.http.server.reactive.HttpHandler; import org.springframework.http.server.reactive.boot.HttpServer; import org.springframework.http.server.reactive.boot.JettyHttpServer; import org.springframework.http.server.reactive.boot.ReactorHttpServer; diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/WriteWithOperatorTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/WriteWithOperatorTests.java index ac847a7f99..2ad1ddd3d4 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/WriteWithOperatorTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/WriteWithOperatorTests.java @@ -31,7 +31,7 @@ import reactor.Publishers; import reactor.core.publisher.PublisherFactory; import reactor.core.subscriber.SubscriberBarrier; import reactor.rx.Streams; -import reactor.rx.action.Signal; +import reactor.rx.stream.Signal; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java index 15d6d8feba..50ac1bd873 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java @@ -55,6 +55,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.http.server.reactive.AbstractHttpHandlerIntegrationTests; import org.springframework.http.server.reactive.HttpHandler; import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -122,6 +123,30 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati assertEquals("Hello!", response.getBody()); } + @Test + public void handleWithThrownException() throws Exception { + + RestTemplate restTemplate = new RestTemplate(); + + URI url = new URI("http://localhost:" + port + "/thrown-exception"); + RequestEntity request = RequestEntity.get(url).build(); + ResponseEntity response = restTemplate.exchange(request, String.class); + + assertEquals("Recovered from error: Boo", response.getBody()); + } + + @Test + public void handleWithErrorSignal() throws Exception { + + RestTemplate restTemplate = new RestTemplate(); + + URI url = new URI("http://localhost:" + port + "/error-signal"); + RequestEntity request = RequestEntity.get(url).build(); + ResponseEntity response = restTemplate.exchange(request, String.class); + + assertEquals("Recovered from error: Boo", response.getBody()); + } + @Test public void serializeAsPojo() throws Exception { serializeAsPojo("http://localhost:" + port + "/person"); @@ -478,6 +503,24 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati return personStream.toList().doOnNext(persons::addAll).flatMap(document -> Observable.empty()); } + @RequestMapping("/thrown-exception") + @ResponseBody + public Publisher handleAndThrowException() { + throw new IllegalStateException("Boo"); + } + + @RequestMapping("/error-signal") + @ResponseBody + public Publisher handleWithError() { + return Publishers.error(new IllegalStateException("Boo")); + } + + @ExceptionHandler + @ResponseBody + public Publisher handleException(IllegalStateException ex) { + return Streams.just("Recovered from error: " + ex.getMessage()); + } + //TODO add mixed and T request mappings tests } From 6b05d172487c3ab27bc947e6b9de950242e442a2 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 29 Dec 2015 17:35:19 -0500 Subject: [PATCH 135/498] Response headers always in sync with native response ServerHttpResponse implementations now immediately propagate HttpHeaders changes as they so there is no need to call applyHeaders(). The writeHeaders from ServerHttpResponse is also removed. RxNetty and Reactor Net both support implicitly completing if the handler completes without explicitly writing the headers or the response body. --- .../http/ExtendedHttpHeaders.java | 99 +++++++++++++++++++ .../InternalServerErrorExceptionHandler.java | 3 +- .../reactive/ReactorServerHttpResponse.java | 56 ++++++----- .../reactive/RxNettyServerHttpRequest.java | 4 +- .../reactive/RxNettyServerHttpResponse.java | 57 ++++++----- .../server/reactive/ServerHttpResponse.java | 9 -- .../reactive/ServletServerHttpResponse.java | 63 ++++++------ .../reactive/UndertowServerHttpResponse.java | 74 +++++++------- .../ResponseStatusExceptionHandler.java | 2 +- .../handler/SimpleHandlerResultHandler.java | 12 ++- .../reactive/MockServerHttpResponse.java | 5 - ...mpleUrlHandlerMappingIntegrationTests.java | 28 +++++- 12 files changed, 267 insertions(+), 145 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/http/ExtendedHttpHeaders.java diff --git a/spring-web-reactive/src/main/java/org/springframework/http/ExtendedHttpHeaders.java b/spring-web-reactive/src/main/java/org/springframework/http/ExtendedHttpHeaders.java new file mode 100644 index 0000000000..ade3903d89 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/http/ExtendedHttpHeaders.java @@ -0,0 +1,99 @@ +/* + * Copyright 2002-2015 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; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +/** + * Extension of HttpHeaders (to be merged into HttpHeaders) that allows the + * registration of {@link HeaderChangeListener}. For use with HTTP response + * implementations that can keep track of changes made headers and keep the + * underlying server headers always in sync. + * + * @author Rossen Stoyanchev + */ +public class ExtendedHttpHeaders extends HttpHeaders { + + private final List listeners = new ArrayList<>(1); + + + public void registerChangeListener(HeaderChangeListener listener) { + this.listeners.add(listener); + } + + + @Override + public void add(String name, String value) { + for (HeaderChangeListener listener : this.listeners) { + listener.headerAdded(name, value); + } + super.add(name, value); + } + + @Override + public void set(String name, String value) { + List values = new LinkedList(); + values.add(value); + put(name, values); + } + + @Override + public List put(String key, List values) { + for (HeaderChangeListener listener : this.listeners) { + listener.headerPut(key, values); + } + return super.put(key, values); + } + + @Override + public List remove(Object key) { + for (HeaderChangeListener listener : this.listeners) { + listener.headerRemoved((String) key); + } + return super.remove(key); + } + + @Override + public void putAll(Map> map) { + for (Entry> entry : map.entrySet()) { + put(entry.getKey(), entry.getValue()); + } + super.putAll(map); + } + + @Override + public void clear() { + for (Entry> entry : super.entrySet()) { + remove(entry.getKey(), entry.getValue()); + } + super.clear(); + } + + + public interface HeaderChangeListener { + + void headerAdded(String name, String value); + + void headerPut(String key, List values); + + void headerRemoved(String key); + + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/InternalServerErrorExceptionHandler.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/InternalServerErrorExceptionHandler.java index b9a69eac64..7c88c8febe 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/InternalServerErrorExceptionHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/InternalServerErrorExceptionHandler.java @@ -16,6 +16,7 @@ package org.springframework.http.server.reactive; import org.reactivestreams.Publisher; +import reactor.Publishers; import org.springframework.http.HttpStatus; @@ -30,7 +31,7 @@ public class InternalServerErrorExceptionHandler implements HttpExceptionHandler @Override public Publisher handle(ServerHttpRequest request, ServerHttpResponse response, Throwable ex) { response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR); - return response.writeHeaders(); + return Publishers.empty(); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java index 3233d38daa..c774ead81f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java @@ -16,6 +16,7 @@ package org.springframework.http.server.reactive; import java.nio.ByteBuffer; +import java.util.List; import org.reactivestreams.Publisher; import reactor.Publishers; @@ -23,12 +24,14 @@ import reactor.io.buffer.Buffer; import reactor.io.net.http.HttpChannel; import reactor.io.net.http.model.Status; +import org.springframework.http.ExtendedHttpHeaders; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.util.Assert; /** * @author Stephane Maldini + * @author Rossen Stoyanchev */ public class ReactorServerHttpResponse implements ServerHttpResponse { @@ -36,13 +39,17 @@ public class ReactorServerHttpResponse implements ServerHttpResponse { private final HttpHeaders headers; - private boolean headersWritten = false; - public ReactorServerHttpResponse(HttpChannel response) { Assert.notNull("'response', response must not be null."); this.channel = response; - this.headers = new HttpHeaders(); + this.headers = initHttpHeaders(); + } + + private HttpHeaders initHttpHeaders() { + ExtendedHttpHeaders headers = new ExtendedHttpHeaders(); + headers.registerChangeListener(new ReactorHeaderChangeListener()); + return headers; } @@ -53,34 +60,33 @@ public class ReactorServerHttpResponse implements ServerHttpResponse { @Override public HttpHeaders getHeaders() { - return (this.headersWritten ? HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers); - } - - @Override - public Publisher writeHeaders() { - if (this.headersWritten) { - return Publishers.empty(); - } - applyHeaders(); - return this.channel.writeHeaders(); + return this.headers; } @Override public Publisher setBody(Publisher publisher) { - return Publishers.lift(publisher, new WriteWithOperator<>(writePublisher -> { - applyHeaders(); - return this.channel.writeWith(Publishers.map(writePublisher, Buffer::new)); - })); + return Publishers.lift(publisher, new WriteWithOperator<>(writePublisher -> + this.channel.writeWith(Publishers.map(writePublisher, Buffer::new)))); } - private void applyHeaders() { - if (!this.headersWritten) { - for (String name : this.headers.keySet()) { - for (String value : this.headers.get(name)) { - this.channel.responseHeaders().add(name, value); - } - } - this.headersWritten = true; + + private class ReactorHeaderChangeListener implements ExtendedHttpHeaders.HeaderChangeListener { + + @Override + public void headerAdded(String name, String value) { + channel.responseHeaders().add(name, value); + } + + @Override + public void headerPut(String key, List values) { + channel.responseHeaders().remove(key); + channel.responseHeaders().add(key, values); + } + + @Override + public void headerRemoved(String key) { + channel.responseHeaders().remove(key); } } + } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java index cf075d9f3f..2f4ae920c5 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java @@ -78,7 +78,9 @@ public class RxNettyServerHttpRequest implements ServerHttpRequest { @Override public Publisher getBody() { - Observable bytesContent = this.request.getContent().map(ByteBuf::nioBuffer); + Observable bytesContent = this.request.getContent() + .concatWith(Observable.empty()) + .map(ByteBuf::nioBuffer); return RxJava1Converter.from(bytesContent); } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java index 78f7141589..2b316492b0 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java @@ -17,15 +17,16 @@ package org.springframework.http.server.reactive; import java.nio.ByteBuffer; +import java.util.List; import io.netty.handler.codec.http.HttpResponseStatus; import io.reactivex.netty.protocol.http.server.HttpServerResponse; import org.reactivestreams.Publisher; import reactor.Publishers; import reactor.core.publisher.convert.RxJava1Converter; -import reactor.io.buffer.Buffer; import rx.Observable; +import org.springframework.http.ExtendedHttpHeaders; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.util.Assert; @@ -40,13 +41,17 @@ public class RxNettyServerHttpResponse implements ServerHttpResponse { private final HttpHeaders headers; - private boolean headersWritten = false; - public RxNettyServerHttpResponse(HttpServerResponse response) { Assert.notNull("'response', response must not be null."); this.response = response; - this.headers = new HttpHeaders(); + this.headers = initHttpHeaders(); + } + + private HttpHeaders initHttpHeaders() { + ExtendedHttpHeaders headers = new ExtendedHttpHeaders(); + headers.registerChangeListener(new RxNettyHeaderChangeListener()); + return headers; } @@ -57,36 +62,42 @@ public class RxNettyServerHttpResponse implements ServerHttpResponse { @Override public HttpHeaders getHeaders() { - return (this.headersWritten ? HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers); - } - - @Override - public Publisher writeHeaders() { - if (this.headersWritten) { - return Publishers.empty(); - } - applyHeaders(); - return RxJava1Converter.from(this.response.sendHeaders()); + return this.headers; } @Override public Publisher setBody(Publisher publisher) { return Publishers.lift(publisher, new WriteWithOperator<>(writePublisher -> { - applyHeaders(); Observable observable = RxJava1Converter.from(writePublisher) - .map(buffer -> new Buffer(buffer).asBytes()); + .map(buffer -> { + byte[] bytes = new byte[buffer.remaining()]; + buffer.get(bytes); + return bytes; + }); return RxJava1Converter.from(this.response.writeBytes(observable)); })); } - private void applyHeaders() { - if (!this.headersWritten) { - for (String name : this.headers.keySet()) { - for (String value : this.headers.get(name)) { - this.response.addHeader(name, value); - } + + private class RxNettyHeaderChangeListener implements ExtendedHttpHeaders.HeaderChangeListener { + + @Override + public void headerAdded(String name, String value) { + response.addHeader(name, value); + } + + @Override + public void headerPut(String key, List values) { + response.removeHeader(key); + for (String value : values) { + response.addHeader(key, value); } - this.headersWritten = true; + } + + @Override + public void headerRemoved(String key) { + response.removeHeader(key); } } + } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java index dabf620f1d..4b31cf19c3 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java @@ -16,8 +16,6 @@ package org.springframework.http.server.reactive; -import org.reactivestreams.Publisher; - import org.springframework.http.HttpStatus; import org.springframework.http.ReactiveHttpOutputMessage; @@ -34,11 +32,4 @@ public interface ServerHttpResponse extends ReactiveHttpOutputMessage { */ void setStatusCode(HttpStatus status); - /** - * Write the response headers. This method must be invoked to send responses without body. - * @return A {@code Publisher} used to signal the demand, and receive a notification - * when the handling is complete (success or error) including the flush of the data on the - * network. - */ - Publisher writeHeaders(); } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java index 984f9dd9ca..72ec01a344 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java @@ -18,9 +18,7 @@ package org.springframework.http.server.reactive; import java.io.IOException; import java.nio.ByteBuffer; -import java.nio.charset.Charset; import java.util.List; -import java.util.Map; import javax.servlet.ServletOutputStream; import javax.servlet.WriteListener; import javax.servlet.http.HttpServletResponse; @@ -32,9 +30,9 @@ import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import reactor.Publishers; +import org.springframework.http.ExtendedHttpHeaders; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; import org.springframework.util.Assert; /** @@ -51,16 +49,20 @@ public class ServletServerHttpResponse implements ServerHttpResponse { private final ResponseBodySubscriber subscriber; - private boolean headersWritten = false; - public ServletServerHttpResponse(HttpServletResponse response, ServletAsyncContextSynchronizer synchronizer) { Assert.notNull(response, "'response' must not be null"); this.response = response; - this.headers = new HttpHeaders(); + this.headers = initHttpHeaders(); this.subscriber = new ResponseBodySubscriber(synchronizer); } + private HttpHeaders initHttpHeaders() { + ExtendedHttpHeaders headers = new ExtendedHttpHeaders(); + headers.registerChangeListener(new ServletHeaderChangeListener()); + return headers; + } + @Override public void setStatusCode(HttpStatus status) { @@ -69,48 +71,41 @@ public class ServletServerHttpResponse implements ServerHttpResponse { @Override public HttpHeaders getHeaders() { - return (this.headersWritten ? HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers); + return this.headers; } WriteListener getWriteListener() { return this.subscriber; } - @Override - public Publisher writeHeaders() { - applyHeaders(); - return Publishers.empty(); - } - @Override public Publisher setBody(final Publisher publisher) { - return Publishers.lift(publisher, new WriteWithOperator<>(writePublisher -> { - applyHeaders(); - return (s -> writePublisher.subscribe(subscriber)); - })); + return Publishers.lift(publisher, new WriteWithOperator<>(writePublisher -> + (s -> writePublisher.subscribe(subscriber)))); } - private void applyHeaders() { - if (!this.headersWritten) { - for (Map.Entry> entry : this.headers.entrySet()) { - String headerName = entry.getKey(); - for (String headerValue : entry.getValue()) { - this.response.addHeader(headerName, headerValue); - } + + private class ServletHeaderChangeListener implements ExtendedHttpHeaders.HeaderChangeListener { + + @Override + public void headerAdded(String name, String value) { + response.addHeader(name, value); + } + + @Override + public void headerPut(String key, List values) { + // We can only add but not remove + for (String value : values) { + response.addHeader(key, value); } - MediaType contentType = this.headers.getContentType(); - if (this.response.getContentType() == null && contentType != null) { - this.response.setContentType(contentType.toString()); - } - Charset charset = (contentType != null ? contentType.getCharSet() : null); - if (this.response.getCharacterEncoding() == null && charset != null) { - this.response.setCharacterEncoding(charset.name()); - } - this.headersWritten = true; + } + + @Override + public void headerRemoved(String key) { + // No Servlet support for removing headers } } - private static class ResponseBodySubscriber implements WriteListener, Subscriber { private final ServletAsyncContextSynchronizer synchronizer; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java index 01f1d9ee53..b7fa1edfb6 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java @@ -19,16 +19,11 @@ package org.springframework.http.server.reactive; import java.io.IOException; import java.nio.ByteBuffer; import java.util.List; -import java.util.Map; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.util.Assert; - import io.undertow.connector.PooledByteBuffer; import io.undertow.server.HttpServerExchange; import io.undertow.util.HttpString; @@ -41,6 +36,11 @@ import org.xnio.channels.StreamSinkChannel; import reactor.Publishers; import reactor.core.subscriber.BaseSubscriber; +import org.springframework.http.ExtendedHttpHeaders; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.util.Assert; + import static org.xnio.ChannelListeners.closingChannelExceptionHandler; import static org.xnio.ChannelListeners.flushingChannelListener; import static org.xnio.IoUtils.safeClose; @@ -58,14 +58,19 @@ public class UndertowServerHttpResponse implements ServerHttpResponse { private final ResponseBodySubscriber bodySubscriber = new ResponseBodySubscriber(); - private final HttpHeaders headers = new HttpHeaders(); - - private boolean headersWritten = false; + private final HttpHeaders headers; public UndertowServerHttpResponse(HttpServerExchange exchange) { Assert.notNull(exchange, "'exchange' is required."); this.exchange = exchange; + this.headers = initHttpHeaders(); + } + + private HttpHeaders initHttpHeaders() { + ExtendedHttpHeaders headers = new ExtendedHttpHeaders(); + headers.registerChangeListener(new UndertowHeaderChangeListener()); + return headers; } @@ -77,44 +82,34 @@ public class UndertowServerHttpResponse implements ServerHttpResponse { @Override public HttpHeaders getHeaders() { - return (this.headersWritten ? HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers); - } - - @Override - public Publisher writeHeaders() { - applyHeaders(); - return s -> s.onSubscribe(new Subscription() { - @Override - public void request(long n) { - s.onComplete(); - } - - @Override - public void cancel() { - } - }); - } - - private void applyHeaders() { - if (!this.headersWritten) { - for (Map.Entry> entry : this.headers.entrySet()) { - HttpString headerName = HttpString.tryFromString(entry.getKey()); - this.exchange.getResponseHeaders().addAll(headerName, entry.getValue()); - - } - this.headersWritten = true; - } + return this.headers; } @Override public Publisher setBody(Publisher publisher) { - return Publishers.lift(publisher, new WriteWithOperator<>(writePublisher -> { - applyHeaders(); - return (subscriber -> writePublisher.subscribe(bodySubscriber)); - })); + return Publishers.lift(publisher, new WriteWithOperator<>(writePublisher -> + (subscriber -> writePublisher.subscribe(bodySubscriber)))); } + private class UndertowHeaderChangeListener implements ExtendedHttpHeaders.HeaderChangeListener { + + @Override + public void headerAdded(String name, String value) { + exchange.getResponseHeaders().add(HttpString.tryFromString(name), value); + } + + @Override + public void headerPut(String key, List values) { + exchange.getResponseHeaders().putAll(HttpString.tryFromString(key), values); + } + + @Override + public void headerRemoved(String key) { + exchange.getResponseHeaders().remove(key); + } + } + private class ResponseBodySubscriber extends BaseSubscriber implements ChannelListener { @@ -266,4 +261,5 @@ public class UndertowServerHttpResponse implements ServerHttpResponse { } } } + } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/ResponseStatusExceptionHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/ResponseStatusExceptionHandler.java index 828c9a52c5..f9e3135532 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/ResponseStatusExceptionHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/ResponseStatusExceptionHandler.java @@ -35,7 +35,7 @@ public class ResponseStatusExceptionHandler implements HttpExceptionHandler { public Publisher handle(ServerHttpRequest request, ServerHttpResponse response, Throwable ex) { if (ex instanceof ResponseStatusException) { response.setStatusCode(((ResponseStatusException) ex).getHttpStatus()); - return response.writeHeaders(); + return Publishers.empty(); } return Publishers.error(ex); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java index 6c7bd02930..efa8d7e831 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java @@ -16,8 +16,6 @@ package org.springframework.web.reactive.handler; -import java.util.Arrays; - import org.reactivestreams.Publisher; import reactor.Publishers; @@ -74,15 +72,19 @@ public class SimpleHandlerResultHandler implements Ordered, HandlerResultHandler ((this.conversionService != null) && this.conversionService.canConvert(type.getRawClass(), Publisher.class)); } + @SuppressWarnings("unchecked") @Override public Publisher handleResult(ServerHttpRequest request, ServerHttpResponse response, HandlerResult result) { Object value = result.getResult(); + if (Void.TYPE.equals(result.getResultType().getRawClass())) { - return response.writeHeaders(); + return Publishers.empty(); } - Publisher completion = (value instanceof Publisher ? (Publisher)value : this.conversionService.convert(value, Publisher.class)); - return Publishers.concat(Publishers.from(Arrays.asList(completion, response.writeHeaders()))); + + return (value instanceof Publisher ? (Publisher)value : + this.conversionService.convert(value, Publisher.class)); } + } diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java index ba12b8d76d..d478d56702 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java @@ -49,11 +49,6 @@ public class MockServerHttpResponse implements ServerHttpResponse { return this.headers; } - @Override - public Publisher writeHeaders() { - return Publishers.empty(); - } - @Override public Publisher setBody(Publisher body) { this.body = body; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java index d0de9544d0..da6cf3fc28 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java @@ -23,6 +23,7 @@ import java.util.Map; import org.junit.Test; import org.reactivestreams.Publisher; +import reactor.Publishers; import reactor.io.buffer.Buffer; import reactor.rx.Streams; @@ -67,7 +68,7 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler } @Test - public void testFoo() throws Exception { + public void testFooHandler() throws Exception { RestTemplate restTemplate = new RestTemplate(); @@ -80,7 +81,7 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler } @Test - public void testBar() throws Exception { + public void testBarHandler() throws Exception { RestTemplate restTemplate = new RestTemplate(); @@ -92,6 +93,19 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler assertArrayEquals("bar".getBytes(UTF_8), response.getBody()); } + @Test + public void testHeaderSettingHandler() throws Exception { + + RestTemplate restTemplate = new RestTemplate(); + + URI url = new URI("http://localhost:" + port + "/header"); + RequestEntity request = RequestEntity.get(url).build(); + ResponseEntity response = restTemplate.exchange(request, byte[].class); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals("bar", response.getHeaders().getFirst("foo")); + } + @Test public void testNotFound() throws Exception { @@ -114,6 +128,7 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler Map map = new HashMap<>(); map.put("/foo", new FooHandler()); map.put("/bar", new BarHandler()); + map.put("/header", new HeaderSettingHandler()); setHandlers(map); } } @@ -134,4 +149,13 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler } } + private static class HeaderSettingHandler implements HttpHandler { + + @Override + public Publisher handle(ServerHttpRequest request, ServerHttpResponse response) { + response.getHeaders().add("foo", "bar"); + return Publishers.empty(); + } + } + } From 869f6bef4086fe66d642a7e4f841c5716e6e45a3 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 31 Dec 2015 12:27:40 -0500 Subject: [PATCH 136/498] Polish server request & response implementations --- .../http/ExtendedHttpHeaders.java | 16 +++-- .../http/ReactiveHttpInputMessage.java | 10 ++-- .../http/ReactiveHttpOutputMessage.java | 15 ++--- .../reactive/ReactorServerHttpRequest.java | 42 ++++++++------ .../reactive/ReactorServerHttpResponse.java | 31 +++++----- .../reactive/RxNettyServerHttpRequest.java | 58 ++++++++++--------- .../reactive/RxNettyServerHttpResponse.java | 44 +++++++------- .../reactive/ServletServerHttpRequest.java | 47 +++++++++------ .../reactive/ServletServerHttpResponse.java | 26 +++++---- .../reactive/UndertowServerHttpRequest.java | 38 +++++++----- .../reactive/UndertowServerHttpResponse.java | 33 ++++++----- 11 files changed, 205 insertions(+), 155 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/ExtendedHttpHeaders.java b/spring-web-reactive/src/main/java/org/springframework/http/ExtendedHttpHeaders.java index ade3903d89..f5fa19922b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/ExtendedHttpHeaders.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/ExtendedHttpHeaders.java @@ -21,10 +21,11 @@ import java.util.List; import java.util.Map; /** - * Extension of HttpHeaders (to be merged into HttpHeaders) that allows the - * registration of {@link HeaderChangeListener}. For use with HTTP response - * implementations that can keep track of changes made headers and keep the - * underlying server headers always in sync. + * Variant of HttpHeaders (to be merged into HttpHeaders) that supports the + * registration of {@link HeaderChangeListener}s. + * + *

    For use with HTTP server response implementations that wish to propagate + * header header changes to the underlying runtime as they occur. * * @author Rossen Stoyanchev */ @@ -33,7 +34,10 @@ public class ExtendedHttpHeaders extends HttpHeaders { private final List listeners = new ArrayList<>(1); - public void registerChangeListener(HeaderChangeListener listener) { + public ExtendedHttpHeaders() { + } + + public ExtendedHttpHeaders(HeaderChangeListener listener) { this.listeners.add(listener); } @@ -48,7 +52,7 @@ public class ExtendedHttpHeaders extends HttpHeaders { @Override public void set(String name, String value) { - List values = new LinkedList(); + List values = new LinkedList<>(); values.add(value); put(name, values); } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpInputMessage.java b/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpInputMessage.java index 7870571b4a..3cff8d9553 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpInputMessage.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpInputMessage.java @@ -21,11 +21,9 @@ import java.nio.ByteBuffer; import org.reactivestreams.Publisher; /** - * Represents a "reactive" HTTP input message, consisting of - * {@linkplain #getHeaders() headers} and a readable - * {@linkplain #getBody() streaming body }. + * An "reactive" HTTP input message that exposes the input as {@link Publisher}. * - *

    Typically implemented by an HTTP request on the server-side, or a response + *

    Typically implemented by an HTTP request on the server-side or a response * on the client-side. * * @author Arjen Poutsma @@ -33,8 +31,8 @@ import org.reactivestreams.Publisher; public interface ReactiveHttpInputMessage extends HttpMessage { /** - * Return the body of the message as an publisher of {@code ByteBuffer}s. - * @return the body + * Return the body of the message as a {@link Publisher}. + * @return the body content publisher */ Publisher getBody(); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java b/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java index 33df2dcdee..83b3507df4 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java @@ -21,11 +21,9 @@ import java.nio.ByteBuffer; import org.reactivestreams.Publisher; /** - * Represents a "reactive" HTTP output message, consisting of - * {@linkplain #getHeaders() headers} and the capability to add a - * {@linkplain #setBody(Publisher) body}. + * A "reactive" HTTP output message that accepts output as a {@link Publisher}. * - *

    Typically implemented by an HTTP request on the client-side, or a response + *

    Typically implemented by an HTTP request on the client-side or a response * on the server-side. * * @author Arjen Poutsma @@ -33,12 +31,11 @@ import org.reactivestreams.Publisher; public interface ReactiveHttpOutputMessage extends HttpMessage { /** - * Sets the body of this message to the given publisher of {@link ByteBuffer}s. - * The publisher will be used to write to the underlying HTTP layer with - * asynchronously, given pull demand by this layer. + * Set the body of the message to the given {@link Publisher} which will be + * used to write to the underlying HTTP layer. * - * @param body the body to use - * @return a publisher that indicates completion + * @param body the body content publisher + * @return a publisher that indicates completion or error. */ Publisher setBody(Publisher body); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java index 343ec20f24..493e37df65 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java @@ -26,36 +26,30 @@ import reactor.io.net.http.HttpChannel; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; -import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.util.Assert; /** + * Adapt {@link ServerHttpRequest} to the Reactor Net {@link HttpChannel}. + * * @author Stephane Maldini */ public class ReactorServerHttpRequest implements ServerHttpRequest { private final HttpChannel channel; + private URI uri; + private HttpHeaders headers; public ReactorServerHttpRequest(HttpChannel request) { - Assert.notNull("'request', request must not be null."); + Assert.notNull("'request' must not be null."); this.channel = request; } - @Override - public HttpHeaders getHeaders() { - if (this.headers == null) { - this.headers = new HttpHeaders(); - for (String name : this.channel.headers().names()) { - for (String value : this.channel.headers().getAll(name)) { - this.headers.add(name, value); - } - } - } - return this.headers; + public HttpChannel getReactorChannel() { + return this.channel; } @Override @@ -65,12 +59,26 @@ public class ReactorServerHttpRequest implements ServerHttpRequest { @Override public URI getURI() { - try { - return new URI(this.channel.uri()); - } catch (URISyntaxException ex) { - throw new IllegalStateException("Could not get URI: " + ex.getMessage(), ex); + if (this.uri == null) { + try { + this.uri = new URI(this.channel.uri()); + } + catch (URISyntaxException ex) { + throw new IllegalStateException("Could not get URI: " + ex.getMessage(), ex); + } } + return this.uri; + } + @Override + public HttpHeaders getHeaders() { + if (this.headers == null) { + this.headers = new HttpHeaders(); + for (String name : this.channel.headers().names()) { + this.headers.put(name, this.channel.headers().getAll(name)); + } + } + return this.headers; } @Override diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java index c774ead81f..d3fdf10a22 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java @@ -30,6 +30,8 @@ import org.springframework.http.HttpStatus; import org.springframework.util.Assert; /** + * Adapt {@link ServerHttpResponse} to the Reactor Net {@link HttpChannel}. + * * @author Stephane Maldini * @author Rossen Stoyanchev */ @@ -41,21 +43,19 @@ public class ReactorServerHttpResponse implements ServerHttpResponse { public ReactorServerHttpResponse(HttpChannel response) { - Assert.notNull("'response', response must not be null."); + Assert.notNull("'response' must not be null."); this.channel = response; - this.headers = initHttpHeaders(); + this.headers = new ExtendedHttpHeaders(new ReactorHeaderChangeListener()); } - private HttpHeaders initHttpHeaders() { - ExtendedHttpHeaders headers = new ExtendedHttpHeaders(); - headers.registerChangeListener(new ReactorHeaderChangeListener()); - return headers; - } + public HttpChannel getReactorChannel() { + return this.channel; + } @Override public void setStatusCode(HttpStatus status) { - this.channel.responseStatus(Status.valueOf(status.value())); + getReactorChannel().responseStatus(Status.valueOf(status.value())); } @Override @@ -65,8 +65,11 @@ public class ReactorServerHttpResponse implements ServerHttpResponse { @Override public Publisher setBody(Publisher publisher) { - return Publishers.lift(publisher, new WriteWithOperator<>(writePublisher -> - this.channel.writeWith(Publishers.map(writePublisher, Buffer::new)))); + return Publishers.lift(publisher, new WriteWithOperator<>(this::setBodyInternal)); + } + + protected Publisher setBodyInternal(Publisher publisher) { + return getReactorChannel().writeWith(Publishers.map(publisher, Buffer::new)); } @@ -74,18 +77,18 @@ public class ReactorServerHttpResponse implements ServerHttpResponse { @Override public void headerAdded(String name, String value) { - channel.responseHeaders().add(name, value); + getReactorChannel().responseHeaders().add(name, value); } @Override public void headerPut(String key, List values) { - channel.responseHeaders().remove(key); - channel.responseHeaders().add(key, values); + getReactorChannel().responseHeaders().remove(key); + getReactorChannel().responseHeaders().add(key, values); } @Override public void headerRemoved(String key) { - channel.responseHeaders().remove(key); + getReactorChannel().responseHeaders().remove(key); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java index 2f4ae920c5..6afad37829 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java @@ -31,6 +31,8 @@ import org.springframework.http.HttpMethod; import org.springframework.util.Assert; /** + * Adapt {@link ServerHttpRequest} to the RxNetty {@link HttpServerRequest}. + * * @author Rossen Stoyanchev * @author Stephane Maldini */ @@ -38,6 +40,8 @@ public class RxNettyServerHttpRequest implements ServerHttpRequest { private final HttpServerRequest request; + private URI uri; + private HttpHeaders headers; @@ -47,44 +51,44 @@ public class RxNettyServerHttpRequest implements ServerHttpRequest { } + public HttpServerRequest getRxNettyRequest() { + return this.request; + } + + @Override + public HttpMethod getMethod() { + return HttpMethod.valueOf(this.getRxNettyRequest().getHttpMethod().name()); + } + + @Override + public URI getURI() { + if (this.uri == null) { + try { + this.uri = new URI(this.getRxNettyRequest().getUri()); + } + catch (URISyntaxException ex) { + throw new IllegalStateException("Could not get URI: " + ex.getMessage(), ex); + } + } + return this.uri; + } + @Override public HttpHeaders getHeaders() { if (this.headers == null) { this.headers = new HttpHeaders(); - for (String name : this.request.getHeaderNames()) { - for (String value : this.request.getAllHeaderValues(name)) { - this.headers.add(name, value); - } + for (String name : this.getRxNettyRequest().getHeaderNames()) { + this.headers.put(name, this.getRxNettyRequest().getAllHeaderValues(name)); } } return this.headers; } - @Override - public HttpMethod getMethod() { - return HttpMethod.valueOf(this.request.getHttpMethod().name()); - } - - @Override - public URI getURI() { - try { - return new URI(this.request.getUri()); - } - catch (URISyntaxException ex) { - throw new IllegalStateException("Could not get URI: " + ex.getMessage(), ex); - } - - } - @Override public Publisher getBody() { - Observable bytesContent = this.request.getContent() - .concatWith(Observable.empty()) - .map(ByteBuf::nioBuffer); - return RxJava1Converter.from(bytesContent); + Observable content = this.getRxNettyRequest().getContent().map(ByteBuf::nioBuffer); + content = content.concatWith(Observable.empty()); // See GH issue #58 + return RxJava1Converter.from(content); } - public Observable asObservable() { - return this.request.getContent().map(ByteBuf::nioBuffer); - } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java index 2b316492b0..aec6680288 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java @@ -32,6 +32,8 @@ import org.springframework.http.HttpStatus; import org.springframework.util.Assert; /** + * Adapt {@link ServerHttpResponse} to the RxNetty {@link HttpServerResponse}. + * * @author Rossen Stoyanchev * @author Stephane Maldini */ @@ -45,19 +47,17 @@ public class RxNettyServerHttpResponse implements ServerHttpResponse { public RxNettyServerHttpResponse(HttpServerResponse response) { Assert.notNull("'response', response must not be null."); this.response = response; - this.headers = initHttpHeaders(); + this.headers = new ExtendedHttpHeaders(new RxNettyHeaderChangeListener()); } - private HttpHeaders initHttpHeaders() { - ExtendedHttpHeaders headers = new ExtendedHttpHeaders(); - headers.registerChangeListener(new RxNettyHeaderChangeListener()); - return headers; - } + public HttpServerResponse getRxNettyResponse() { + return this.response; + } @Override public void setStatusCode(HttpStatus status) { - this.response.setStatus(HttpResponseStatus.valueOf(status.value())); + getRxNettyResponse().setStatus(HttpResponseStatus.valueOf(status.value())); } @Override @@ -67,15 +67,19 @@ public class RxNettyServerHttpResponse implements ServerHttpResponse { @Override public Publisher setBody(Publisher publisher) { - return Publishers.lift(publisher, new WriteWithOperator<>(writePublisher -> { - Observable observable = RxJava1Converter.from(writePublisher) - .map(buffer -> { - byte[] bytes = new byte[buffer.remaining()]; - buffer.get(bytes); - return bytes; - }); - return RxJava1Converter.from(this.response.writeBytes(observable)); - })); + return Publishers.lift(publisher, new WriteWithOperator<>(this::setBodyInternal)); + } + + protected Publisher setBodyInternal(Publisher publisher) { + Observable content = RxJava1Converter.from(publisher).map(this::toBytes); + Observable completion = getRxNettyResponse().writeBytes(content); + return RxJava1Converter.from(completion); + } + + private byte[] toBytes(ByteBuffer buffer) { + byte[] bytes = new byte[buffer.remaining()]; + buffer.get(bytes); + return bytes; } @@ -83,20 +87,20 @@ public class RxNettyServerHttpResponse implements ServerHttpResponse { @Override public void headerAdded(String name, String value) { - response.addHeader(name, value); + getRxNettyResponse().addHeader(name, value); } @Override public void headerPut(String key, List values) { - response.removeHeader(key); + getRxNettyResponse().removeHeader(key); for (String value : values) { - response.addHeader(key, value); + getRxNettyResponse().addHeader(key, value); } } @Override public void headerRemoved(String key) { - response.removeHeader(key); + getRxNettyResponse().removeHeader(key); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java index 46158c306e..477f2b6a86 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java @@ -43,6 +43,8 @@ import org.springframework.util.LinkedCaseInsensitiveMap; import org.springframework.util.StringUtils; /** + * Adapt {@link ServerHttpRequest} to the Servlet {@link HttpServletRequest}. + * * @author Rossen Stoyanchev */ public class ServletServerHttpRequest implements ServerHttpRequest { @@ -52,44 +54,55 @@ public class ServletServerHttpRequest implements ServerHttpRequest { private static final Log logger = LogFactory.getLog(ServletServerHttpRequest.class); - private final HttpServletRequest servletRequest; + private final HttpServletRequest request; + + private URI uri; private HttpHeaders headers; private final RequestBodyPublisher requestBodyPublisher; - public ServletServerHttpRequest(HttpServletRequest servletRequest, ServletAsyncContextSynchronizer synchronizer) { - Assert.notNull(servletRequest, "HttpServletRequest must not be null"); - this.servletRequest = servletRequest; + public ServletServerHttpRequest(HttpServletRequest request, ServletAsyncContextSynchronizer synchronizer) { + Assert.notNull(request, "'request' must not be null."); + this.request = request; this.requestBodyPublisher = new RequestBodyPublisher(synchronizer, BUFFER_SIZE); } + public HttpServletRequest getServletRequest() { + return this.request; + } + @Override public HttpMethod getMethod() { - return HttpMethod.valueOf(this.servletRequest.getMethod()); + return HttpMethod.valueOf(getServletRequest().getMethod()); } @Override public URI getURI() { - try { - return new URI(this.servletRequest.getScheme(), null, this.servletRequest.getServerName(), - this.servletRequest.getServerPort(), this.servletRequest.getRequestURI(), - this.servletRequest.getQueryString(), null); - } - catch (URISyntaxException ex) { - throw new IllegalStateException("Could not get HttpServletRequest URI: " + ex.getMessage(), ex); + if (this.uri == null) { + try { + this.uri = new URI(getServletRequest().getScheme(), null, + getServletRequest().getServerName(), + getServletRequest().getServerPort(), + getServletRequest().getRequestURI(), + getServletRequest().getQueryString(), null); + } + catch (URISyntaxException ex) { + throw new IllegalStateException("Could not get HttpServletRequest URI: " + ex.getMessage(), ex); + } } + return this.uri; } @Override public HttpHeaders getHeaders() { if (this.headers == null) { this.headers = new HttpHeaders(); - for (Enumeration names = this.servletRequest.getHeaderNames(); names.hasMoreElements(); ) { + for (Enumeration names = getServletRequest().getHeaderNames(); names.hasMoreElements(); ) { String headerName = (String) names.nextElement(); - for (Enumeration headerValues = this.servletRequest.getHeaders(headerName); + for (Enumeration headerValues = getServletRequest().getHeaders(headerName); headerValues.hasMoreElements(); ) { String headerValue = (String) headerValues.nextElement(); this.headers.add(headerName, headerValue); @@ -98,14 +111,14 @@ public class ServletServerHttpRequest implements ServerHttpRequest { // HttpServletRequest exposes some headers as properties: we should include those if not already present MediaType contentType = this.headers.getContentType(); if (contentType == null) { - String requestContentType = this.servletRequest.getContentType(); + String requestContentType = getServletRequest().getContentType(); if (StringUtils.hasLength(requestContentType)) { contentType = MediaType.parseMediaType(requestContentType); this.headers.setContentType(contentType); } } if (contentType != null && contentType.getCharSet() == null) { - String requestEncoding = this.servletRequest.getCharacterEncoding(); + String requestEncoding = getServletRequest().getCharacterEncoding(); if (StringUtils.hasLength(requestEncoding)) { Charset charSet = Charset.forName(requestEncoding); Map params = new LinkedCaseInsensitiveMap<>(); @@ -116,7 +129,7 @@ public class ServletServerHttpRequest implements ServerHttpRequest { } } if (this.headers.getContentLength() == -1) { - int requestContentLength = this.servletRequest.getContentLength(); + int requestContentLength = getServletRequest().getContentLength(); if (requestContentLength != -1) { this.headers.setContentLength(requestContentLength); } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java index 72ec01a344..55dfee31a8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java @@ -36,6 +36,8 @@ import org.springframework.http.HttpStatus; import org.springframework.util.Assert; /** + * Adapt {@link ServerHttpResponse} to the Servlet {@link HttpServletResponse}. + * * @author Rossen Stoyanchev */ public class ServletServerHttpResponse implements ServerHttpResponse { @@ -53,20 +55,18 @@ public class ServletServerHttpResponse implements ServerHttpResponse { public ServletServerHttpResponse(HttpServletResponse response, ServletAsyncContextSynchronizer synchronizer) { Assert.notNull(response, "'response' must not be null"); this.response = response; - this.headers = initHttpHeaders(); + this.headers = new ExtendedHttpHeaders(new ServletHeaderChangeListener()); this.subscriber = new ResponseBodySubscriber(synchronizer); } - private HttpHeaders initHttpHeaders() { - ExtendedHttpHeaders headers = new ExtendedHttpHeaders(); - headers.registerChangeListener(new ServletHeaderChangeListener()); - return headers; - } + public HttpServletResponse getServletResponse() { + return this.response; + } @Override public void setStatusCode(HttpStatus status) { - this.response.setStatus(status.value()); + getServletResponse().setStatus(status.value()); } @Override @@ -80,8 +80,11 @@ public class ServletServerHttpResponse implements ServerHttpResponse { @Override public Publisher setBody(final Publisher publisher) { - return Publishers.lift(publisher, new WriteWithOperator<>(writePublisher -> - (s -> writePublisher.subscribe(subscriber)))); + return Publishers.lift(publisher, new WriteWithOperator<>(this::setBodyInternal)); + } + + protected Publisher setBodyInternal(Publisher publisher) { + return s -> publisher.subscribe(subscriber); } @@ -89,14 +92,14 @@ public class ServletServerHttpResponse implements ServerHttpResponse { @Override public void headerAdded(String name, String value) { - response.addHeader(name, value); + getServletResponse().addHeader(name, value); } @Override public void headerPut(String key, List values) { // We can only add but not remove for (String value : values) { - response.addHeader(key, value); + getServletResponse().addHeader(key, value); } } @@ -106,6 +109,7 @@ public class ServletServerHttpResponse implements ServerHttpResponse { } } + private static class ResponseBodySubscriber implements WriteListener, Subscriber { private final ServletAsyncContextSynchronizer synchronizer; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java index db3e5936d3..e8133f8bc8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java @@ -36,12 +36,13 @@ import reactor.core.support.BackpressureUtils; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; -import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.util.Assert; import static org.xnio.IoUtils.safeClose; /** + * Adapt {@link ServerHttpRequest} to the Underow {@link HttpServerExchange}. + * * @author Marek Hawrylczak * @author Rossen Stoyanchev */ @@ -49,10 +50,12 @@ public class UndertowServerHttpRequest implements ServerHttpRequest { private final HttpServerExchange exchange; - private final Publisher body = new RequestBodyPublisher(); + private URI uri; private HttpHeaders headers; + private final Publisher body = new RequestBodyPublisher(); + public UndertowServerHttpRequest(HttpServerExchange exchange) { Assert.notNull(exchange, "'exchange' is required."); @@ -60,31 +63,38 @@ public class UndertowServerHttpRequest implements ServerHttpRequest { } + public HttpServerExchange getUndertowExchange() { + return this.exchange; + } + @Override public HttpMethod getMethod() { - return HttpMethod.valueOf(this.exchange.getRequestMethod().toString()); + return HttpMethod.valueOf(this.getUndertowExchange().getRequestMethod().toString()); } @Override public URI getURI() { - try { - return new URI(this.exchange.getRequestScheme(), null, this.exchange.getHostName(), - this.exchange.getHostPort(), this.exchange.getRequestURI(), - this.exchange.getQueryString(), null); - } - catch (URISyntaxException ex) { - throw new IllegalStateException("Could not get URI: " + ex.getMessage(), ex); + if (this.uri == null) { + try { + return new URI(this.getUndertowExchange().getRequestScheme(), null, + this.getUndertowExchange().getHostName(), + this.getUndertowExchange().getHostPort(), + this.getUndertowExchange().getRequestURI(), + this.getUndertowExchange().getQueryString(), null); + } + catch (URISyntaxException ex) { + throw new IllegalStateException("Could not get URI: " + ex.getMessage(), ex); + } } + return this.uri; } @Override public HttpHeaders getHeaders() { if (this.headers == null) { this.headers = new HttpHeaders(); - for (HeaderValues headerValues : this.exchange.getRequestHeaders()) { - for (String value : headerValues) { - this.headers.add(headerValues.getHeaderName().toString(), value); - } + for (HeaderValues values : this.getUndertowExchange().getRequestHeaders()) { + this.headers.put(values.getHeaderName().toString(), values); } } return this.headers; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java index b7fa1edfb6..316257bf8b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java @@ -46,6 +46,8 @@ import static org.xnio.ChannelListeners.flushingChannelListener; import static org.xnio.IoUtils.safeClose; /** + * Adapt {@link ServerHttpResponse} to the Undertow {@link HttpServerExchange}. + * * @author Marek Hawrylczak * @author Rossen Stoyanchev */ @@ -56,28 +58,26 @@ public class UndertowServerHttpResponse implements ServerHttpResponse { private final HttpServerExchange exchange; - private final ResponseBodySubscriber bodySubscriber = new ResponseBodySubscriber(); - private final HttpHeaders headers; + private final ResponseBodySubscriber bodySubscriber = new ResponseBodySubscriber(); + public UndertowServerHttpResponse(HttpServerExchange exchange) { Assert.notNull(exchange, "'exchange' is required."); this.exchange = exchange; - this.headers = initHttpHeaders(); + this.headers = new ExtendedHttpHeaders(new UndertowHeaderChangeListener()); } - private HttpHeaders initHttpHeaders() { - ExtendedHttpHeaders headers = new ExtendedHttpHeaders(); - headers.registerChangeListener(new UndertowHeaderChangeListener()); - return headers; - } + public HttpServerExchange getUndertowExchange() { + return this.exchange; + } @Override public void setStatusCode(HttpStatus status) { Assert.notNull(status); - this.exchange.setStatusCode(status.value()); + getUndertowExchange().setStatusCode(status.value()); } @Override @@ -87,8 +87,11 @@ public class UndertowServerHttpResponse implements ServerHttpResponse { @Override public Publisher setBody(Publisher publisher) { - return Publishers.lift(publisher, new WriteWithOperator<>(writePublisher -> - (subscriber -> writePublisher.subscribe(bodySubscriber)))); + return Publishers.lift(publisher, new WriteWithOperator<>(this::setBodyInternal)); + } + + protected Publisher setBodyInternal(Publisher writePublisher) { + return subscriber -> writePublisher.subscribe(bodySubscriber); } @@ -96,17 +99,19 @@ public class UndertowServerHttpResponse implements ServerHttpResponse { @Override public void headerAdded(String name, String value) { - exchange.getResponseHeaders().add(HttpString.tryFromString(name), value); + HttpString headerName = HttpString.tryFromString(name); + getUndertowExchange().getResponseHeaders().add(headerName, value); } @Override public void headerPut(String key, List values) { - exchange.getResponseHeaders().putAll(HttpString.tryFromString(key), values); + HttpString headerName = HttpString.tryFromString(key); + getUndertowExchange().getResponseHeaders().putAll(headerName, values); } @Override public void headerRemoved(String key) { - exchange.getResponseHeaders().remove(key); + getUndertowExchange().getResponseHeaders().remove(key); } } From 0aade73c88d5bbde5e6f7383560eeae017045bae Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 31 Dec 2015 12:55:09 -0500 Subject: [PATCH 137/498] Update Servlet + Undertow req & resp implementations For those runtimes that don't directly support Reacitve Streams this commit separates more formally Reactive Streams bridge code out of the request and response implementations which become simple adapters to the ServerHttpRequest/Response contracts like their RxNetty and Reactor Net counterparts. --- .../reactive/ServletHttpHandlerAdapter.java | 292 ++++++++++++- .../reactive/ServletServerHttpRequest.java | 209 +-------- .../reactive/ServletServerHttpResponse.java | 106 +---- .../reactive/UndertowHttpHandlerAdapter.java | 397 +++++++++++++++++- .../reactive/UndertowServerHttpRequest.java | 218 +--------- .../reactive/UndertowServerHttpResponse.java | 184 +------- 6 files changed, 703 insertions(+), 703 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java index 1ea7e4cd0c..97bf0d7f4b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java @@ -17,8 +17,15 @@ package org.springframework.http.server.reactive; import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicLong; import javax.servlet.AsyncContext; +import javax.servlet.ReadListener; import javax.servlet.ServletException; +import javax.servlet.ServletInputStream; +import javax.servlet.ServletOutputStream; +import javax.servlet.WriteListener; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; @@ -26,10 +33,12 @@ import javax.servlet.http.HttpServletResponse; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import org.springframework.http.HttpStatus; +import org.springframework.util.Assert; /** * @author Arjen Poutsma @@ -38,6 +47,8 @@ import org.springframework.http.HttpStatus; @WebServlet(asyncSupported = true) public class ServletHttpHandlerAdapter extends HttpServlet { + private static final int BUFFER_SIZE = 8192; + private static Log logger = LogFactory.getLog(ServletHttpHandlerAdapter.class); @@ -50,23 +61,288 @@ public class ServletHttpHandlerAdapter extends HttpServlet { @Override - protected void service(HttpServletRequest request, HttpServletResponse response) + protected void service(HttpServletRequest servletRequest, HttpServletResponse servletResponse) throws ServletException, IOException { - AsyncContext context = request.startAsync(); + AsyncContext context = servletRequest.startAsync(); ServletAsyncContextSynchronizer synchronizer = new ServletAsyncContextSynchronizer(context); - ServletServerHttpRequest httpRequest = new ServletServerHttpRequest(request, synchronizer); - request.getInputStream().setReadListener(httpRequest.getReadListener()); + RequestBodyPublisher requestBody = new RequestBodyPublisher(synchronizer, BUFFER_SIZE); + ServletServerHttpRequest request = new ServletServerHttpRequest(servletRequest, requestBody); + servletRequest.getInputStream().setReadListener(requestBody); - ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response, synchronizer); - response.getOutputStream().setWriteListener(httpResponse.getWriteListener()); + ResponseBodySubscriber responseBodySubscriber = new ResponseBodySubscriber(synchronizer); + ServletServerHttpResponse response = new ServletServerHttpResponse(servletResponse, + publisher -> subscriber -> publisher.subscribe(responseBodySubscriber)); + servletResponse.getOutputStream().setWriteListener(responseBodySubscriber); - HandlerResultSubscriber resultSubscriber = new HandlerResultSubscriber(synchronizer, httpResponse); - this.handler.handle(httpRequest, httpResponse).subscribe(resultSubscriber); + HandlerResultSubscriber resultSubscriber = new HandlerResultSubscriber(synchronizer, response); + this.handler.handle(request, response).subscribe(resultSubscriber); } + private static class RequestBodyPublisher implements ReadListener, Publisher { + + private final ServletAsyncContextSynchronizer synchronizer; + + private final byte[] buffer; + + private final DemandCounter demand = new DemandCounter(); + + private Subscriber subscriber; + + private boolean stalled; + + private boolean cancelled; + + + public RequestBodyPublisher(ServletAsyncContextSynchronizer synchronizer, int bufferSize) { + this.synchronizer = synchronizer; + this.buffer = new byte[bufferSize]; + } + + + @Override + public void subscribe(Subscriber subscriber) { + if (subscriber == null) { + throw new NullPointerException(); + } + else if (this.subscriber != null) { + subscriber.onError(new IllegalStateException("Only one subscriber allowed")); + } + this.subscriber = subscriber; + this.subscriber.onSubscribe(new RequestBodySubscription()); + } + + @Override + public void onDataAvailable() throws IOException { + if (cancelled) { + return; + } + ServletInputStream input = this.synchronizer.getInputStream(); + logger.debug("onDataAvailable: " + input); + + while (true) { + logger.debug("Demand: " + this.demand); + + if (!demand.hasDemand()) { + stalled = true; + break; + } + + boolean ready = input.isReady(); + logger.debug("Input ready: " + ready + " finished: " + input.isFinished()); + + if (!ready) { + break; + } + + int read = input.read(buffer); + logger.debug("Input read:" + read); + + if (read == -1) { + break; + } + else if (read > 0) { + this.demand.decrement(); + byte[] copy = Arrays.copyOf(this.buffer, read); + +// logger.debug("Next: " + new String(copy, UTF_8)); + + this.subscriber.onNext(ByteBuffer.wrap(copy)); + + } + } + } + + @Override + public void onAllDataRead() throws IOException { + if (cancelled) { + return; + } + logger.debug("All data read"); + this.synchronizer.readComplete(); + if (this.subscriber != null) { + this.subscriber.onComplete(); + } + } + + @Override + public void onError(Throwable t) { + if (cancelled) { + return; + } + logger.error("RequestBodyPublisher Error", t); + this.synchronizer.readComplete(); + if (this.subscriber != null) { + this.subscriber.onError(t); + } + } + + private class RequestBodySubscription implements Subscription { + + @Override + public void request(long n) { + if (cancelled) { + return; + } + logger.debug("Updating demand " + demand + " by " + n); + + demand.increase(n); + + logger.debug("Stalled: " + stalled); + + if (stalled) { + stalled = false; + try { + onDataAvailable(); + } + catch (IOException ex) { + onError(ex); + } + } + } + + @Override + public void cancel() { + if (cancelled) { + return; + } + cancelled = true; + synchronizer.readComplete(); + demand.reset(); + } + } + + + /** + * Small utility class for keeping track of Reactive Streams demand. + */ + private static final class DemandCounter { + + private final AtomicLong demand = new AtomicLong(); + + /** + * Increases the demand by the given number + * @param n the positive number to increase demand by + * @return the increased demand + * @see org.reactivestreams.Subscription#request(long) + */ + public long increase(long n) { + Assert.isTrue(n > 0, "'n' must be higher than 0"); + return demand.updateAndGet(d -> d != Long.MAX_VALUE ? d + n : Long.MAX_VALUE); + } + + /** + * Decreases the demand by one. + * @return the decremented demand + */ + public long decrement() { + return demand.updateAndGet(d -> d != Long.MAX_VALUE ? d - 1 : Long.MAX_VALUE); + } + + /** + * Indicates whether this counter has demand, i.e. whether it is higher than 0. + * @return {@code true} if this counter has demand; {@code false} otherwise + */ + public boolean hasDemand() { + return this.demand.get() > 0; + } + + /** + * Resets this counter to 0. + * @see org.reactivestreams.Subscription#cancel() + */ + public void reset() { + this.demand.set(0); + } + + @Override + public String toString() { + return demand.toString(); + } + } + } + + private static class ResponseBodySubscriber implements WriteListener, Subscriber { + + private final ServletAsyncContextSynchronizer synchronizer; + + private Subscription subscription; + + private ByteBuffer buffer; + + private volatile boolean subscriberComplete = false; + + + public ResponseBodySubscriber(ServletAsyncContextSynchronizer synchronizer) { + this.synchronizer = synchronizer; + } + + + @Override + public void onSubscribe(Subscription subscription) { + this.subscription = subscription; + this.subscription.request(1); + } + + @Override + public void onNext(ByteBuffer bytes) { + + Assert.isNull(buffer); + + this.buffer = bytes; + try { + onWritePossible(); + } + catch (IOException e) { + onError(e); + } + } + + @Override + public void onComplete() { + logger.debug("Complete buffer: " + (buffer == null)); + + this.subscriberComplete = true; + + if (buffer == null) { + this.synchronizer.writeComplete(); + } + } + + @Override + public void onWritePossible() throws IOException { + ServletOutputStream output = this.synchronizer.getOutputStream(); + + boolean ready = output.isReady(); + logger.debug("Output: " + ready + " buffer: " + (buffer == null)); + + if (ready) { + if (this.buffer != null) { + byte[] bytes = new byte[this.buffer.remaining()]; + this.buffer.get(bytes); + this.buffer = null; + output.write(bytes); + if (!subscriberComplete) { + this.subscription.request(1); + } + else { + this.synchronizer.writeComplete(); + } + } + else { + this.subscription.request(1); + } + } + } + + @Override + public void onError(Throwable t) { + logger.error("ResponseBodySubscriber error", t); + } + } + private static class HandlerResultSubscriber implements Subscriber { private final ServletAsyncContextSynchronizer synchronizer; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java index 477f2b6a86..3f1eb1c69c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java @@ -16,24 +16,15 @@ package org.springframework.http.server.reactive; -import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.nio.ByteBuffer; import java.nio.charset.Charset; -import java.util.Arrays; import java.util.Enumeration; import java.util.Map; -import java.util.concurrent.atomic.AtomicLong; -import javax.servlet.ReadListener; -import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; import org.reactivestreams.Publisher; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -49,24 +40,20 @@ import org.springframework.util.StringUtils; */ public class ServletServerHttpRequest implements ServerHttpRequest { - private static final int BUFFER_SIZE = 8192; - - private static final Log logger = LogFactory.getLog(ServletServerHttpRequest.class); - - private final HttpServletRequest request; private URI uri; private HttpHeaders headers; - private final RequestBodyPublisher requestBodyPublisher; + private final Publisher requestBodyPublisher; - public ServletServerHttpRequest(HttpServletRequest request, ServletAsyncContextSynchronizer synchronizer) { + public ServletServerHttpRequest(HttpServletRequest request, Publisher body) { Assert.notNull(request, "'request' must not be null."); + Assert.notNull(body, "'body' must not be null."); this.request = request; - this.requestBodyPublisher = new RequestBodyPublisher(synchronizer, BUFFER_SIZE); + this.requestBodyPublisher = body; } @@ -143,192 +130,4 @@ public class ServletServerHttpRequest implements ServerHttpRequest { return this.requestBodyPublisher; } - ReadListener getReadListener() { - return this.requestBodyPublisher; - } - - - private static class RequestBodyPublisher implements ReadListener, Publisher { - - private final ServletAsyncContextSynchronizer synchronizer; - - private final byte[] buffer; - - private final DemandCounter demand = new DemandCounter(); - - private Subscriber subscriber; - - private boolean stalled; - - private boolean cancelled; - - - public RequestBodyPublisher(ServletAsyncContextSynchronizer synchronizer, int bufferSize) { - this.synchronizer = synchronizer; - this.buffer = new byte[bufferSize]; - } - - - @Override - public void subscribe(Subscriber subscriber) { - if (subscriber == null) { - throw new NullPointerException(); - } - else if (this.subscriber != null) { - subscriber.onError(new IllegalStateException("Only one subscriber allowed")); - } - this.subscriber = subscriber; - this.subscriber.onSubscribe(new RequestBodySubscription()); - } - - @Override - public void onDataAvailable() throws IOException { - if (cancelled) { - return; - } - ServletInputStream input = this.synchronizer.getInputStream(); - logger.debug("onDataAvailable: " + input); - - while (true) { - logger.debug("Demand: " + this.demand); - - if (!demand.hasDemand()) { - stalled = true; - break; - } - - boolean ready = input.isReady(); - logger.debug("Input ready: " + ready + " finished: " + input.isFinished()); - - if (!ready) { - break; - } - - int read = input.read(buffer); - logger.debug("Input read:" + read); - - if (read == -1) { - break; - } - else if (read > 0) { - this.demand.decrement(); - byte[] copy = Arrays.copyOf(this.buffer, read); - -// logger.debug("Next: " + new String(copy, UTF_8)); - - this.subscriber.onNext(ByteBuffer.wrap(copy)); - - } - } - } - - @Override - public void onAllDataRead() throws IOException { - if (cancelled) { - return; - } - logger.debug("All data read"); - this.synchronizer.readComplete(); - if (this.subscriber != null) { - this.subscriber.onComplete(); - } - } - - @Override - public void onError(Throwable t) { - if (cancelled) { - return; - } - logger.error("RequestBodyPublisher Error", t); - this.synchronizer.readComplete(); - if (this.subscriber != null) { - this.subscriber.onError(t); - } - } - - private class RequestBodySubscription implements Subscription { - - @Override - public void request(long n) { - if (cancelled) { - return; - } - logger.debug("Updating demand " + demand + " by " + n); - - demand.increase(n); - - logger.debug("Stalled: " + stalled); - - if (stalled) { - stalled = false; - try { - onDataAvailable(); - } - catch (IOException ex) { - onError(ex); - } - } - } - - @Override - public void cancel() { - if (cancelled) { - return; - } - cancelled = true; - synchronizer.readComplete(); - demand.reset(); - } - } - - - /** - * Small utility class for keeping track of Reactive Streams demand. - */ - private static final class DemandCounter { - - private final AtomicLong demand = new AtomicLong(); - - /** - * Increases the demand by the given number - * @param n the positive number to increase demand by - * @return the increased demand - * @see org.reactivestreams.Subscription#request(long) - */ - public long increase(long n) { - Assert.isTrue(n > 0, "'n' must be higher than 0"); - return demand.updateAndGet(d -> d != Long.MAX_VALUE ? d + n : Long.MAX_VALUE); - } - - /** - * Decreases the demand by one. - * @return the decremented demand - */ - public long decrement() { - return demand.updateAndGet(d -> d != Long.MAX_VALUE ? d - 1 : Long.MAX_VALUE); - } - - /** - * Indicates whether this counter has demand, i.e. whether it is higher than 0. - * @return {@code true} if this counter has demand; {@code false} otherwise - */ - public boolean hasDemand() { - return this.demand.get() > 0; - } - - /** - * Resets this counter to 0. - * @see org.reactivestreams.Subscription#cancel() - */ - public void reset() { - this.demand.set(0); - } - - @Override - public String toString() { - return demand.toString(); - } - } - } - } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java index 55dfee31a8..19e10d3128 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java @@ -16,18 +16,12 @@ package org.springframework.http.server.reactive; -import java.io.IOException; import java.nio.ByteBuffer; import java.util.List; -import javax.servlet.ServletOutputStream; -import javax.servlet.WriteListener; +import java.util.function.Function; import javax.servlet.http.HttpServletResponse; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; import org.reactivestreams.Publisher; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; import reactor.Publishers; import org.springframework.http.ExtendedHttpHeaders; @@ -42,21 +36,21 @@ import org.springframework.util.Assert; */ public class ServletServerHttpResponse implements ServerHttpResponse { - private static final Log logger = LogFactory.getLog(ServletServerHttpResponse.class); - - private final HttpServletResponse response; + private final Function, Publisher> responseBodyWriter; + private final HttpHeaders headers; - private final ResponseBodySubscriber subscriber; + public ServletServerHttpResponse(HttpServletResponse response, + Function, Publisher> responseBodyWriter) { - public ServletServerHttpResponse(HttpServletResponse response, ServletAsyncContextSynchronizer synchronizer) { Assert.notNull(response, "'response' must not be null"); + Assert.notNull(responseBodyWriter, "'responseBodyWriter' must not be null"); this.response = response; + this.responseBodyWriter = responseBodyWriter; this.headers = new ExtendedHttpHeaders(new ServletHeaderChangeListener()); - this.subscriber = new ResponseBodySubscriber(synchronizer); } @@ -74,17 +68,13 @@ public class ServletServerHttpResponse implements ServerHttpResponse { return this.headers; } - WriteListener getWriteListener() { - return this.subscriber; - } - @Override public Publisher setBody(final Publisher publisher) { return Publishers.lift(publisher, new WriteWithOperator<>(this::setBodyInternal)); } protected Publisher setBodyInternal(Publisher publisher) { - return s -> publisher.subscribe(subscriber); + return this.responseBodyWriter.apply(publisher); } @@ -109,84 +99,4 @@ public class ServletServerHttpResponse implements ServerHttpResponse { } } - - private static class ResponseBodySubscriber implements WriteListener, Subscriber { - - private final ServletAsyncContextSynchronizer synchronizer; - - private Subscription subscription; - - private ByteBuffer buffer; - - private volatile boolean subscriberComplete = false; - - - public ResponseBodySubscriber(ServletAsyncContextSynchronizer synchronizer) { - this.synchronizer = synchronizer; - } - - - @Override - public void onSubscribe(Subscription subscription) { - this.subscription = subscription; - this.subscription.request(1); - } - - @Override - public void onNext(ByteBuffer bytes) { - - Assert.isNull(buffer); - - this.buffer = bytes; - try { - onWritePossible(); - } - catch (IOException e) { - onError(e); - } - } - - @Override - public void onComplete() { - logger.debug("Complete buffer: " + (buffer == null)); - - this.subscriberComplete = true; - - if (buffer == null) { - this.synchronizer.writeComplete(); - } - } - - @Override - public void onWritePossible() throws IOException { - ServletOutputStream output = this.synchronizer.getOutputStream(); - - boolean ready = output.isReady(); - logger.debug("Output: " + ready + " buffer: " + (buffer == null)); - - if (ready) { - if (this.buffer != null) { - byte[] bytes = new byte[this.buffer.remaining()]; - this.buffer.get(bytes); - this.buffer = null; - output.write(bytes); - if (!subscriberComplete) { - this.subscription.request(1); - } - else { - this.synchronizer.writeComplete(); - } - } - else { - this.subscription.request(1); - } - } - } - - @Override - public void onError(Throwable t) { - logger.error("ResponseBodySubscriber error", t); - } - } - } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java index dce2694e75..db61960ef6 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java @@ -16,13 +16,34 @@ package org.springframework.http.server.reactive; -import org.springframework.util.Assert; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLongFieldUpdater; +import io.undertow.connector.PooledByteBuffer; import io.undertow.server.HttpServerExchange; +import io.undertow.util.SameThreadExecutor; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; +import org.xnio.ChannelListener; +import org.xnio.channels.StreamSinkChannel; +import org.xnio.channels.StreamSourceChannel; +import reactor.core.error.SpecificationExceptions; +import reactor.core.subscriber.BaseSubscriber; +import reactor.core.support.BackpressureUtils; + +import org.springframework.util.Assert; + +import static org.xnio.ChannelListeners.closingChannelExceptionHandler; +import static org.xnio.ChannelListeners.flushingChannelListener; +import static org.xnio.IoUtils.safeClose; /** @@ -46,8 +67,12 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle @Override public void handleRequest(HttpServerExchange exchange) throws Exception { - ServerHttpRequest request = new UndertowServerHttpRequest(exchange); - ServerHttpResponse response = new UndertowServerHttpResponse(exchange); + RequestBodyPublisher requestBody = new RequestBodyPublisher(exchange); + ServerHttpRequest request = new UndertowServerHttpRequest(exchange, requestBody); + + ResponseBodySubscriber responseBodySubscriber = new ResponseBodySubscriber(exchange); + ServerHttpResponse response = new UndertowServerHttpResponse(exchange, + publisher -> subscriber -> publisher.subscribe(responseBodySubscriber)); exchange.dispatch(); @@ -81,4 +106,370 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle }); } + + private static class RequestBodyPublisher implements Publisher { + + private static final AtomicLongFieldUpdater DEMAND = + AtomicLongFieldUpdater.newUpdater(RequestBodySubscription.class, "demand"); + + + private final HttpServerExchange exchange; + + private Subscriber subscriber; + + + public RequestBodyPublisher(HttpServerExchange exchange) { + this.exchange = exchange; + } + + + @Override + public void subscribe(Subscriber subscriber) { + if (subscriber == null) { + throw SpecificationExceptions.spec_2_13_exception(); + } + if (this.subscriber != null) { + subscriber.onError(new IllegalStateException("Only one subscriber allowed")); + } + + this.subscriber = subscriber; + this.subscriber.onSubscribe(new RequestBodySubscription()); + } + + + private class RequestBodySubscription implements Subscription, Runnable, + ChannelListener { + + volatile long demand; + + private PooledByteBuffer pooledBuffer; + + private StreamSourceChannel channel; + + private boolean subscriptionClosed; + + private boolean draining; + + + @Override + public void request(long n) { + BackpressureUtils.checkRequest(n, subscriber); + if (this.subscriptionClosed) { + return; + } + BackpressureUtils.getAndAdd(DEMAND, this, n); + scheduleNextMessage(); + } + + private void scheduleNextMessage() { + exchange.dispatch(exchange.isInIoThread() ? SameThreadExecutor.INSTANCE : + exchange.getIoThread(), this); + } + + @Override + public void cancel() { + this.subscriptionClosed = true; + close(); + } + + private void close() { + if (this.pooledBuffer != null) { + safeClose(this.pooledBuffer); + this.pooledBuffer = null; + } + if (this.channel != null) { + safeClose(this.channel); + this.channel = null; + } + } + + @Override + public void run() { + if (this.subscriptionClosed || this.draining) { + return; + } + if (0 == BackpressureUtils.getAndSub(DEMAND, this, 1)) { + return; + } + + this.draining = true; + + if (this.channel == null) { + this.channel = exchange.getRequestChannel(); + + if (this.channel == null) { + if (exchange.isRequestComplete()) { + return; + } + else { + throw new IllegalStateException("Failed to acquire channel!"); + } + } + } + if (this.pooledBuffer == null) { + this.pooledBuffer = exchange.getConnection().getByteBufferPool().allocate(); + } + else { + this.pooledBuffer.getBuffer().clear(); + } + + try { + ByteBuffer buffer = this.pooledBuffer.getBuffer(); + int count; + do { + count = this.channel.read(buffer); + if (count == 0) { + this.channel.getReadSetter().set(this); + this.channel.resumeReads(); + } + else if (count == -1) { + if (buffer.position() > 0) { + doOnNext(buffer); + } + doOnComplete(); + } + else { + if (buffer.remaining() == 0) { + if (this.demand == 0) { + this.channel.suspendReads(); + } + doOnNext(buffer); + if (this.demand > 0) { + scheduleNextMessage(); + } + break; + } + } + } while (count > 0); + } + catch (IOException e) { + doOnError(e); + } + } + + private void doOnNext(ByteBuffer buffer) { + this.draining = false; + buffer.flip(); + subscriber.onNext(buffer); + } + + private void doOnComplete() { + this.subscriptionClosed = true; + try { + subscriber.onComplete(); + } + finally { + close(); + } + } + + private void doOnError(Throwable t) { + this.subscriptionClosed = true; + try { + subscriber.onError(t); + } + finally { + close(); + } + } + + @Override + public void handleEvent(StreamSourceChannel channel) { + if (this.subscriptionClosed) { + return; + } + + try { + ByteBuffer buffer = this.pooledBuffer.getBuffer(); + int count; + do { + count = channel.read(buffer); + if (count == 0) { + return; + } + else if (count == -1) { + if (buffer.position() > 0) { + doOnNext(buffer); + } + doOnComplete(); + } + else { + if (buffer.remaining() == 0) { + if (this.demand == 0) { + channel.suspendReads(); + } + doOnNext(buffer); + if (this.demand > 0) { + scheduleNextMessage(); + } + break; + } + } + } while (count > 0); + } + catch (IOException e) { + doOnError(e); + } + } + } + } + + private static class ResponseBodySubscriber extends BaseSubscriber + implements ChannelListener { + + private final HttpServerExchange exchange; + + private Subscription subscription; + + private final Queue buffers = new ConcurrentLinkedQueue<>(); + + private final AtomicInteger writing = new AtomicInteger(); + + private final AtomicBoolean closing = new AtomicBoolean(); + + private StreamSinkChannel responseChannel; + + + public ResponseBodySubscriber(HttpServerExchange exchange) { + this.exchange = exchange; + } + + @Override + public void onSubscribe(Subscription subscription) { + super.onSubscribe(subscription); + this.subscription = subscription; + this.subscription.request(1); + } + + @Override + public void onNext(ByteBuffer buffer) { + super.onNext(buffer); + + if (this.responseChannel == null) { + this.responseChannel = exchange.getResponseChannel(); + } + + this.writing.incrementAndGet(); + try { + int c; + do { + c = this.responseChannel.write(buffer); + } while (buffer.hasRemaining() && c > 0); + + if (buffer.hasRemaining()) { + this.writing.incrementAndGet(); + enqueue(buffer); + this.responseChannel.getWriteSetter().set(this); + this.responseChannel.resumeWrites(); + } + else { + this.subscription.request(1); + } + + } + catch (IOException ex) { + onError(ex); + } + finally { + this.writing.decrementAndGet(); + if (this.closing.get()) { + closeIfDone(); + } + } + } + + private void enqueue(ByteBuffer src) { + do { + PooledByteBuffer buffer = exchange.getConnection().getByteBufferPool().allocate(); + ByteBuffer dst = buffer.getBuffer(); + copy(dst, src); + dst.flip(); + this.buffers.add(buffer); + } while (src.remaining() > 0); + } + + private void copy(ByteBuffer dst, ByteBuffer src) { + int n = Math.min(dst.capacity(), src.remaining()); + for (int i = 0; i < n; i++) { + dst.put(src.get()); + } + } + + @Override + public void handleEvent(StreamSinkChannel channel) { + try { + int c; + do { + ByteBuffer buffer = this.buffers.peek().getBuffer(); + do { + c = channel.write(buffer); + } while (buffer.hasRemaining() && c > 0); + + if (!buffer.hasRemaining()) { + safeClose(this.buffers.remove()); + } + } while (!this.buffers.isEmpty() && c > 0); + + if (!this.buffers.isEmpty()) { + channel.resumeWrites(); + } + else { + this.writing.decrementAndGet(); + + if (this.closing.get()) { + closeIfDone(); + } + else { + this.subscription.request(1); + } + } + } + catch (IOException ex) { + onError(ex); + } + } + + @Override + public void onError(Throwable ex) { + super.onError(ex); + logger.error("ResponseBodySubscriber error", ex); + if (!exchange.isResponseStarted() && exchange.getStatusCode() < 500) { + exchange.setStatusCode(500); + } + } + + @Override + public void onComplete() { + super.onComplete(); + if (this.responseChannel != null) { + this.closing.set(true); + closeIfDone(); + } + } + + private void closeIfDone() { + if (this.writing.get() == 0) { + if (this.closing.compareAndSet(true, false)) { + closeChannel(); + } + } + } + + private void closeChannel() { + try { + this.responseChannel.shutdownWrites(); + + if (!this.responseChannel.flush()) { + this.responseChannel.getWriteSetter().set(flushingChannelListener( + o -> safeClose(this.responseChannel), closingChannelExceptionHandler())); + this.responseChannel.resumeWrites(); + } + this.responseChannel = null; + } + catch (IOException ex) { + onError(ex); + } + } + } + } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java index e8133f8bc8..9b710945fd 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java @@ -16,30 +16,18 @@ package org.springframework.http.server.reactive; -import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.nio.ByteBuffer; -import java.util.concurrent.atomic.AtomicLongFieldUpdater; -import io.undertow.connector.PooledByteBuffer; import io.undertow.server.HttpServerExchange; import io.undertow.util.HeaderValues; -import io.undertow.util.SameThreadExecutor; import org.reactivestreams.Publisher; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; -import org.xnio.ChannelListener; -import org.xnio.channels.StreamSourceChannel; -import reactor.core.error.SpecificationExceptions; -import reactor.core.support.BackpressureUtils; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.util.Assert; -import static org.xnio.IoUtils.safeClose; - /** * Adapt {@link ServerHttpRequest} to the Underow {@link HttpServerExchange}. * @@ -54,12 +42,14 @@ public class UndertowServerHttpRequest implements ServerHttpRequest { private HttpHeaders headers; - private final Publisher body = new RequestBodyPublisher(); + private final Publisher body; - public UndertowServerHttpRequest(HttpServerExchange exchange) { + public UndertowServerHttpRequest(HttpServerExchange exchange, Publisher body) { Assert.notNull(exchange, "'exchange' is required."); + Assert.notNull(exchange, "'body' is required."); this.exchange = exchange; + this.body = body; } @@ -105,204 +95,4 @@ public class UndertowServerHttpRequest implements ServerHttpRequest { return this.body; } - - private static final AtomicLongFieldUpdater DEMAND = - AtomicLongFieldUpdater.newUpdater(RequestBodyPublisher.RequestBodySubscription.class, "demand"); - - private class RequestBodyPublisher implements Publisher { - - private Subscriber subscriber; - - - @Override - public void subscribe(Subscriber subscriber) { - if (subscriber == null) { - throw SpecificationExceptions.spec_2_13_exception(); - } - if (this.subscriber != null) { - subscriber.onError(new IllegalStateException("Only one subscriber allowed")); - } - - this.subscriber = subscriber; - this.subscriber.onSubscribe(new RequestBodySubscription()); - } - - - private class RequestBodySubscription implements Subscription, Runnable, - ChannelListener { - - volatile long demand; - - private PooledByteBuffer pooledBuffer; - - private StreamSourceChannel channel; - - private boolean subscriptionClosed; - - private boolean draining; - - - @Override - public void request(long n) { - BackpressureUtils.checkRequest(n, subscriber); - if (this.subscriptionClosed) { - return; - } - BackpressureUtils.getAndAdd(DEMAND, this, n); - scheduleNextMessage(); - } - - private void scheduleNextMessage() { - exchange.dispatch(exchange.isInIoThread() ? SameThreadExecutor.INSTANCE : - exchange.getIoThread(), this); - } - - @Override - public void cancel() { - this.subscriptionClosed = true; - close(); - } - - private void close() { - if (this.pooledBuffer != null) { - safeClose(this.pooledBuffer); - this.pooledBuffer = null; - } - if (this.channel != null) { - safeClose(this.channel); - this.channel = null; - } - } - - @Override - public void run() { - if (this.subscriptionClosed || this.draining) { - return; - } - if (0 == BackpressureUtils.getAndSub(DEMAND, this, 1)) { - return; - } - - this.draining = true; - - if (this.channel == null) { - this.channel = exchange.getRequestChannel(); - - if (this.channel == null) { - if (exchange.isRequestComplete()) { - return; - } - else { - throw new IllegalStateException("Failed to acquire channel!"); - } - } - } - if (this.pooledBuffer == null) { - this.pooledBuffer = exchange.getConnection().getByteBufferPool().allocate(); - } - else { - this.pooledBuffer.getBuffer().clear(); - } - - try { - ByteBuffer buffer = this.pooledBuffer.getBuffer(); - int count; - do { - count = this.channel.read(buffer); - if (count == 0) { - this.channel.getReadSetter().set(this); - this.channel.resumeReads(); - } - else if (count == -1) { - if (buffer.position() > 0) { - doOnNext(buffer); - } - doOnComplete(); - } - else { - if (buffer.remaining() == 0) { - if (this.demand == 0) { - this.channel.suspendReads(); - } - doOnNext(buffer); - if (this.demand > 0) { - scheduleNextMessage(); - } - break; - } - } - } while (count > 0); - } - catch (IOException e) { - doOnError(e); - } - } - - private void doOnNext(ByteBuffer buffer) { - this.draining = false; - buffer.flip(); - subscriber.onNext(buffer); - } - - private void doOnComplete() { - this.subscriptionClosed = true; - try { - subscriber.onComplete(); - } - finally { - close(); - } - } - - private void doOnError(Throwable t) { - this.subscriptionClosed = true; - try { - subscriber.onError(t); - } - finally { - close(); - } - } - - @Override - public void handleEvent(StreamSourceChannel channel) { - if (this.subscriptionClosed) { - return; - } - - try { - ByteBuffer buffer = this.pooledBuffer.getBuffer(); - int count; - do { - count = channel.read(buffer); - if (count == 0) { - return; - } - else if (count == -1) { - if (buffer.position() > 0) { - doOnNext(buffer); - } - doOnComplete(); - } - else { - if (buffer.remaining() == 0) { - if (this.demand == 0) { - channel.suspendReads(); - } - doOnNext(buffer); - if (this.demand > 0) { - scheduleNextMessage(); - } - break; - } - } - } while (count > 0); - } - catch (IOException e) { - doOnError(e); - } - } - } - } - } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java index 316257bf8b..d13b80eda2 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java @@ -16,35 +16,20 @@ package org.springframework.http.server.reactive; -import java.io.IOException; import java.nio.ByteBuffer; import java.util.List; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; -import io.undertow.connector.PooledByteBuffer; import io.undertow.server.HttpServerExchange; import io.undertow.util.HttpString; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; import org.reactivestreams.Publisher; -import org.reactivestreams.Subscription; -import org.xnio.ChannelListener; -import org.xnio.channels.StreamSinkChannel; import reactor.Publishers; -import reactor.core.subscriber.BaseSubscriber; import org.springframework.http.ExtendedHttpHeaders; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.util.Assert; -import static org.xnio.ChannelListeners.closingChannelExceptionHandler; -import static org.xnio.ChannelListeners.flushingChannelListener; -import static org.xnio.IoUtils.safeClose; - /** * Adapt {@link ServerHttpResponse} to the Undertow {@link HttpServerExchange}. * @@ -53,19 +38,20 @@ import static org.xnio.IoUtils.safeClose; */ public class UndertowServerHttpResponse implements ServerHttpResponse { - private static final Log logger = LogFactory.getLog(UndertowServerHttpResponse.class); - - private final HttpServerExchange exchange; + private final Function, Publisher> responseBodyWriter; + private final HttpHeaders headers; - private final ResponseBodySubscriber bodySubscriber = new ResponseBodySubscriber(); + public UndertowServerHttpResponse(HttpServerExchange exchange, + Function, Publisher> responseBodyWriter) { - public UndertowServerHttpResponse(HttpServerExchange exchange) { Assert.notNull(exchange, "'exchange' is required."); + Assert.notNull(responseBodyWriter, "'responseBodyWriter' must not be null"); this.exchange = exchange; + this.responseBodyWriter = responseBodyWriter; this.headers = new ExtendedHttpHeaders(new UndertowHeaderChangeListener()); } @@ -90,8 +76,8 @@ public class UndertowServerHttpResponse implements ServerHttpResponse { return Publishers.lift(publisher, new WriteWithOperator<>(this::setBodyInternal)); } - protected Publisher setBodyInternal(Publisher writePublisher) { - return subscriber -> writePublisher.subscribe(bodySubscriber); + protected Publisher setBodyInternal(Publisher publisher) { + return this.responseBodyWriter.apply(publisher); } @@ -115,156 +101,4 @@ public class UndertowServerHttpResponse implements ServerHttpResponse { } } - private class ResponseBodySubscriber extends BaseSubscriber - implements ChannelListener { - - private Subscription subscription; - - private final Queue buffers = new ConcurrentLinkedQueue<>(); - - private final AtomicInteger writing = new AtomicInteger(); - - private final AtomicBoolean closing = new AtomicBoolean(); - - private StreamSinkChannel responseChannel; - - - @Override - public void onSubscribe(Subscription subscription) { - super.onSubscribe(subscription); - this.subscription = subscription; - this.subscription.request(1); - } - - @Override - public void onNext(ByteBuffer buffer) { - super.onNext(buffer); - - if (this.responseChannel == null) { - this.responseChannel = exchange.getResponseChannel(); - } - - this.writing.incrementAndGet(); - try { - int c; - do { - c = this.responseChannel.write(buffer); - } while (buffer.hasRemaining() && c > 0); - - if (buffer.hasRemaining()) { - this.writing.incrementAndGet(); - enqueue(buffer); - this.responseChannel.getWriteSetter().set(this); - this.responseChannel.resumeWrites(); - } - else { - this.subscription.request(1); - } - - } - catch (IOException ex) { - onError(ex); - } - finally { - this.writing.decrementAndGet(); - if (this.closing.get()) { - closeIfDone(); - } - } - } - - private void enqueue(ByteBuffer src) { - do { - PooledByteBuffer buffer = exchange.getConnection().getByteBufferPool().allocate(); - ByteBuffer dst = buffer.getBuffer(); - copy(dst, src); - dst.flip(); - this.buffers.add(buffer); - } while (src.remaining() > 0); - } - - private void copy(ByteBuffer dst, ByteBuffer src) { - int n = Math.min(dst.capacity(), src.remaining()); - for (int i = 0; i < n; i++) { - dst.put(src.get()); - } - } - - @Override - public void handleEvent(StreamSinkChannel channel) { - try { - int c; - do { - ByteBuffer buffer = this.buffers.peek().getBuffer(); - do { - c = channel.write(buffer); - } while (buffer.hasRemaining() && c > 0); - - if (!buffer.hasRemaining()) { - safeClose(this.buffers.remove()); - } - } while (!this.buffers.isEmpty() && c > 0); - - if (!this.buffers.isEmpty()) { - channel.resumeWrites(); - } - else { - this.writing.decrementAndGet(); - - if (this.closing.get()) { - closeIfDone(); - } - else { - this.subscription.request(1); - } - } - } - catch (IOException ex) { - onError(ex); - } - } - - @Override - public void onError(Throwable ex) { - super.onError(ex); - logger.error("ResponseBodySubscriber error", ex); - if (!exchange.isResponseStarted() && exchange.getStatusCode() < 500) { - exchange.setStatusCode(500); - } - } - - @Override - public void onComplete() { - super.onComplete(); - if (this.responseChannel != null) { - this.closing.set(true); - closeIfDone(); - } - } - - private void closeIfDone() { - if (this.writing.get() == 0) { - if (this.closing.compareAndSet(true, false)) { - closeChannel(); - } - } - } - - private void closeChannel() { - try { - this.responseChannel.shutdownWrites(); - - if (!this.responseChannel.flush()) { - this.responseChannel.getWriteSetter().set(flushingChannelListener( - o -> safeClose(this.responseChannel), closingChannelExceptionHandler())); - this.responseChannel.resumeWrites(); - } - this.responseChannel = null; - } - catch (IOException ex) { - onError(ex); - } - } - } - } From 8ef2ce44f4e0e1bed2e076fc72e42058007f892b Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 31 Dec 2015 13:18:13 -0500 Subject: [PATCH 138/498] Temporarily remove Reactor from integration tests https://github.com/spring-projects/spring-reactive/issues/57 --- .../server/reactive/AbstractHttpHandlerIntegrationTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AbstractHttpHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AbstractHttpHandlerIntegrationTests.java index 83781e29f9..5b4b4da4be 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AbstractHttpHandlerIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AbstractHttpHandlerIntegrationTests.java @@ -44,7 +44,7 @@ public abstract class AbstractHttpHandlerIntegrationTests { return new Object[][] { {new JettyHttpServer()}, {new RxNettyHttpServer()}, - {new ReactorHttpServer()}, +// {new ReactorHttpServer()}, {new TomcatHttpServer()}, {new UndertowHttpServer()} }; From 8ef7e2ff77f913773e30b1463d4657368db700e5 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Wed, 16 Dec 2015 18:05:37 +0100 Subject: [PATCH 139/498] Use Reactor 2.5 Flux and Mono Rx light API Flux and Mono are used both for implementation and exposed at API level to express 1 versus N semantic and to provide default Rx operators: - Flux for multiple values Publisher (issue #48) - Mono for single value Publisher (issue #50) - Mono for Publisher with no value (issue #49) --- spring-web-reactive/build.gradle | 4 +- .../springframework/core/codec/Decoder.java | 3 +- .../springframework/core/codec/Encoder.java | 3 +- .../core/codec/support/ByteBufferDecoder.java | 5 +- .../core/codec/support/ByteBufferEncoder.java | 5 +- .../codec/support/JacksonJsonDecoder.java | 9 ++- .../codec/support/JacksonJsonEncoder.java | 6 +- .../core/codec/support/Jaxb2Decoder.java | 12 +-- .../core/codec/support/Jaxb2Encoder.java | 6 +- .../core/codec/support/JsonObjectDecoder.java | 14 ++-- .../core/codec/support/JsonObjectEncoder.java | 7 +- .../core/codec/support/StringDecoder.java | 6 +- .../core/codec/support/StringEncoder.java | 6 +- ...ctiveStreamsToReactorStreamConverter.java} | 6 +- .../http/ReactiveHttpInputMessage.java | 3 +- .../http/ReactiveHttpOutputMessage.java | 3 +- .../reactive/ErrorHandlingHttpHandler.java | 15 ++-- .../reactive/FilterChainHttpHandler.java | 7 +- .../server/reactive/HttpExceptionHandler.java | 4 +- .../http/server/reactive/HttpFilter.java | 3 +- .../http/server/reactive/HttpFilterChain.java | 5 +- .../http/server/reactive/HttpHandler.java | 4 +- .../server/reactive/HttpHandlerDecorator.java | 4 +- .../InternalServerErrorExceptionHandler.java | 7 +- .../reactive/ReactorHttpHandlerAdapter.java | 7 +- .../reactive/ReactorServerHttpRequest.java | 7 +- .../reactive/ReactorServerHttpResponse.java | 11 +-- .../reactive/RxNettyServerHttpRequest.java | 3 +- .../reactive/RxNettyServerHttpResponse.java | 11 +-- .../reactive/ServletHttpHandlerAdapter.java | 3 +- .../reactive/ServletServerHttpRequest.java | 7 +- .../reactive/ServletServerHttpResponse.java | 13 ++-- .../reactive/UndertowHttpHandlerAdapter.java | 7 +- .../reactive/UndertowServerHttpRequest.java | 7 +- .../reactive/UndertowServerHttpResponse.java | 13 ++-- .../util/ByteBufferPublisherInputStream.java | 4 +- .../web/reactive/DispatcherHandler.java | 75 +++++-------------- .../web/reactive/HandlerAdapter.java | 3 +- .../web/reactive/HandlerMapping.java | 6 +- .../web/reactive/HandlerResult.java | 10 +-- .../web/reactive/HandlerResultHandler.java | 8 +- .../ResponseStatusExceptionHandler.java | 9 +-- .../reactive/handler/HttpHandlerAdapter.java | 8 +- .../handler/SimpleHandlerResultHandler.java | 10 +-- .../handler/SimpleUrlHandlerMapping.java | 10 +-- .../method/HandlerMethodArgumentResolver.java | 4 +- .../method/InvocableHandlerMethod.java | 53 +++++-------- .../RequestBodyArgumentResolver.java | 13 ++-- .../RequestMappingHandlerAdapter.java | 22 +++--- .../RequestMappingHandlerMapping.java | 10 +-- .../RequestParamArgumentResolver.java | 10 +-- .../annotation/ResponseBodyResultHandler.java | 12 +-- .../http/server/reactive/EchoHandler.java | 8 +- .../ErrorHandlingHttpHandlerTests.java | 22 +++--- .../reactive/FilterChainHttpHandlerTests.java | 38 ++++------ .../reactive/MockServerHttpRequest.java | 9 ++- .../reactive/MockServerHttpResponse.java | 7 +- .../http/server/reactive/RandomHandler.java | 7 +- .../reactive/WriteWithOperatorTests.java | 26 +++---- .../http/server/reactive/XmlHandler.java | 4 +- .../codec/decoder/ByteBufferDecoderTests.java | 4 +- .../decoder/JacksonJsonDecoderTests.java | 4 +- .../codec/decoder/Jaxb2DecoderTests.java | 4 +- .../codec/decoder/JsonObjectDecoderTests.java | 16 ++-- .../codec/decoder/StringDecoderTests.java | 4 +- .../codec/encoder/ByteBufferEncoderTests.java | 4 +- .../encoder/JacksonJsonEncoderTests.java | 4 +- .../codec/encoder/Jaxb2EncoderTests.java | 4 +- .../codec/encoder/JsonObjectEncoderTests.java | 12 +-- .../codec/encoder/StringEncoderTests.java | 4 +- .../reactive/DispatcherHandlerErrorTests.java | 25 ++++--- .../ResponseStatusExceptionHandlerTests.java | 4 +- .../SimpleHandlerResultHandlerTests.java | 4 +- ...mpleUrlHandlerMappingIntegrationTests.java | 11 ++- .../method/InvocableHandlerMethodTests.java | 24 +++--- .../RequestMappingHandlerMappingTests.java | 3 +- .../RequestMappingIntegrationTests.java | 23 +++--- 77 files changed, 354 insertions(+), 424 deletions(-) rename spring-web-reactive/src/main/java/org/springframework/core/convert/support/{ReactiveStreamsToReactorConverter.java => ReactiveStreamsToReactorStreamConverter.java} (91%) diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index 99c780e6d6..117cab0e2a 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -20,9 +20,9 @@ group = 'org.springframework.reactive' repositories { mavenCentral() - mavenLocal() maven { url 'https://oss.jfrog.org/libs-snapshot' } // RxNetty 0.5.x snapshots maven { url 'http://repo.spring.io/snapshot' } // Reactor snapshot + mavenLocal() } configurations.all { @@ -32,7 +32,7 @@ configurations.all { ext { springVersion = '4.2.3.RELEASE' - reactorVersion = '2.1.0.BUILD-SNAPSHOT' + reactorVersion = '2.5.0.BUILD-SNAPSHOT' tomcatVersion = '8.0.28' jettyVersion = '9.3.5.v20151012' } diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/Decoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/Decoder.java index b786547efc..f78397dba8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/Decoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/Decoder.java @@ -20,6 +20,7 @@ import java.nio.ByteBuffer; import java.util.List; import org.reactivestreams.Publisher; +import reactor.Flux; import org.springframework.core.ResolvableType; import org.springframework.util.MimeType; @@ -49,7 +50,7 @@ public interface Decoder { * @param hints Additional information about how to do decode, optional. * @return the output stream */ - Publisher decode(Publisher inputStream, ResolvableType type, + Flux decode(Publisher inputStream, ResolvableType type, MimeType mimeType, Object... hints); /** diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/Encoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/Encoder.java index 72e9a38f5d..bee2f78ed6 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/Encoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/Encoder.java @@ -20,6 +20,7 @@ import java.nio.ByteBuffer; import java.util.List; import org.reactivestreams.Publisher; +import reactor.Flux; import org.springframework.core.ResolvableType; import org.springframework.util.MimeType; @@ -49,7 +50,7 @@ public interface Encoder { * @param hints Additional information about how to do decode, optional. * @return the output stream */ - Publisher encode(Publisher inputStream, ResolvableType type, + Flux encode(Publisher inputStream, ResolvableType type, MimeType mimeType, Object... hints); /** diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferDecoder.java index 54b78143ee..e24b722fe4 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferDecoder.java @@ -19,6 +19,7 @@ package org.springframework.core.codec.support; import java.nio.ByteBuffer; import org.reactivestreams.Publisher; +import reactor.Flux; import org.springframework.core.ResolvableType; import org.springframework.util.MimeType; @@ -42,10 +43,10 @@ public class ByteBufferDecoder extends AbstractDecoder { } @Override - public Publisher decode(Publisher inputStream, ResolvableType type, + public Flux decode(Publisher inputStream, ResolvableType type, MimeType mimeType, Object... hints) { - return inputStream; + return Flux.from(inputStream); } } \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferEncoder.java index fad1fc8d28..71a2986581 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferEncoder.java @@ -19,6 +19,7 @@ package org.springframework.core.codec.support; import java.nio.ByteBuffer; import org.reactivestreams.Publisher; +import reactor.Flux; import org.springframework.core.ResolvableType; import org.springframework.util.MimeType; @@ -42,11 +43,11 @@ public class ByteBufferEncoder extends AbstractEncoder { } @Override - public Publisher encode(Publisher inputStream, ResolvableType type, + public Flux encode(Publisher inputStream, ResolvableType type, MimeType mimeType, Object... hints) { //noinspection unchecked - return (Publisher) inputStream; + return Flux.from((Publisher)inputStream); } } \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonDecoder.java index 3280831ee1..5042480450 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonDecoder.java @@ -23,7 +23,7 @@ import java.nio.charset.StandardCharsets; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectReader; import org.reactivestreams.Publisher; -import reactor.Publishers; +import reactor.Flux; import org.springframework.core.ResolvableType; import org.springframework.core.codec.CodecException; @@ -62,16 +62,17 @@ public class JacksonJsonDecoder extends AbstractDecoder { @Override - public Publisher decode(Publisher inputStream, ResolvableType type, + public Flux decode(Publisher inputStream, ResolvableType type, MimeType mimeType, Object... hints) { ObjectReader reader = this.mapper.readerFor(type.getRawClass()); + Flux stream = Flux.from(inputStream); if (this.preProcessor != null) { - inputStream = this.preProcessor.decode(inputStream, type, mimeType, hints); + stream = this.preProcessor.decode(inputStream, type, mimeType, hints); } - return Publishers.map(inputStream, content -> { + return stream.map(content -> { try { return reader.readValue(new ByteBufferInputStream(content)); } diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonEncoder.java index 5b656935f5..d1f78ee5d5 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonEncoder.java @@ -22,7 +22,7 @@ import java.nio.charset.StandardCharsets; import com.fasterxml.jackson.databind.ObjectMapper; import org.reactivestreams.Publisher; -import reactor.Publishers; +import reactor.Flux; import reactor.io.buffer.Buffer; import org.springframework.core.ResolvableType; @@ -61,10 +61,10 @@ public class JacksonJsonEncoder extends AbstractEncoder { } @Override - public Publisher encode(Publisher inputStream, + public Flux encode(Publisher inputStream, ResolvableType type, MimeType mimeType, Object... hints) { - Publisher stream = Publishers.map(inputStream, value -> { + Flux stream = Flux.from(inputStream).map(value -> { Buffer buffer = new Buffer(); BufferOutputStream outputStream = new BufferOutputStream(buffer); try { diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Decoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Decoder.java index 12c2973dd8..337cb24512 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Decoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Decoder.java @@ -34,7 +34,7 @@ import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.XMLReader; import org.xml.sax.helpers.XMLReaderFactory; -import reactor.Publishers; +import reactor.Flux; import org.springframework.core.ResolvableType; import org.springframework.core.codec.CodecException; @@ -60,7 +60,7 @@ public class Jaxb2Decoder extends AbstractDecoder { @Override - public Publisher decode(Publisher inputStream, ResolvableType type, + public Flux decode(Publisher inputStream, ResolvableType type, MimeType mimeType, Object... hints) { Class outputClass = type.getRawClass(); @@ -68,19 +68,19 @@ public class Jaxb2Decoder extends AbstractDecoder { Source source = processSource(new StreamSource(new ByteBufferPublisherInputStream(inputStream))); Unmarshaller unmarshaller = createUnmarshaller(outputClass); if (outputClass.isAnnotationPresent(XmlRootElement.class)) { - return Publishers.just(unmarshaller.unmarshal(source)); + return Flux.just(unmarshaller.unmarshal(source)); } else { JAXBElement jaxbElement = unmarshaller.unmarshal(source, outputClass); - return Publishers.just(jaxbElement.getValue()); + return Flux.just(jaxbElement.getValue()); } } catch (UnmarshalException ex) { - return Publishers.error( + return Flux.error( new CodecException("Could not unmarshal to [" + outputClass + "]: " + ex.getMessage(), ex)); } catch (JAXBException ex) { - return Publishers.error(new CodecException("Could not instantiate JAXBContext: " + + return Flux.error(new CodecException("Could not instantiate JAXBContext: " + ex.getMessage(), ex)); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Encoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Encoder.java index a3a64e56ae..f518d9f5e2 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Encoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Encoder.java @@ -26,7 +26,7 @@ import javax.xml.bind.MarshalException; import javax.xml.bind.Marshaller; import org.reactivestreams.Publisher; -import reactor.Publishers; +import reactor.Flux; import reactor.io.buffer.Buffer; import org.springframework.core.ResolvableType; @@ -54,10 +54,10 @@ public class Jaxb2Encoder extends AbstractEncoder { @Override - public Publisher encode(Publisher messageStream, ResolvableType type, + public Flux encode(Publisher messageStream, ResolvableType type, MimeType mimeType, Object... hints) { - return Publishers.map(messageStream, value -> { + return Flux.from(messageStream).map(value -> { try { Buffer buffer = new Buffer(); BufferOutputStream outputStream = new BufferOutputStream(buffer); diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectDecoder.java index 95889640b4..59598d519e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectDecoder.java @@ -25,7 +25,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; import org.reactivestreams.Publisher; -import reactor.Publishers; +import reactor.Flux; import reactor.fn.Function; import org.springframework.core.ResolvableType; @@ -95,10 +95,10 @@ public class JsonObjectDecoder extends AbstractDecoder { } @Override - public Publisher decode(Publisher inputStream, ResolvableType type, + public Flux decode(Publisher inputStream, ResolvableType type, MimeType mimeType, Object... hints) { - return Publishers.flatMap(inputStream, new Function>() { + return Flux.from(inputStream).flatMap(new Function>() { int openBraces; int index; @@ -120,13 +120,13 @@ public class JsonObjectDecoder extends AbstractDecoder { } if (this.state == ST_CORRUPTED) { this.input.skipBytes(this.input.readableBytes()); - return Publishers.error(new IllegalStateException("Corrupted stream")); + return Flux.error(new IllegalStateException("Corrupted stream")); } if (this.writerIndex > maxObjectLength) { // buffer size exceeded maxObjectLength; discarding the complete buffer. this.input.skipBytes(this.input.readableBytes()); reset(); - return Publishers.error(new IllegalStateException("object length exceeds " + + return Flux.error(new IllegalStateException("object length exceeds " + maxObjectLength + ": " + this.writerIndex + " bytes discarded")); } for (/* use current index */; this.index < this.writerIndex; this.index++) { @@ -199,7 +199,7 @@ public class JsonObjectDecoder extends AbstractDecoder { } else { this.state = ST_CORRUPTED; - return Publishers.error(new IllegalStateException( + return Flux.error(new IllegalStateException( "invalid JSON received at byte position " + this.index + ": " + ByteBufUtil.hexDump(this.input))); } @@ -208,7 +208,7 @@ public class JsonObjectDecoder extends AbstractDecoder { if (this.input.readableBytes() == 0) { this.index = 0; } - return Publishers.from(chunks); + return Flux.fromIterable(chunks); } /** diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectEncoder.java index ffe5a85d8c..d00ef799a9 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectEncoder.java @@ -23,6 +23,7 @@ import java.util.concurrent.atomic.AtomicLongFieldUpdater; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; +import reactor.Flux; import reactor.core.subscriber.SubscriberBarrier; import reactor.core.support.BackpressureUtils; import reactor.io.buffer.Buffer; @@ -30,8 +31,6 @@ import reactor.io.buffer.Buffer; import org.springframework.core.ResolvableType; import org.springframework.util.MimeType; -import static reactor.Publishers.lift; - /** * Encode a byte stream of individual JSON element to a byte stream representing * a single JSON array when if it contains more than one element. @@ -49,11 +48,11 @@ public class JsonObjectEncoder extends AbstractEncoder { } @Override - public Publisher encode(Publisher messageStream, + public Flux encode(Publisher messageStream, ResolvableType type, MimeType mimeType, Object... hints) { //noinspection Convert2MethodRef - return lift(messageStream, bbs -> new JsonEncoderBarrier(bbs)); + return Flux.from(messageStream).lift(bbs -> new JsonEncoderBarrier(bbs)); } diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java index c2b20fd8b4..5205a1e0d0 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java @@ -21,7 +21,7 @@ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import org.reactivestreams.Publisher; -import reactor.Publishers; +import reactor.Flux; import reactor.io.buffer.Buffer; import org.springframework.core.ResolvableType; @@ -48,7 +48,7 @@ public class StringDecoder extends AbstractDecoder { } @Override - public Publisher decode(Publisher inputStream, ResolvableType type, + public Flux decode(Publisher inputStream, ResolvableType type, MimeType mimeType, Object... hints) { Charset charset; @@ -58,7 +58,7 @@ public class StringDecoder extends AbstractDecoder { else { charset = DEFAULT_CHARSET; } - return Publishers.map(inputStream, content -> new String(new Buffer(content).asBytes(), charset)); + return Flux.from(inputStream).map(content -> new String(new Buffer(content).asBytes(), charset)); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringEncoder.java index 756638198e..415e8d9f9a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringEncoder.java @@ -21,7 +21,7 @@ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import org.reactivestreams.Publisher; -import reactor.Publishers; +import reactor.Flux; import org.springframework.core.ResolvableType; import org.springframework.util.MimeType; @@ -49,7 +49,7 @@ public class StringEncoder extends AbstractEncoder { } @Override - public Publisher encode(Publisher elementStream, + public Flux encode(Publisher elementStream, ResolvableType type, MimeType mimeType, Object... hints) { Charset charset; @@ -59,7 +59,7 @@ public class StringEncoder extends AbstractEncoder { else { charset = DEFAULT_CHARSET; } - return Publishers.map(elementStream, s -> ByteBuffer.wrap(s.getBytes(charset))); + return Flux.from(elementStream).map(s -> ByteBuffer.wrap(s.getBytes(charset))); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToReactorConverter.java b/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToReactorStreamConverter.java similarity index 91% rename from spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToReactorConverter.java rename to spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToReactorStreamConverter.java index 4a59deb976..9ae1c8d19c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToReactorConverter.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToReactorStreamConverter.java @@ -31,7 +31,7 @@ import org.springframework.core.convert.converter.GenericConverter; * @author Stephane Maldini * @author Sebastien Deleuze */ -public final class ReactiveStreamsToReactorConverter implements GenericConverter { +public final class ReactiveStreamsToReactorStreamConverter implements GenericConverter { @Override public Set getConvertibleTypes() { @@ -52,13 +52,13 @@ public final class ReactiveStreamsToReactorConverter implements GenericConverter return source; } else if (Stream.class.isAssignableFrom(targetType.getResolvableType().getRawClass())) { - return Streams.wrap((Publisher)source); + return Streams.from((Publisher)source); } else if (Promise.class.isAssignableFrom(source.getClass())) { return source; } else if (Promise.class.isAssignableFrom(targetType.getResolvableType().getRawClass())) { - return Streams.wrap((Publisher)source).next(); + return Streams.from((Publisher)source).promise(); } return null; } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpInputMessage.java b/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpInputMessage.java index 3cff8d9553..65778e219d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpInputMessage.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpInputMessage.java @@ -19,6 +19,7 @@ package org.springframework.http; import java.nio.ByteBuffer; import org.reactivestreams.Publisher; +import reactor.Flux; /** * An "reactive" HTTP input message that exposes the input as {@link Publisher}. @@ -34,6 +35,6 @@ public interface ReactiveHttpInputMessage extends HttpMessage { * Return the body of the message as a {@link Publisher}. * @return the body content publisher */ - Publisher getBody(); + Flux getBody(); } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java b/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java index 83b3507df4..1b76212bca 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java @@ -19,6 +19,7 @@ package org.springframework.http; import java.nio.ByteBuffer; import org.reactivestreams.Publisher; +import reactor.Mono; /** * A "reactive" HTTP output message that accepts output as a {@link Publisher}. @@ -37,6 +38,6 @@ public interface ReactiveHttpOutputMessage extends HttpMessage { * @param body the body content publisher * @return a publisher that indicates completion or error. */ - Publisher setBody(Publisher body); + Mono setBody(Publisher body); } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ErrorHandlingHttpHandler.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ErrorHandlingHttpHandler.java index 0050a4c010..92e2fa2c9d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ErrorHandlingHttpHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ErrorHandlingHttpHandler.java @@ -18,8 +18,7 @@ package org.springframework.http.server.reactive; import java.util.Arrays; import java.util.List; -import org.reactivestreams.Publisher; -import reactor.Publishers; +import reactor.Mono; import org.springframework.util.Assert; @@ -44,13 +43,13 @@ public class ErrorHandlingHttpHandler extends HttpHandlerDecorator { @Override - public Publisher handle(ServerHttpRequest request, ServerHttpResponse response) { - Publisher publisher; + public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { + Mono publisher; try { publisher = getDelegate().handle(request, response); } catch (Throwable ex) { - publisher = Publishers.error(ex); + publisher = Mono.error(ex); } for (HttpExceptionHandler handler : this.exceptionHandlers) { publisher = applyExceptionHandler(publisher, handler, request, response); @@ -58,12 +57,10 @@ public class ErrorHandlingHttpHandler extends HttpHandlerDecorator { return publisher; } - private static Publisher applyExceptionHandler(Publisher publisher, + private static Mono applyExceptionHandler(Mono publisher, HttpExceptionHandler handler, ServerHttpRequest request, ServerHttpResponse response) { - return Publishers.onErrorResumeNext(publisher, ex -> { - return handler.handle(request, response, ex); - }); + return publisher.flux().onErrorResumeWith(ex -> handler.handle(request, response, ex)).after(); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/FilterChainHttpHandler.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/FilterChainHttpHandler.java index 224e0fcf9d..80ac46057b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/FilterChainHttpHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/FilterChainHttpHandler.java @@ -19,8 +19,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; -import org.reactivestreams.Publisher; - +import reactor.Mono; /** * {@link HttpHandler} that delegates to a chain of {@link HttpFilter}s followed @@ -40,7 +39,7 @@ public class FilterChainHttpHandler extends HttpHandlerDecorator { @Override - public Publisher handle(ServerHttpRequest request, ServerHttpResponse response) { + public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { return new DefaultHttpFilterChain().filter(request, response); } @@ -50,7 +49,7 @@ public class FilterChainHttpHandler extends HttpHandlerDecorator { private int index; @Override - public Publisher filter(ServerHttpRequest request, ServerHttpResponse response) { + public Mono filter(ServerHttpRequest request, ServerHttpResponse response) { if (this.index < filters.size()) { HttpFilter filter = filters.get(this.index++); return filter.filter(request, response, this); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpExceptionHandler.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpExceptionHandler.java index 4fb95c20b6..5258d02564 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpExceptionHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpExceptionHandler.java @@ -15,7 +15,7 @@ */ package org.springframework.http.server.reactive; -import org.reactivestreams.Publisher; +import reactor.Mono; /** * A contract for resolving exceptions from HTTP request handling. @@ -38,6 +38,6 @@ public interface HttpExceptionHandler { * @param ex the exception to handle * @return Publisher to indicate when exception handling is complete. */ - Publisher handle(ServerHttpRequest request, ServerHttpResponse response, Throwable ex); + Mono handle(ServerHttpRequest request, ServerHttpResponse response, Throwable ex); } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpFilter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpFilter.java index 525fb4d625..b53fb8639b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpFilter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpFilter.java @@ -17,6 +17,7 @@ package org.springframework.http.server.reactive; import org.reactivestreams.Publisher; +import reactor.Mono; /** * Contract for interception-style, chained processing of HTTP requests. @@ -40,7 +41,7 @@ public interface HttpFilter { * @param chain provides a way to delegate to the next HttpFilter. * @return Publisher to indicate when request processing is complete. */ - Publisher filter(ServerHttpRequest request, ServerHttpResponse response, + Mono filter(ServerHttpRequest request, ServerHttpResponse response, HttpFilterChain chain); } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpFilterChain.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpFilterChain.java index a18a644f2d..7650014cad 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpFilterChain.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpFilterChain.java @@ -15,8 +15,7 @@ */ package org.springframework.http.server.reactive; -import org.reactivestreams.Publisher; - +import reactor.Mono; /** * Represents a chain of {@link HttpFilter}s allowing each {@link HttpFilter} to @@ -32,6 +31,6 @@ public interface HttpFilterChain { * @param response current HTTP response. * @return Publisher to indicate when request handling is complete. */ - Publisher filter(ServerHttpRequest request, ServerHttpResponse response); + Mono filter(ServerHttpRequest request, ServerHttpResponse response); } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpHandler.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpHandler.java index ab0e93197a..3d90ca5289 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpHandler.java @@ -16,7 +16,7 @@ package org.springframework.http.server.reactive; -import org.reactivestreams.Publisher; +import reactor.Mono; /** * Contract for handling HTTP requests in a non-blocking way. @@ -35,6 +35,6 @@ public interface HttpHandler { * @param response current HTTP response. * @return Publisher to indicate when request handling is complete. */ - Publisher handle(ServerHttpRequest request, ServerHttpResponse response); + Mono handle(ServerHttpRequest request, ServerHttpResponse response); } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpHandlerDecorator.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpHandlerDecorator.java index e8ba2baf77..49190be05d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpHandlerDecorator.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpHandlerDecorator.java @@ -15,7 +15,7 @@ */ package org.springframework.http.server.reactive; -import org.reactivestreams.Publisher; +import reactor.Mono; import org.springframework.util.Assert; @@ -40,7 +40,7 @@ public class HttpHandlerDecorator implements HttpHandler { @Override - public Publisher handle(ServerHttpRequest request, ServerHttpResponse response) { + public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { return this.delegate.handle(request, response); } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/InternalServerErrorExceptionHandler.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/InternalServerErrorExceptionHandler.java index 7c88c8febe..8d6cc4a22a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/InternalServerErrorExceptionHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/InternalServerErrorExceptionHandler.java @@ -15,8 +15,7 @@ */ package org.springframework.http.server.reactive; -import org.reactivestreams.Publisher; -import reactor.Publishers; +import reactor.Mono; import org.springframework.http.HttpStatus; @@ -29,9 +28,9 @@ public class InternalServerErrorExceptionHandler implements HttpExceptionHandler @Override - public Publisher handle(ServerHttpRequest request, ServerHttpResponse response, Throwable ex) { + public Mono handle(ServerHttpRequest request, ServerHttpResponse response, Throwable ex) { response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR); - return Publishers.empty(); + return Mono.empty(); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorHttpHandlerAdapter.java index b5005a3126..72ddbba872 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorHttpHandlerAdapter.java @@ -15,14 +15,11 @@ */ package org.springframework.http.server.reactive; -import org.reactivestreams.Publisher; +import reactor.Mono; import reactor.io.buffer.Buffer; import reactor.io.net.ReactiveChannelHandler; import reactor.io.net.http.HttpChannel; -import org.springframework.http.server.reactive.HttpHandler; -import org.springframework.http.server.reactive.ReactorServerHttpRequest; -import org.springframework.http.server.reactive.ReactorServerHttpResponse; import org.springframework.util.Assert; /** @@ -40,7 +37,7 @@ public class ReactorHttpHandlerAdapter } @Override - public Publisher apply(HttpChannel channel) { + public Mono apply(HttpChannel channel) { ReactorServerHttpRequest adaptedRequest = new ReactorServerHttpRequest(channel); ReactorServerHttpResponse adaptedResponse = new ReactorServerHttpResponse(channel); return this.httpHandler.handle(adaptedRequest, adaptedResponse); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java index 493e37df65..6d569c9352 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java @@ -19,8 +19,7 @@ import java.net.URI; import java.net.URISyntaxException; import java.nio.ByteBuffer; -import org.reactivestreams.Publisher; -import reactor.Publishers; +import reactor.Flux; import reactor.io.buffer.Buffer; import reactor.io.net.http.HttpChannel; @@ -82,8 +81,8 @@ public class ReactorServerHttpRequest implements ServerHttpRequest { } @Override - public Publisher getBody() { - return Publishers.map(this.channel.input(), Buffer::byteBuffer); + public Flux getBody() { + return Flux.from(this.channel.input()).map(Buffer::byteBuffer); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java index d3fdf10a22..58a8947440 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java @@ -19,7 +19,8 @@ import java.nio.ByteBuffer; import java.util.List; import org.reactivestreams.Publisher; -import reactor.Publishers; +import reactor.Flux; +import reactor.Mono; import reactor.io.buffer.Buffer; import reactor.io.net.http.HttpChannel; import reactor.io.net.http.model.Status; @@ -64,12 +65,12 @@ public class ReactorServerHttpResponse implements ServerHttpResponse { } @Override - public Publisher setBody(Publisher publisher) { - return Publishers.lift(publisher, new WriteWithOperator<>(this::setBodyInternal)); + public Mono setBody(Publisher publisher) { + return Flux.from(publisher).lift(new WriteWithOperator<>(this::setBodyInternal)).after(); } - protected Publisher setBodyInternal(Publisher publisher) { - return getReactorChannel().writeWith(Publishers.map(publisher, Buffer::new)); + protected Mono setBodyInternal(Publisher publisher) { + return Mono.from(getReactorChannel().writeWith(Flux.from(publisher).map(Buffer::new))); } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java index 6afad37829..f4ee9f3dcf 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java @@ -23,6 +23,7 @@ import java.nio.ByteBuffer; import io.netty.buffer.ByteBuf; import io.reactivex.netty.protocol.http.server.HttpServerRequest; import org.reactivestreams.Publisher; +import reactor.Flux; import reactor.core.publisher.convert.RxJava1Converter; import rx.Observable; @@ -85,7 +86,7 @@ public class RxNettyServerHttpRequest implements ServerHttpRequest { } @Override - public Publisher getBody() { + public Flux getBody() { Observable content = this.getRxNettyRequest().getContent().map(ByteBuf::nioBuffer); content = content.concatWith(Observable.empty()); // See GH issue #58 return RxJava1Converter.from(content); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java index aec6680288..05ed55e8c3 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java @@ -22,7 +22,8 @@ import java.util.List; import io.netty.handler.codec.http.HttpResponseStatus; import io.reactivex.netty.protocol.http.server.HttpServerResponse; import org.reactivestreams.Publisher; -import reactor.Publishers; +import reactor.Flux; +import reactor.Mono; import reactor.core.publisher.convert.RxJava1Converter; import rx.Observable; @@ -66,14 +67,14 @@ public class RxNettyServerHttpResponse implements ServerHttpResponse { } @Override - public Publisher setBody(Publisher publisher) { - return Publishers.lift(publisher, new WriteWithOperator<>(this::setBodyInternal)); + public Mono setBody(Publisher publisher) { + return Flux.from(publisher).lift(new WriteWithOperator<>(this::setBodyInternal)).after(); } - protected Publisher setBodyInternal(Publisher publisher) { + protected Mono setBodyInternal(Publisher publisher) { Observable content = RxJava1Converter.from(publisher).map(this::toBytes); Observable completion = getRxNettyResponse().writeBytes(content); - return RxJava1Converter.from(completion); + return RxJava1Converter.from(completion).after(); } private byte[] toBytes(ByteBuffer buffer) { diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java index 97bf0d7f4b..41b229c6bc 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java @@ -36,6 +36,7 @@ import org.apache.commons.logging.LogFactory; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; +import reactor.Mono; import org.springframework.http.HttpStatus; import org.springframework.util.Assert; @@ -73,7 +74,7 @@ public class ServletHttpHandlerAdapter extends HttpServlet { ResponseBodySubscriber responseBodySubscriber = new ResponseBodySubscriber(synchronizer); ServletServerHttpResponse response = new ServletServerHttpResponse(servletResponse, - publisher -> subscriber -> publisher.subscribe(responseBodySubscriber)); + publisher -> Mono.from(subscriber -> publisher.subscribe(responseBodySubscriber))); servletResponse.getOutputStream().setWriteListener(responseBodySubscriber); HandlerResultSubscriber resultSubscriber = new HandlerResultSubscriber(synchronizer, response); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java index 3f1eb1c69c..d5c1461860 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java @@ -25,6 +25,7 @@ import java.util.Map; import javax.servlet.http.HttpServletRequest; import org.reactivestreams.Publisher; +import reactor.Flux; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -46,14 +47,14 @@ public class ServletServerHttpRequest implements ServerHttpRequest { private HttpHeaders headers; - private final Publisher requestBodyPublisher; + private final Flux requestBodyPublisher; public ServletServerHttpRequest(HttpServletRequest request, Publisher body) { Assert.notNull(request, "'request' must not be null."); Assert.notNull(body, "'body' must not be null."); this.request = request; - this.requestBodyPublisher = body; + this.requestBodyPublisher = Flux.from(body); } @@ -126,7 +127,7 @@ public class ServletServerHttpRequest implements ServerHttpRequest { } @Override - public Publisher getBody() { + public Flux getBody() { return this.requestBodyPublisher; } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java index 19e10d3128..0fc838af41 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java @@ -22,7 +22,8 @@ import java.util.function.Function; import javax.servlet.http.HttpServletResponse; import org.reactivestreams.Publisher; -import reactor.Publishers; +import reactor.Flux; +import reactor.Mono; import org.springframework.http.ExtendedHttpHeaders; import org.springframework.http.HttpHeaders; @@ -38,13 +39,13 @@ public class ServletServerHttpResponse implements ServerHttpResponse { private final HttpServletResponse response; - private final Function, Publisher> responseBodyWriter; + private final Function, Mono> responseBodyWriter; private final HttpHeaders headers; public ServletServerHttpResponse(HttpServletResponse response, - Function, Publisher> responseBodyWriter) { + Function, Mono> responseBodyWriter) { Assert.notNull(response, "'response' must not be null"); Assert.notNull(responseBodyWriter, "'responseBodyWriter' must not be null"); @@ -69,11 +70,11 @@ public class ServletServerHttpResponse implements ServerHttpResponse { } @Override - public Publisher setBody(final Publisher publisher) { - return Publishers.lift(publisher, new WriteWithOperator<>(this::setBodyInternal)); + public Mono setBody(final Publisher publisher) { + return Flux.from(publisher).lift(new WriteWithOperator<>(this::setBodyInternal)).after(); } - protected Publisher setBodyInternal(Publisher publisher) { + protected Mono setBodyInternal(Publisher publisher) { return this.responseBodyWriter.apply(publisher); } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java index db61960ef6..0e4f86f60d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java @@ -35,7 +35,8 @@ import org.reactivestreams.Subscription; import org.xnio.ChannelListener; import org.xnio.channels.StreamSinkChannel; import org.xnio.channels.StreamSourceChannel; -import reactor.core.error.SpecificationExceptions; +import reactor.Mono; +import reactor.core.error.Exceptions; import reactor.core.subscriber.BaseSubscriber; import reactor.core.support.BackpressureUtils; @@ -72,7 +73,7 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle ResponseBodySubscriber responseBodySubscriber = new ResponseBodySubscriber(exchange); ServerHttpResponse response = new UndertowServerHttpResponse(exchange, - publisher -> subscriber -> publisher.subscribe(responseBodySubscriber)); + publisher -> Mono.from(subscriber -> publisher.subscribe(responseBodySubscriber))); exchange.dispatch(); @@ -126,7 +127,7 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle @Override public void subscribe(Subscriber subscriber) { if (subscriber == null) { - throw SpecificationExceptions.spec_2_13_exception(); + throw Exceptions.spec_2_13_exception(); } if (this.subscriber != null) { subscriber.onError(new IllegalStateException("Only one subscriber allowed")); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java index 9b710945fd..98c766e589 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java @@ -23,6 +23,7 @@ import java.nio.ByteBuffer; import io.undertow.server.HttpServerExchange; import io.undertow.util.HeaderValues; import org.reactivestreams.Publisher; +import reactor.Flux; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -42,14 +43,14 @@ public class UndertowServerHttpRequest implements ServerHttpRequest { private HttpHeaders headers; - private final Publisher body; + private final Flux body; public UndertowServerHttpRequest(HttpServerExchange exchange, Publisher body) { Assert.notNull(exchange, "'exchange' is required."); Assert.notNull(exchange, "'body' is required."); this.exchange = exchange; - this.body = body; + this.body = Flux.from(body); } @@ -91,7 +92,7 @@ public class UndertowServerHttpRequest implements ServerHttpRequest { } @Override - public Publisher getBody() { + public Flux getBody() { return this.body; } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java index d13b80eda2..2d303c4cd0 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java @@ -23,7 +23,8 @@ import java.util.function.Function; import io.undertow.server.HttpServerExchange; import io.undertow.util.HttpString; import org.reactivestreams.Publisher; -import reactor.Publishers; +import reactor.Flux; +import reactor.Mono; import org.springframework.http.ExtendedHttpHeaders; import org.springframework.http.HttpHeaders; @@ -40,13 +41,13 @@ public class UndertowServerHttpResponse implements ServerHttpResponse { private final HttpServerExchange exchange; - private final Function, Publisher> responseBodyWriter; + private final Function, Mono> responseBodyWriter; private final HttpHeaders headers; public UndertowServerHttpResponse(HttpServerExchange exchange, - Function, Publisher> responseBodyWriter) { + Function, Mono> responseBodyWriter) { Assert.notNull(exchange, "'exchange' is required."); Assert.notNull(responseBodyWriter, "'responseBodyWriter' must not be null"); @@ -72,11 +73,11 @@ public class UndertowServerHttpResponse implements ServerHttpResponse { } @Override - public Publisher setBody(Publisher publisher) { - return Publishers.lift(publisher, new WriteWithOperator<>(this::setBodyInternal)); + public Mono setBody(Publisher publisher) { + return Flux.from(publisher).lift(new WriteWithOperator<>(this::setBodyInternal)).after(); } - protected Publisher setBodyInternal(Publisher publisher) { + protected Mono setBodyInternal(Publisher publisher) { return this.responseBodyWriter.apply(publisher); } diff --git a/spring-web-reactive/src/main/java/org/springframework/util/ByteBufferPublisherInputStream.java b/spring-web-reactive/src/main/java/org/springframework/util/ByteBufferPublisherInputStream.java index 26d2e5ee41..10aa4c23d2 100644 --- a/spring-web-reactive/src/main/java/org/springframework/util/ByteBufferPublisherInputStream.java +++ b/spring-web-reactive/src/main/java/org/springframework/util/ByteBufferPublisherInputStream.java @@ -23,7 +23,7 @@ import java.util.concurrent.BlockingQueue; import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; -import reactor.Publishers; +import reactor.rx.Streams; /** * {@code InputStream} implementation based on a byte array {@link Publisher}. @@ -60,7 +60,7 @@ public class ByteBufferPublisherInputStream extends InputStream { public ByteBufferPublisherInputStream(Publisher publisher, int requestSize) { Assert.notNull(publisher, "'publisher' must not be null"); - this.queue = Publishers.toReadQueue(publisher, requestSize); + this.queue = Streams.from(publisher).toBlockingQueue(requestSize); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java index ee3bfe70e3..93e1aa3031 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java @@ -23,10 +23,8 @@ import java.util.function.Function; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.reactivestreams.Publisher; -import org.reactivestreams.Subscriber; -import reactor.Publishers; -import reactor.fn.BiConsumer; +import reactor.Flux; +import reactor.Mono; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactoryUtils; @@ -114,42 +112,25 @@ public class DispatcherHandler implements HttpHandler, ApplicationContextAware { @Override - public Publisher handle(ServerHttpRequest request, ServerHttpResponse response) { + public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { if (logger.isDebugEnabled()) { logger.debug("Processing " + request.getMethod() + " request for [" + request.getURI() + "]"); } - - Publisher mappings = Publishers.from(this.handlerMappings); - Publisher handlerPublisher = Publishers.concatMap(mappings, m -> m.getHandler(request)); - handlerPublisher = first(handlerPublisher); - - Publisher resultPublisher = Publishers.concatMap(handlerPublisher, handler -> { - HandlerAdapter handlerAdapter = getHandlerAdapter(handler); - return handlerAdapter.handle(request, response, handler); - }); - - Publisher completionPublisher = Publishers.concatMap(resultPublisher, result -> { - Publisher publisher; - if (result.hasError()) { - publisher = Publishers.error(result.getError()); - } - else { - HandlerResultHandler handler = getResultHandler(result); - publisher = handler.handleResult(request, response, result); - } - if (result.hasExceptionMapper()) { - return Publishers.onErrorResumeNext(publisher, ex -> { - return Publishers.concatMap(result.getExceptionMapper().apply(ex), - errorResult -> { - HandlerResultHandler handler = getResultHandler(errorResult); - return handler.handleResult(request, response, errorResult); - }); - }); - } - return publisher; - }); - - return mapError(completionPublisher, this.errorMapper); + return Flux.fromIterable(this.handlerMappings) + .concatMap(m -> m.getHandler(request)) + .next() + .then(handler -> getHandlerAdapter(handler).handle(request, response, handler)) + .then(result -> { + Mono publisher = (result.hasError() ? Mono.error(result.getError()) : + getResultHandler(result).handleResult(request, response, result)); + if (result.hasExceptionMapper()) { + return publisher + .otherwise(ex -> result.getExceptionMapper().apply(ex) + .then(errorResult -> getResultHandler(errorResult).handleResult(request, response, errorResult))); + } + return publisher; + }) + .otherwise(ex -> Mono.error(this.errorMapper.apply(ex))); } protected HandlerAdapter getHandlerAdapter(Object handler) { @@ -171,22 +152,6 @@ public class DispatcherHandler implements HttpHandler, ApplicationContextAware { } - private static Publisher first(Publisher source) { - return Publishers.lift(source, (e, subscriber) -> { - subscriber.onNext(e); - subscriber.onComplete(); - }); - } - - private static Publisher mapError(Publisher source, Function function) { - return Publishers.lift(source, null, new BiConsumer>() { - @Override - public void accept(Throwable throwable, Subscriber subscriber) { - subscriber.onError(function.apply(throwable)); - } - }, null); - } - private static class NotFoundHandlerMapping implements HandlerMapping { @SuppressWarnings("ThrowableInstanceNeverThrown") @@ -194,8 +159,8 @@ public class DispatcherHandler implements HttpHandler, ApplicationContextAware { @Override - public Publisher getHandler(ServerHttpRequest request) { - return Publishers.error(HANDLER_NOT_FOUND_EXCEPTION); + public Mono getHandler(ServerHttpRequest request) { + return Mono.error(HANDLER_NOT_FOUND_EXCEPTION); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerAdapter.java index 40f0c7251f..c6f5c3541c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerAdapter.java @@ -17,6 +17,7 @@ package org.springframework.web.reactive; import org.reactivestreams.Publisher; +import reactor.Mono; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; @@ -54,7 +55,7 @@ public interface HandlerAdapter { * returned {@code true}. * @return A {@link Publisher} object that produces a single {@link HandlerResult} element */ - Publisher handle(ServerHttpRequest request, ServerHttpResponse response, + Mono handle(ServerHttpRequest request, ServerHttpResponse response, Object handler); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerMapping.java index 1f399035a1..ae276d6230 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerMapping.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerMapping.java @@ -16,7 +16,7 @@ package org.springframework.web.reactive; -import org.reactivestreams.Publisher; +import reactor.Mono; import org.springframework.http.server.reactive.ServerHttpRequest; @@ -32,8 +32,8 @@ public interface HandlerMapping { /** * Return a handler for this request. * @param request current HTTP request - * @return A {@link Publisher} object that produces a single handler element + * @return A {@link Mono} object that produces a single handler element */ - Publisher getHandler(ServerHttpRequest request); + Mono getHandler(ServerHttpRequest request); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java index 4a6bae92ab..ff84c5caa5 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java @@ -17,10 +17,8 @@ package org.springframework.web.reactive; import java.util.function.Function; -import java.util.logging.Handler; -import org.reactivestreams.Publisher; -import reactor.Publishers; +import reactor.Mono; import org.springframework.core.ResolvableType; import org.springframework.util.Assert; @@ -40,7 +38,7 @@ public class HandlerResult { private final Throwable error; - private Function> exceptionMapper; + private Function> exceptionMapper; public HandlerResult(Object handler, Object result, ResolvableType resultType) { @@ -95,12 +93,12 @@ public class HandlerResult { * that results in an error. * @param function the exception resolving function */ - public HandlerResult setExceptionMapper(Function> function) { + public HandlerResult setExceptionMapper(Function> function) { this.exceptionMapper = function; return this; } - public Function> getExceptionMapper() { + public Function> getExceptionMapper() { return this.exceptionMapper; } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResultHandler.java index 0fc158c120..e121bdff98 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResultHandler.java @@ -16,7 +16,7 @@ package org.springframework.web.reactive; -import org.reactivestreams.Publisher; +import reactor.Mono; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; @@ -42,13 +42,13 @@ public interface HandlerResultHandler { * Process the given result in an asynchronous non blocking way, by eventually modifying * response headers, or writing some data stream into the response. * Implementations should not throw exceptions but signal them via the returned - * {@code Publisher}. + * {@code Mono}. * - * @return A {@code Publisher} used to signal the demand, and receive a notification + * @return A {@code Mono} used to signal the demand, and receive a notification * when the handling is complete (success or error) including the flush of the data on the * network. */ - Publisher handleResult(ServerHttpRequest request, ServerHttpResponse response, + Mono handleResult(ServerHttpRequest request, ServerHttpResponse response, HandlerResult result); } \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/ResponseStatusExceptionHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/ResponseStatusExceptionHandler.java index f9e3135532..713a692871 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/ResponseStatusExceptionHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/ResponseStatusExceptionHandler.java @@ -15,8 +15,7 @@ */ package org.springframework.web.reactive; -import org.reactivestreams.Publisher; -import reactor.Publishers; +import reactor.Mono; import org.springframework.http.server.reactive.HttpExceptionHandler; import org.springframework.http.server.reactive.ServerHttpRequest; @@ -32,12 +31,12 @@ public class ResponseStatusExceptionHandler implements HttpExceptionHandler { @Override - public Publisher handle(ServerHttpRequest request, ServerHttpResponse response, Throwable ex) { + public Mono handle(ServerHttpRequest request, ServerHttpResponse response, Throwable ex) { if (ex instanceof ResponseStatusException) { response.setStatusCode(((ResponseStatusException) ex).getHttpStatus()); - return Publishers.empty(); + return Mono.empty(); } - return Publishers.error(ex); + return Mono.error(ex); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/HttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/HttpHandlerAdapter.java index 34ae1fcf98..853f522f75 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/HttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/HttpHandlerAdapter.java @@ -17,7 +17,7 @@ package org.springframework.web.reactive.handler; import org.reactivestreams.Publisher; -import reactor.Publishers; +import reactor.Mono; import org.springframework.core.ResolvableType; import org.springframework.http.server.reactive.ServerHttpRequest; @@ -50,12 +50,12 @@ public class HttpHandlerAdapter implements HandlerAdapter { } @Override - public Publisher handle(ServerHttpRequest request, + public Mono handle(ServerHttpRequest request, ServerHttpResponse response, Object handler) { HttpHandler httpHandler = (HttpHandler)handler; - Publisher completion = httpHandler.handle(request, response); - return Publishers.just(new HandlerResult(httpHandler, completion, PUBLISHER_VOID)); + Mono completion = httpHandler.handle(request, response); + return Mono.just(new HandlerResult(httpHandler, completion, PUBLISHER_VOID)); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java index efa8d7e831..0e27adc704 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java @@ -17,7 +17,7 @@ package org.springframework.web.reactive.handler; import org.reactivestreams.Publisher; -import reactor.Publishers; +import reactor.Mono; import org.springframework.core.Ordered; import org.springframework.core.ResolvableType; @@ -74,17 +74,17 @@ public class SimpleHandlerResultHandler implements Ordered, HandlerResultHandler @SuppressWarnings("unchecked") @Override - public Publisher handleResult(ServerHttpRequest request, + public Mono handleResult(ServerHttpRequest request, ServerHttpResponse response, HandlerResult result) { Object value = result.getResult(); if (Void.TYPE.equals(result.getResultType().getRawClass())) { - return Publishers.empty(); + return Mono.empty(); } - return (value instanceof Publisher ? (Publisher)value : - this.conversionService.convert(value, Publisher.class)); + return (value instanceof Mono ? (Mono)value : + Mono.from(this.conversionService.convert(value, Publisher.class))); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMapping.java index b57ede729a..0ef5e9c286 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMapping.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMapping.java @@ -19,8 +19,8 @@ package org.springframework.web.reactive.handler; import java.util.HashMap; import java.util.Map; -import org.reactivestreams.Publisher; -import reactor.core.publisher.PublisherFactory; +import reactor.Flux; +import reactor.Mono; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.web.reactive.HandlerMapping; @@ -42,15 +42,15 @@ public class SimpleUrlHandlerMapping implements HandlerMapping { @Override - public Publisher getHandler(ServerHttpRequest request) { - return PublisherFactory.create(subscriber -> { + public Mono getHandler(ServerHttpRequest request) { + return Flux.create(subscriber -> { String path = request.getURI().getPath(); Object handler = this.handlerMap.get(path); if (handler != null) { subscriber.onNext(handler); } subscriber.onComplete(); - }); + }).next(); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/HandlerMethodArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/HandlerMethodArgumentResolver.java index 1df3064b1f..f7c9c6e190 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/HandlerMethodArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/HandlerMethodArgumentResolver.java @@ -16,7 +16,7 @@ package org.springframework.web.reactive.method; -import org.reactivestreams.Publisher; +import reactor.Mono; import org.springframework.core.MethodParameter; import org.springframework.http.server.reactive.ServerHttpRequest; @@ -39,6 +39,6 @@ public interface HandlerMethodArgumentResolver { * resolve to any value which will result in passing {@code null} as the * argument value. */ - Publisher resolveArgument(MethodParameter parameter, ServerHttpRequest request); + Mono resolveArgument(MethodParameter parameter, ServerHttpRequest request); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java index 5cebab6252..a1c3babd4a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java @@ -21,15 +21,13 @@ import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; -import org.reactivestreams.Publisher; -import reactor.Publishers; +import reactor.Flux; +import reactor.Mono; import reactor.fn.tuple.Tuple; -import reactor.rx.Streams; import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.GenericTypeResolver; @@ -48,7 +46,7 @@ import org.springframework.web.reactive.HandlerResult; */ public class InvocableHandlerMethod extends HandlerMethod { - public static final Publisher NO_ARGS = Publishers.just(new Object[0]); + public static final Mono NO_ARGS = Mono.just(new Object[0]); private final static Object NO_VALUE = new Object(); @@ -86,38 +84,37 @@ public class InvocableHandlerMethod extends HandlerMethod { * @return Publisher that produces a single HandlerResult or an error signal; * never throws an exception. */ - public Publisher invokeForRequest(ServerHttpRequest request, Object... providedArgs) { + public Mono invokeForRequest(ServerHttpRequest request, Object... providedArgs) { - Publisher argsPublisher = NO_ARGS; + Mono argsPublisher = NO_ARGS; try { if (!ObjectUtils.isEmpty(getMethodParameters())) { - List> publishers = resolveArguments(request, providedArgs); - argsPublisher = Publishers.zip(publishers, this::initArgs); - argsPublisher = first(argsPublisher); + List> publishers = resolveArguments(request, providedArgs); + argsPublisher = Flux.zip(publishers, this::initArgs).next(); } } catch (Throwable ex) { - return Publishers.error(ex); + return Mono.error(ex); } - return Publishers.concatMap(argsPublisher, args -> { + return Flux.from(argsPublisher).concatMap(args -> { try { Object value = doInvoke(args); ResolvableType type = ResolvableType.forMethodParameter(getReturnType()); HandlerResult handlerResult = new HandlerResult(this, value, type); - return Publishers.just(handlerResult); + return Mono.just(handlerResult); } catch (InvocationTargetException ex) { - return Publishers.error(ex.getTargetException()); + return Mono.error(ex.getTargetException()); } catch (Throwable ex) { String s = getInvocationErrorMessage(args); - return Publishers.error(new IllegalStateException(s)); + return Mono.error(new IllegalStateException(s)); } - }); + }).next(); } - private List> resolveArguments(ServerHttpRequest request, Object... providedArgs) { + private List> resolveArguments(ServerHttpRequest request, Object... providedArgs) { return Stream.of(getMethodParameters()) .map(parameter -> { parameter.initParameterNameDiscovery(this.parameterNameDiscoverer); @@ -125,7 +122,7 @@ public class InvocableHandlerMethod extends HandlerMethod { if (!ObjectUtils.isEmpty(providedArgs)) { for (Object providedArg : providedArgs) { if (parameter.getParameterType().isInstance(providedArg)) { - return Publishers.just(providedArg); + return Mono.just(providedArg); } } } @@ -134,9 +131,10 @@ public class InvocableHandlerMethod extends HandlerMethod { .findFirst() .orElseThrow(() -> getArgError("No resolver for ", parameter, null)); try { - Publisher publisher = resolver.resolveArgument(parameter, request); - publisher = mapError(publisher, ex -> getArgError("Error resolving ", parameter, ex)); - return Streams.wrap(publisher).defaultIfEmpty(NO_VALUE); + return resolver.resolveArgument(parameter, request) + // TODO Add a defaultIfEmpty alias to Mono to avoid conversion to Flux + .flux().defaultIfEmpty(NO_VALUE).next() + .otherwise(ex -> Mono.error(getArgError("Error resolving ", parameter, ex))); } catch (Exception ex) { throw getArgError("Error resolving ", parameter, ex); @@ -180,17 +178,4 @@ public class InvocableHandlerMethod extends HandlerMethod { return Stream.of(tuple.toArray()).map(o -> o != NO_VALUE ? o : null).toArray(); } - - private static Publisher first(Publisher source) { - return Publishers.lift(source, (e, subscriber) -> { - subscriber.onNext(e); - subscriber.onComplete(); - }); - } - - private static Publisher mapError(Publisher source, Function function) { - return Publishers.lift(source, null, - (throwable, subscriber) -> subscriber.onError(function.apply(throwable)), null); - } - } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestBodyArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestBodyArgumentResolver.java index ec44088220..2bb4cdafa1 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestBodyArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestBodyArgumentResolver.java @@ -20,7 +20,8 @@ import java.nio.ByteBuffer; import java.util.List; import org.reactivestreams.Publisher; -import reactor.Publishers; +import reactor.Flux; +import reactor.Mono; import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; @@ -57,14 +58,14 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve } @Override - public Publisher resolveArgument(MethodParameter parameter, ServerHttpRequest request) { + public Mono resolveArgument(MethodParameter parameter, ServerHttpRequest request) { MediaType mediaType = request.getHeaders().getContentType(); if (mediaType == null) { mediaType = MediaType.APPLICATION_OCTET_STREAM; } ResolvableType type = ResolvableType.forMethodParameter(parameter); - Publisher body = request.getBody(); - Publisher elementStream = body; + Flux body = request.getBody(); + Flux elementStream = body; ResolvableType elementType = type.hasGenerics() ? type.getGeneric(0) : type; Decoder decoder = resolveDecoder(elementType, mediaType); @@ -73,10 +74,10 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve } if (this.conversionService.canConvert(Publisher.class, type.getRawClass())) { - return Publishers.just(this.conversionService.convert(elementStream, type.getRawClass())); + return Mono.just(this.conversionService.convert(elementStream, type.getRawClass())); } - return Publishers.map(elementStream, element -> element); + return (Mono)Mono.from(elementStream); } private Decoder resolveDecoder(ResolvableType type, MediaType mediaType, Object... hints) { diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java index de6106177d..d1409e4e93 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java @@ -25,8 +25,8 @@ import java.util.concurrent.ConcurrentHashMap; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.reactivestreams.Publisher; -import reactor.Publishers; +import reactor.Flux; +import reactor.Mono; import org.springframework.beans.factory.InitializingBean; import org.springframework.core.codec.Decoder; @@ -106,7 +106,7 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Initializin } @Override - public Publisher handle(ServerHttpRequest request, + public Mono handle(ServerHttpRequest request, ServerHttpResponse response, Object handler) { HandlerMethod handlerMethod = (HandlerMethod) handler; @@ -114,20 +114,18 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Initializin InvocableHandlerMethod invocable = new InvocableHandlerMethod(handlerMethod); invocable.setHandlerMethodArgumentResolvers(this.argumentResolvers); - Publisher publisher = invocable.invokeForRequest(request); + Flux publisher = invocable.invokeForRequest(request).flux(); + publisher = publisher.onErrorResumeWith(ex -> Mono.just(new HandlerResult(handler, ex))); - publisher = Publishers.onErrorResumeNext(publisher, ex -> { - return Publishers.just(new HandlerResult(handler, ex)); - }); - publisher = Publishers.map(publisher, + publisher = publisher.map( result -> result.setExceptionMapper( - ex -> mapException((Exception) ex, handlerMethod, request, response))); + ex -> mapException(ex, handlerMethod, request, response))); - return publisher; + return publisher.next(); } - private Publisher mapException(Throwable ex, HandlerMethod handlerMethod, + private Mono mapException(Throwable ex, HandlerMethod handlerMethod, ServerHttpRequest request, ServerHttpResponse response) { if (ex instanceof Exception) { @@ -147,7 +145,7 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Initializin } } } - return Publishers.error(ex); + return Mono.error(ex); } protected InvocableHandlerMethod findExceptionHandler(HandlerMethod handlerMethod, Exception exception) { diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMapping.java index f19f7f96cf..476b0bc8f9 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMapping.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMapping.java @@ -27,8 +27,8 @@ import java.util.TreeSet; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.reactivestreams.Publisher; -import reactor.core.publisher.PublisherFactory; +import reactor.Flux; +import reactor.Mono; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ApplicationContext; @@ -94,8 +94,8 @@ public class RequestMappingHandlerMapping implements HandlerMapping, } @Override - public Publisher getHandler(ServerHttpRequest request) { - return PublisherFactory.create(subscriber -> { + public Mono getHandler(ServerHttpRequest request) { + return Flux.create(subscriber -> { for (Map.Entry entry : this.methodMap.entrySet()) { RequestMappingInfo info = entry.getKey(); if (info.matchesRequest(request)) { @@ -109,7 +109,7 @@ public class RequestMappingHandlerMapping implements HandlerMapping, } } subscriber.onComplete(); - }); + }).next(); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestParamArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestParamArgumentResolver.java index b50c5ebe8c..8ba175976b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestParamArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestParamArgumentResolver.java @@ -16,11 +16,7 @@ package org.springframework.web.reactive.method.annotation; - -import java.util.Optional; - -import org.reactivestreams.Publisher; -import reactor.Publishers; +import reactor.Mono; import org.springframework.core.MethodParameter; import org.springframework.http.server.reactive.ServerHttpRequest; @@ -45,12 +41,12 @@ public class RequestParamArgumentResolver implements HandlerMethodArgumentResolv @Override - public Publisher resolveArgument(MethodParameter param, ServerHttpRequest request) { + public Mono resolveArgument(MethodParameter param, ServerHttpRequest request) { RequestParam annotation = param.getParameterAnnotation(RequestParam.class); String name = (annotation.value().length() != 0 ? annotation.value() : param.getParameterName()); UriComponents uriComponents = UriComponentsBuilder.fromUri(request.getURI()).build(); String value = uriComponents.getQueryParams().getFirst(name); - return (value != null ? Publishers.just(value) : Publishers.empty()); + return (value != null ? Mono.just(value) : Mono.empty()); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java index e52099f27d..05e3a2adc0 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java @@ -28,7 +28,7 @@ import java.util.Set; import java.util.stream.Collectors; import org.reactivestreams.Publisher; -import reactor.Publishers; +import reactor.Mono; import org.springframework.core.Ordered; import org.springframework.core.ResolvableType; @@ -127,12 +127,12 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered @Override @SuppressWarnings("unchecked") - public Publisher handleResult(ServerHttpRequest request, + public Mono handleResult(ServerHttpRequest request, ServerHttpResponse response, HandlerResult result) { Object value = result.getResult(); if (value == null) { - return Publishers.empty(); + return Mono.empty(); } Publisher publisher; @@ -143,7 +143,7 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered elementType = returnType.getGeneric(0); } else { - publisher = Publishers.just(value); + publisher = Mono.just(value); elementType = returnType; } @@ -163,7 +163,7 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered } } if (compatibleMediaTypes.isEmpty()) { - return Publishers.error(new HttpMediaTypeNotAcceptableException(producibleMediaTypes)); + return Mono.error(new HttpMediaTypeNotAcceptableException(producibleMediaTypes)); } List mediaTypes = new ArrayList<>(compatibleMediaTypes); @@ -189,7 +189,7 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered } } - return Publishers.error(new HttpMediaTypeNotAcceptableException(this.allMediaTypes)); + return Mono.error(new HttpMediaTypeNotAcceptableException(this.allMediaTypes)); } private List getAcceptableMediaTypes(ServerHttpRequest request) { diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/EchoHandler.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/EchoHandler.java index 684f1f52be..c6f7a7b28a 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/EchoHandler.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/EchoHandler.java @@ -16,11 +16,7 @@ package org.springframework.http.server.reactive; -import org.reactivestreams.Publisher; - -import org.springframework.http.server.reactive.HttpHandler; -import org.springframework.http.server.reactive.ServerHttpRequest; -import org.springframework.http.server.reactive.ServerHttpResponse; +import reactor.Mono; /** * @author Arjen Poutsma @@ -28,7 +24,7 @@ import org.springframework.http.server.reactive.ServerHttpResponse; public class EchoHandler implements HttpHandler { @Override - public Publisher handle(ServerHttpRequest request, ServerHttpResponse response) { + public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { return response.setBody(request.getBody()); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ErrorHandlingHttpHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ErrorHandlingHttpHandlerTests.java index ce13fa5694..925eeacc7c 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ErrorHandlingHttpHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ErrorHandlingHttpHandlerTests.java @@ -17,12 +17,11 @@ package org.springframework.http.server.reactive; import java.net.URI; -import java.util.concurrent.TimeUnit; import org.junit.Before; import org.junit.Test; import org.reactivestreams.Publisher; -import reactor.Publishers; +import reactor.Mono; import reactor.rx.Streams; import reactor.rx.stream.Signal; @@ -56,8 +55,7 @@ public class ErrorHandlingHttpHandlerTests { HttpHandler targetHandler = new TestHttpHandler(new IllegalStateException("boo")); HttpHandler handler = new ErrorHandlingHttpHandler(targetHandler, exceptionHandler); - Publisher publisher = handler.handle(this.request, this.response); - Streams.wrap(publisher).toList().await(5, TimeUnit.SECONDS); + handler.handle(this.request, this.response).get(); assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, this.response.getStatus()); } @@ -73,8 +71,7 @@ public class ErrorHandlingHttpHandlerTests { HttpHandler targetHandler = new TestHttpHandler(new IllegalStateException("boo")); HttpHandler httpHandler = new ErrorHandlingHttpHandler(targetHandler, exceptionHandlers); - Publisher publisher = httpHandler.handle(this.request, this.response); - Streams.wrap(publisher).toList().await(5, TimeUnit.SECONDS); + httpHandler.handle(this.request, this.response).get(); assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, this.response.getStatus()); } @@ -98,15 +95,14 @@ public class ErrorHandlingHttpHandlerTests { HttpHandler targetHandler = new TestHttpHandler(new IllegalStateException("boo"), true); HttpHandler handler = new ErrorHandlingHttpHandler(targetHandler, exceptionHandler); - Publisher publisher = handler.handle(this.request, this.response); - Streams.wrap(publisher).toList().await(5, TimeUnit.SECONDS); + handler.handle(this.request, this.response).get(); assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, this.response.getStatus()); } private Throwable awaitErrorSignal(Publisher publisher) throws Exception { - Signal signal = Streams.wrap(publisher).materialize().toList().await(5, TimeUnit.SECONDS).get(0); + Signal signal = Streams.from(publisher).materialize().toList().get().get(0); assertEquals("Unexpected signal: " + signal, Signal.Type.ERROR, signal.getType()); return signal.getThrowable(); } @@ -129,11 +125,11 @@ public class ErrorHandlingHttpHandlerTests { } @Override - public Publisher handle(ServerHttpRequest request, ServerHttpResponse response) { + public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { if (this.raise) { throw this.exception; } - return Publishers.error(this.exception); + return Mono.error(this.exception); } } @@ -142,8 +138,8 @@ public class ErrorHandlingHttpHandlerTests { private static class UnresolvedExceptionHandler implements HttpExceptionHandler { @Override - public Publisher handle(ServerHttpRequest request, ServerHttpResponse response, Throwable ex) { - return Publishers.error(ex); + public Mono handle(ServerHttpRequest request, ServerHttpResponse response, Throwable ex) { + return Mono.error(ex); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/FilterChainHttpHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/FilterChainHttpHandlerTests.java index dcf1fff42d..3f88b4b97a 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/FilterChainHttpHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/FilterChainHttpHandlerTests.java @@ -16,15 +16,11 @@ package org.springframework.http.server.reactive; -import java.util.concurrent.TimeUnit; - import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.junit.Before; import org.junit.Test; -import org.reactivestreams.Publisher; -import reactor.Publishers; -import reactor.rx.Streams; +import reactor.Mono; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -57,8 +53,7 @@ public class FilterChainHttpHandlerTests { TestFilter filter3 = new TestFilter(); FilterChainHttpHandler filterHandler = new FilterChainHttpHandler(handler, filter1, filter2, filter3); - Publisher voidPublisher = filterHandler.handle(this.request, this.response); - Streams.wrap(voidPublisher).toList().await(10, TimeUnit.SECONDS); + filterHandler.handle(this.request, this.response).get(); assertTrue(filter1.invoked()); assertTrue(filter2.invoked()); @@ -71,8 +66,7 @@ public class FilterChainHttpHandlerTests { StubHandler handler = new StubHandler(); FilterChainHttpHandler filterHandler = new FilterChainHttpHandler(handler); - Publisher voidPublisher = filterHandler.handle(this.request, this.response); - Streams.wrap(voidPublisher).toList().await(10, TimeUnit.SECONDS); + filterHandler.handle(this.request, this.response).get(); assertTrue(handler.invoked()); } @@ -85,8 +79,7 @@ public class FilterChainHttpHandlerTests { TestFilter filter3 = new TestFilter(); FilterChainHttpHandler filterHandler = new FilterChainHttpHandler(handler, filter1, filter2, filter3); - Publisher voidPublisher = filterHandler.handle(this.request, this.response); - Streams.wrap(voidPublisher).toList().await(10, TimeUnit.SECONDS); + filterHandler.handle(this.request, this.response).get(); assertTrue(filter1.invoked()); assertTrue(filter2.invoked()); @@ -100,8 +93,7 @@ public class FilterChainHttpHandlerTests { AsyncFilter filter = new AsyncFilter(); FilterChainHttpHandler filterHandler = new FilterChainHttpHandler(handler, filter); - Publisher voidPublisher = filterHandler.handle(this.request, this.response); - Streams.wrap(voidPublisher).toList().await(10, TimeUnit.SECONDS); + filterHandler.handle(this.request, this.response).get(); assertTrue(filter.invoked()); assertTrue(handler.invoked()); @@ -119,14 +111,14 @@ public class FilterChainHttpHandlerTests { } @Override - public Publisher filter(ServerHttpRequest req, ServerHttpResponse res, + public Mono filter(ServerHttpRequest req, ServerHttpResponse res, HttpFilterChain chain) { this.invoked = true; return doFilter(req, res, chain); } - public Publisher doFilter(ServerHttpRequest req, ServerHttpResponse res, + public Mono doFilter(ServerHttpRequest req, ServerHttpResponse res, HttpFilterChain chain) { return chain.filter(req, res); @@ -136,25 +128,25 @@ public class FilterChainHttpHandlerTests { private static class ShortcircuitingFilter extends TestFilter { @Override - public Publisher doFilter(ServerHttpRequest req, ServerHttpResponse res, + public Mono doFilter(ServerHttpRequest req, ServerHttpResponse res, HttpFilterChain chain) { - return Publishers.empty(); + return Mono.empty(); } } private static class AsyncFilter extends TestFilter { @Override - public Publisher doFilter(ServerHttpRequest req, ServerHttpResponse res, HttpFilterChain chain) { - return Publishers.concatMap(doAsyncWork(), asyncResult -> { + public Mono doFilter(ServerHttpRequest req, ServerHttpResponse res, HttpFilterChain chain) { + return doAsyncWork().then(asyncResult -> { logger.debug("Async result: " + asyncResult); return chain.filter(req, res); }); } - private Publisher doAsyncWork() { - return Publishers.just("123"); + private Mono doAsyncWork() { + return Mono.just("123"); } } @@ -168,10 +160,10 @@ public class FilterChainHttpHandlerTests { } @Override - public Publisher handle(ServerHttpRequest req, ServerHttpResponse res) { + public Mono handle(ServerHttpRequest req, ServerHttpResponse res) { logger.trace("StubHandler invoked."); this.invoked = true; - return Publishers.empty(); + return Mono.empty(); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpRequest.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpRequest.java index c5ffbc83fa..db4412c6f5 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpRequest.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpRequest.java @@ -19,6 +19,7 @@ import java.net.URI; import java.nio.ByteBuffer; import org.reactivestreams.Publisher; +import reactor.Flux; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -34,7 +35,7 @@ public class MockServerHttpRequest implements ServerHttpRequest { private HttpHeaders headers = new HttpHeaders(); - private Publisher body; + private Flux body; public MockServerHttpRequest(HttpMethod httpMethod, URI uri) { @@ -43,7 +44,7 @@ public class MockServerHttpRequest implements ServerHttpRequest { } public MockServerHttpRequest(Publisher body, HttpMethod httpMethod, URI uri) { - this.body = body; + this.body = Flux.from(body); this.httpMethod = httpMethod; this.uri = uri; } @@ -77,11 +78,11 @@ public class MockServerHttpRequest implements ServerHttpRequest { } @Override - public Publisher getBody() { + public Flux getBody() { return this.body; } public void setBody(Publisher body) { - this.body = body; + this.body = Flux.from(body); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java index d478d56702..0a282a95b3 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java @@ -18,7 +18,8 @@ package org.springframework.http.server.reactive; import java.nio.ByteBuffer; import org.reactivestreams.Publisher; -import reactor.Publishers; +import reactor.Flux; +import reactor.Mono; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -50,9 +51,9 @@ public class MockServerHttpResponse implements ServerHttpResponse { } @Override - public Publisher setBody(Publisher body) { + public Mono setBody(Publisher body) { this.body = body; - return Publishers.concatMap(body, b -> Publishers.empty()); + return Flux.from(body).after(); } public Publisher getBody() { diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/RandomHandler.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/RandomHandler.java index 97af86dc70..477114c892 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/RandomHandler.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/RandomHandler.java @@ -21,11 +21,10 @@ import java.util.Random; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; +import reactor.Mono; import reactor.io.buffer.Buffer; -import reactor.rx.Streams; import static org.junit.Assert.assertEquals; @@ -41,7 +40,7 @@ public class RandomHandler implements HttpHandler { private final Random rnd = new Random(); @Override - public Publisher handle(ServerHttpRequest request, ServerHttpResponse response) { + public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { request.getBody().subscribe(new Subscriber() { private Subscription s; @@ -73,7 +72,7 @@ public class RandomHandler implements HttpHandler { }); response.getHeaders().setContentLength(RESPONSE_SIZE); - return response.setBody(Streams.just(ByteBuffer.wrap(randomBytes()))); + return response.setBody(Mono.just(ByteBuffer.wrap(randomBytes()))); } private byte[] randomBytes() { diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/WriteWithOperatorTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/WriteWithOperatorTests.java index 2ad1ddd3d4..12b06a3d2e 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/WriteWithOperatorTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/WriteWithOperatorTests.java @@ -27,14 +27,12 @@ import org.junit.Test; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; -import reactor.Publishers; -import reactor.core.publisher.PublisherFactory; +import reactor.Flux; import reactor.core.subscriber.SubscriberBarrier; import reactor.rx.Streams; import reactor.rx.stream.Signal; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; @@ -58,8 +56,8 @@ public class WriteWithOperatorTests { @Test public void errorBeforeFirstItem() throws Exception { IllegalStateException error = new IllegalStateException("boo"); - Publisher completion = Publishers.lift(Publishers.error(error), this.operator); - List> signals = Streams.wrap(completion).materialize().toList().await(5, TimeUnit.SECONDS); + Publisher completion = Flux.error(error).lift(this.operator); + List> signals = Streams.from(completion).materialize().toList().get(); assertEquals(1, signals.size()); assertSame("Unexpected signal: " + signals.get(0), error, signals.get(0).getThrowable()); @@ -67,8 +65,8 @@ public class WriteWithOperatorTests { @Test public void completionBeforeFirstItem() throws Exception { - Publisher completion = Publishers.lift(Publishers.empty(), this.operator); - List> signals = Streams.wrap(completion).materialize().toList().await(5, TimeUnit.SECONDS); + Publisher completion = Flux.empty().lift(this.operator); + List> signals = Streams.from(completion).materialize().toList().get(); assertEquals(1, signals.size()); assertTrue("Unexpected signal: " + signals.get(0), signals.get(0).isOnComplete()); @@ -79,8 +77,8 @@ public class WriteWithOperatorTests { @Test public void writeOneItem() throws Exception { - Publisher completion = Publishers.lift(Publishers.just("one"), this.operator); - List> signals = Streams.wrap(completion).materialize().toList().await(5, TimeUnit.SECONDS); + Publisher completion = Flux.just("one").lift(this.operator); + List> signals = Streams.from(completion).materialize().toList().get(); assertEquals(1, signals.size()); assertTrue("Unexpected signal: " + signals.get(0), signals.get(0).isOnComplete()); @@ -94,8 +92,8 @@ public class WriteWithOperatorTests { @Test public void writeMultipleItems() throws Exception { List items = Arrays.asList("one", "two", "three"); - Publisher completion = Publishers.lift(Publishers.from(items), this.operator); - List> signals = Streams.wrap(completion).materialize().consumeAsList().await(5, TimeUnit.SECONDS); + Publisher completion = Flux.fromIterable(items).lift(this.operator); + List> signals = Streams.from(completion).materialize().toList().get(); assertEquals(1, signals.size()); assertTrue("Unexpected signal: " + signals.get(0), signals.get(0).isOnComplete()); @@ -110,15 +108,15 @@ public class WriteWithOperatorTests { @Test public void errorAfterMultipleItems() throws Exception { IllegalStateException error = new IllegalStateException("boo"); - Publisher publisher = PublisherFactory.create(subscriber -> { + Flux publisher = Flux.create(subscriber -> { int i = subscriber.context().incrementAndGet(); subscriber.onNext(String.valueOf(i)); if (i == 3) { subscriber.onError(error); } }, subscriber -> new AtomicInteger()); - Publisher completion = Publishers.lift(publisher, this.operator); - List> signals = Streams.wrap(completion).materialize().toList().await(5, TimeUnit.SECONDS); + Publisher completion = publisher.lift(this.operator); + List> signals = Streams.from(completion).materialize().toList().get(); assertEquals(1, signals.size()); assertSame("Unexpected signal: " + signals.get(0), error, signals.get(0).getThrowable()); diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/XmlHandler.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/XmlHandler.java index 838d1ad877..dd1a4278de 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/XmlHandler.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/XmlHandler.java @@ -22,7 +22,7 @@ import javax.xml.bind.Unmarshaller; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.reactivestreams.Publisher; +import reactor.Mono; import reactor.io.buffer.Buffer; import reactor.rx.Streams; @@ -40,7 +40,7 @@ public class XmlHandler implements HttpHandler { private static final Log logger = LogFactory.getLog(XmlHandler.class); @Override - public Publisher handle(ServerHttpRequest request, + public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { try { JAXBContext jaxbContext = JAXBContext.newInstance(XmlHandlerIntegrationTests.Person.class); diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/ByteBufferDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/ByteBufferDecoderTests.java index 543fd60f68..a934afd749 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/ByteBufferDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/ByteBufferDecoderTests.java @@ -49,8 +49,8 @@ public class ByteBufferDecoderTests { ByteBuffer fooBuffer = Buffer.wrap("foo").byteBuffer(); ByteBuffer barBuffer = Buffer.wrap("bar").byteBuffer(); Stream source = Streams.just(fooBuffer, barBuffer); - List results = Streams.wrap(decoder.decode(source, - ResolvableType.forClassWithGenerics(Publisher.class, ByteBuffer.class), null)).toList().await(); + List results = Streams.from(decoder.decode(source, + ResolvableType.forClassWithGenerics(Publisher.class, ByteBuffer.class), null)).toList().get(); assertEquals(2, results.size()); assertEquals(fooBuffer, results.get(0)); assertEquals(barBuffer, results.get(1)); diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoderTests.java index adc55f2e45..8059a681ae 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoderTests.java @@ -48,8 +48,8 @@ public class JacksonJsonDecoderTests { @Test public void decode() throws InterruptedException { Stream source = Streams.just(Buffer.wrap("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}").byteBuffer()); - List results = Streams.wrap(decoder.decode(source, ResolvableType.forClass(Pojo.class), null)) - .toList().await(); + List results = Streams.from(decoder.decode(source, ResolvableType.forClass(Pojo.class), null)) + .toList().get(); assertEquals(1, results.size()); assertEquals("foofoo", ((Pojo) results.get(0)).getFoo()); } diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/Jaxb2DecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/Jaxb2DecoderTests.java index 110db95799..65b23e4ac3 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/Jaxb2DecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/Jaxb2DecoderTests.java @@ -49,8 +49,8 @@ public class Jaxb2DecoderTests { @Test public void decode() throws InterruptedException { Stream source = Streams.just(Buffer.wrap("barbarfoofoo").byteBuffer()); - List results = Streams.wrap(decoder.decode(source, ResolvableType.forClass(Pojo.class), null)) - .toList().await(); + List results = Streams.from(decoder.decode(source, ResolvableType.forClass(Pojo.class), null)) + .toList().get(); assertEquals(1, results.size()); assertEquals("foofoo", ((Pojo) results.get(0)).getFoo()); } diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/JsonObjectDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/JsonObjectDecoderTests.java index 915d9677da..ac3eea8399 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/JsonObjectDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/JsonObjectDecoderTests.java @@ -37,11 +37,11 @@ public class JsonObjectDecoderTests { public void decodeSingleChunkToJsonObject() throws InterruptedException { JsonObjectDecoder decoder = new JsonObjectDecoder(); Stream source = Streams.just(Buffer.wrap("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}").byteBuffer()); - List results = Streams.wrap(decoder.decode(source, null, null)).map(chunk -> { + List results = Streams.from(decoder.decode(source, null, null)).map(chunk -> { byte[] b = new byte[chunk.remaining()]; chunk.get(b); return new String(b, StandardCharsets.UTF_8); - }).toList().await(); + }).toList().get(); assertEquals(1, results.size()); assertEquals("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}", results.get(0)); } @@ -50,11 +50,11 @@ public class JsonObjectDecoderTests { public void decodeMultipleChunksToJsonObject() throws InterruptedException { JsonObjectDecoder decoder = new JsonObjectDecoder(); Stream source = Streams.just(Buffer.wrap("{\"foo\": \"foofoo\"").byteBuffer(), Buffer.wrap(", \"bar\": \"barbar\"}").byteBuffer()); - List results = Streams.wrap(decoder.decode(source, null, null)).map(chunk -> { + List results = Streams.from(decoder.decode(source, null, null)).map(chunk -> { byte[] b = new byte[chunk.remaining()]; chunk.get(b); return new String(b, StandardCharsets.UTF_8); - }).toList().await(); + }).toList().get(); assertEquals(1, results.size()); assertEquals("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}", results.get(0)); } @@ -63,11 +63,11 @@ public class JsonObjectDecoderTests { public void decodeSingleChunkToArray() throws InterruptedException { JsonObjectDecoder decoder = new JsonObjectDecoder(); Stream source = Streams.just(Buffer.wrap("[{\"foo\": \"foofoo\", \"bar\": \"barbar\"},{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}]").byteBuffer()); - List results = Streams.wrap(decoder.decode(source, null, null)).map(chunk -> { + List results = Streams.from(decoder.decode(source, null, null)).map(chunk -> { byte[] b = new byte[chunk.remaining()]; chunk.get(b); return new String(b, StandardCharsets.UTF_8); - }).toList().await(); + }).toList().get(); assertEquals(2, results.size()); assertEquals("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}", results.get(0)); assertEquals("{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}", results.get(1)); @@ -77,11 +77,11 @@ public class JsonObjectDecoderTests { public void decodeMultipleChunksToArray() throws InterruptedException { JsonObjectDecoder decoder = new JsonObjectDecoder(); Stream source = Streams.just(Buffer.wrap("[{\"foo\": \"foofoo\", \"bar\"").byteBuffer(), Buffer.wrap(": \"barbar\"},{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}]").byteBuffer()); - List results = Streams.wrap(decoder.decode(source, null, null)).map(chunk -> { + List results = Streams.from(decoder.decode(source, null, null)).map(chunk -> { byte[] b = new byte[chunk.remaining()]; chunk.get(b); return new String(b, StandardCharsets.UTF_8); - }).toList().await(); + }).toList().get(); assertEquals(2, results.size()); assertEquals("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}", results.get(0)); assertEquals("{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}", results.get(1)); diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/StringDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/StringDecoderTests.java index 90ab1978ec..4e22989b9b 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/StringDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/StringDecoderTests.java @@ -49,8 +49,8 @@ public class StringDecoderTests { @Test public void decode() throws InterruptedException { Stream source = Streams.just(Buffer.wrap("foo").byteBuffer(), Buffer.wrap("bar").byteBuffer()); - List results = Streams.wrap(decoder.decode(source, - ResolvableType.forClassWithGenerics(Publisher.class, String.class), null)).toList().await(); + List results = Streams.from(decoder.decode(source, + ResolvableType.forClassWithGenerics(Publisher.class, String.class), null)).toList().get(); assertEquals(2, results.size()); assertEquals("foo", results.get(0)); assertEquals("bar", results.get(1)); diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/ByteBufferEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/ByteBufferEncoderTests.java index 6c79d56bb4..168d150de9 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/ByteBufferEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/ByteBufferEncoderTests.java @@ -49,8 +49,8 @@ public class ByteBufferEncoderTests { ByteBuffer fooBuffer = Buffer.wrap("foo").byteBuffer(); ByteBuffer barBuffer = Buffer.wrap("bar").byteBuffer(); Stream source = Streams.just(fooBuffer, barBuffer); - List results = Streams.wrap(encoder.encode(source, - ResolvableType.forClassWithGenerics(Publisher.class, ByteBuffer.class), null)).toList().await(); + List results = Streams.from(encoder.encode(source, + ResolvableType.forClassWithGenerics(Publisher.class, ByteBuffer.class), null)).toList().get(); assertEquals(2, results.size()); assertEquals(fooBuffer, results.get(0)); assertEquals(barBuffer, results.get(1)); diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoderTests.java index 8193b938dc..5350148d76 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoderTests.java @@ -46,11 +46,11 @@ public class JacksonJsonEncoderTests { @Test public void write() throws InterruptedException { Stream source = Streams.just(new Pojo("foofoo", "barbar"), new Pojo("foofoofoo", "barbarbar")); - List results = Streams.wrap(encoder.encode(source, null, null)).map(chunk -> { + List results = Streams.from(encoder.encode(source, null, null)).map(chunk -> { byte[] b = new byte[chunk.remaining()]; chunk.get(b); return new String(b, StandardCharsets.UTF_8); - }).toList().await(); + }).toList().get(); assertEquals(2, results.size()); assertEquals("{\"foo\":\"foofoo\",\"bar\":\"barbar\"}", results.get(0)); assertEquals("{\"foo\":\"foofoofoo\",\"bar\":\"barbarbar\"}", results.get(1)); diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/Jaxb2EncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/Jaxb2EncoderTests.java index 1b6a8b7360..2536b2c027 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/Jaxb2EncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/Jaxb2EncoderTests.java @@ -47,11 +47,11 @@ public class Jaxb2EncoderTests { @Test public void encode() throws InterruptedException { Stream source = Streams.just(new Pojo("foofoo", "barbar"), new Pojo("foofoofoo", "barbarbar")); - List results = Streams.wrap(encoder.encode(source, null, null)).map(chunk -> { + List results = Streams.from(encoder.encode(source, null, null)).map(chunk -> { byte[] b = new byte[chunk.remaining()]; chunk.get(b); return new String(b, StandardCharsets.UTF_8); - }).toList().await(); + }).toList().get(); assertEquals(2, results.size()); assertEquals("barbarfoofoo", results.get(0)); assertEquals("barbarbarfoofoofoo", results.get(1)); diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/JsonObjectEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/JsonObjectEncoderTests.java index 5e56f044c5..a41f62ad98 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/JsonObjectEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/JsonObjectEncoderTests.java @@ -37,11 +37,11 @@ public class JsonObjectEncoderTests { public void encodeSingleElement() throws InterruptedException { JsonObjectEncoder encoder = new JsonObjectEncoder(); Stream source = Streams.just(Buffer.wrap("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}").byteBuffer()); - List results = Streams.wrap(encoder.encode(source, null, null)).map(chunk -> { + List results = Streams.from(encoder.encode(source, null, null)).map(chunk -> { byte[] b = new byte[chunk.remaining()]; chunk.get(b); return new String(b, StandardCharsets.UTF_8); - }).toList().await(); + }).toList().get(); String result = String.join("", results); assertEquals("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}", result); } @@ -52,11 +52,11 @@ public class JsonObjectEncoderTests { Stream source = Streams.just( Buffer.wrap("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}").byteBuffer(), Buffer.wrap("{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}").byteBuffer()); - List results = Streams.wrap(encoder.encode(source, null, null)).map(chunk -> { + List results = Streams.from(encoder.encode(source, null, null)).map(chunk -> { byte[] b = new byte[chunk.remaining()]; chunk.get(b); return new String(b, StandardCharsets.UTF_8); - }).toList().await(); + }).toList().get(); String result = String.join("", results); assertEquals("[{\"foo\": \"foofoo\", \"bar\": \"barbar\"},{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}]", result); } @@ -69,11 +69,11 @@ public class JsonObjectEncoderTests { Buffer.wrap("{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}").byteBuffer(), Buffer.wrap("{\"foo\": \"foofoofoofoo\", \"bar\": \"barbarbarbar\"}").byteBuffer() ); - List results = Streams.wrap(encoder.encode(source, null, null)).map(chunk -> { + List results = Streams.from(encoder.encode(source, null, null)).map(chunk -> { byte[] b = new byte[chunk.remaining()]; chunk.get(b); return new String(b, StandardCharsets.UTF_8); - }).toList().await(); + }).toList().get(); String result = String.join("", results); assertEquals("[{\"foo\": \"foofoo\", \"bar\": \"barbar\"},{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"},{\"foo\": \"foofoofoofoo\", \"bar\": \"barbarbarbar\"}]", result); } diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/StringEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/StringEncoderTests.java index 2c371f0391..3eef55cfe5 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/StringEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/StringEncoderTests.java @@ -45,12 +45,12 @@ public class StringEncoderTests { @Test public void write() throws InterruptedException { - List results = Streams.wrap(encoder.encode(Streams.just("foo"), null, null)) + List results = Streams.from(encoder.encode(Streams.just("foo"), null, null)) .map(chunk -> { byte[] b = new byte[chunk.remaining()]; chunk.get(b); return new String(b, StandardCharsets.UTF_8); - }).toList().await(); + }).toList().get(); assertEquals(1, results.size()); assertEquals("foo", results.get(0)); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java index 6ca079a6fc..5a43444de4 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java @@ -19,12 +19,11 @@ import java.net.URI; import java.nio.ByteBuffer; import java.util.Collections; import java.util.List; -import java.util.concurrent.TimeUnit; import org.junit.Before; import org.junit.Test; import org.reactivestreams.Publisher; -import reactor.Publishers; +import reactor.Mono; import reactor.rx.Streams; import reactor.rx.stream.Signal; @@ -154,7 +153,7 @@ public class DispatcherHandlerErrorTests { public void notAcceptable() throws Exception { this.request.setUri(new URI("/request-body")); this.request.getHeaders().setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); - this.request.setBody(Publishers.just(ByteBuffer.wrap("body".getBytes("UTF-8")))); + this.request.setBody(Mono.just(ByteBuffer.wrap("body".getBytes("UTF-8")))); Publisher publisher = this.dispatcherHandler.handle(this.request, this.response); Throwable ex = awaitErrorSignal(publisher); @@ -167,12 +166,14 @@ public class DispatcherHandlerErrorTests { @Test public void requestBodyError() throws Exception { this.request.setUri(new URI("/request-body")); - this.request.setBody(Publishers.error(EXCEPTION)); + this.request.setBody(Mono.error(EXCEPTION)); Publisher publisher = this.dispatcherHandler.handle(this.request, this.response); Throwable ex = awaitErrorSignal(publisher); + ex.printStackTrace(); assertSame(EXCEPTION, ex); + } @Test @@ -183,7 +184,7 @@ public class DispatcherHandlerErrorTests { HttpHandler httpHandler = new ErrorHandlingHttpHandler(this.dispatcherHandler, exceptionHandler); Publisher publisher = httpHandler.handle(this.request, this.response); - Streams.wrap(publisher).toList().await(5, TimeUnit.SECONDS); + Streams.from(publisher).toList().get(); assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, this.response.getStatus()); } @@ -196,13 +197,13 @@ public class DispatcherHandlerErrorTests { httpHandler = new ErrorHandlingHttpHandler(httpHandler, new ServerError500ExceptionHandler()); Publisher publisher = httpHandler.handle(this.request, this.response); - Streams.wrap(publisher).toList().await(5, TimeUnit.SECONDS); + Streams.from(publisher).toList().get(); assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, this.response.getStatus()); } private Throwable awaitErrorSignal(Publisher publisher) throws Exception { - Signal signal = Streams.wrap(publisher).materialize().toList().await(5, TimeUnit.SECONDS).get(0); + Signal signal = Streams.from(publisher).materialize().toList().get().get(0); assertEquals("Unexpected signal: " + signal, Signal.Type.ERROR, signal.getType()); return signal.getThrowable(); } @@ -245,7 +246,7 @@ public class DispatcherHandlerErrorTests { @RequestMapping("/error-signal") @ResponseBody public Publisher errorSignal() { - return Publishers.error(EXCEPTION); + return Mono.error(EXCEPTION); } @RequestMapping("/raise-exception") @@ -261,7 +262,7 @@ public class DispatcherHandlerErrorTests { @RequestMapping("/request-body") @ResponseBody public Publisher requestBody(@RequestBody Publisher body) { - return Publishers.map(body, s -> "hello " + s); + return Mono.from(body).map(s -> "hello " + s); } } @@ -271,16 +272,16 @@ public class DispatcherHandlerErrorTests { private static class ServerError500ExceptionHandler implements HttpExceptionHandler { @Override - public Publisher handle(ServerHttpRequest request, ServerHttpResponse response, Throwable ex) { + public Mono handle(ServerHttpRequest request, ServerHttpResponse response, Throwable ex) { response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR); - return Publishers.empty(); + return Mono.empty(); } } private static class TestHttpFilter implements HttpFilter { @Override - public Publisher filter(ServerHttpRequest req, ServerHttpResponse res, HttpFilterChain chain) { + public Mono filter(ServerHttpRequest req, ServerHttpResponse res, HttpFilterChain chain) { return chain.filter(req, res); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java index 076a589230..3744ecaa6e 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java @@ -60,7 +60,7 @@ public class ResponseStatusExceptionHandlerTests { Throwable ex = new ResponseStatusException(HttpStatus.BAD_REQUEST); Publisher publisher = this.handler.handle(this.request, this.response, ex); - Streams.wrap(publisher).toList().await(5, TimeUnit.SECONDS); + Streams.from(publisher).toList().get(); assertEquals(HttpStatus.BAD_REQUEST, this.response.getStatus()); } @@ -69,7 +69,7 @@ public class ResponseStatusExceptionHandlerTests { Throwable ex = new IllegalStateException(); Publisher publisher = this.handler.handle(this.request, this.response, ex); - List> signals = Streams.wrap(publisher).materialize().toList().await(5, TimeUnit.SECONDS); + List> signals = Streams.from(publisher).materialize().toList().get(); assertEquals(1, signals.size()); assertTrue(signals.get(0).hasError()); assertSame(ex, signals.get(0).getThrowable()); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandlerTests.java index 0a04a12eba..9e6381c672 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandlerTests.java @@ -28,7 +28,7 @@ import rx.Observable; import org.springframework.core.ResolvableType; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter; -import org.springframework.core.convert.support.ReactiveStreamsToReactorConverter; +import org.springframework.core.convert.support.ReactiveStreamsToReactorStreamConverter; import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter; import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerResult; @@ -75,7 +75,7 @@ public class SimpleHandlerResultHandlerTests { GenericConversionService conversionService = new GenericConversionService(); conversionService.addConverter(new ReactiveStreamsToCompletableFutureConverter()); - conversionService.addConverter(new ReactiveStreamsToReactorConverter()); + conversionService.addConverter(new ReactiveStreamsToReactorStreamConverter()); conversionService.addConverter(new ReactiveStreamsToRxJava1Converter()); SimpleHandlerResultHandler resultHandler = new SimpleHandlerResultHandler(conversionService); TestController controller = new TestController(); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java index da6cf3fc28..82690ca99d 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java @@ -22,8 +22,7 @@ import java.util.HashMap; import java.util.Map; import org.junit.Test; -import org.reactivestreams.Publisher; -import reactor.Publishers; +import reactor.Mono; import reactor.io.buffer.Buffer; import reactor.rx.Streams; @@ -136,7 +135,7 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler private static class FooHandler implements HttpHandler { @Override - public Publisher handle(ServerHttpRequest request, ServerHttpResponse response) { + public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { return response.setBody(Streams.just(Buffer.wrap("foo").byteBuffer())); } } @@ -144,7 +143,7 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler private static class BarHandler implements HttpHandler { @Override - public Publisher handle(ServerHttpRequest request, ServerHttpResponse response) { + public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { return response.setBody(Streams.just(Buffer.wrap("bar").byteBuffer())); } } @@ -152,9 +151,9 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler private static class HeaderSettingHandler implements HttpHandler { @Override - public Publisher handle(ServerHttpRequest request, ServerHttpResponse response) { + public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { response.getHeaders().add("foo", "bar"); - return Publishers.empty(); + return Mono.empty(); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java index c7b0bee5fd..65a31fbd3a 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java @@ -21,12 +21,12 @@ import java.net.URI; import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.concurrent.TimeUnit; import org.junit.Before; import org.junit.Test; import org.reactivestreams.Publisher; -import reactor.Publishers; +import reactor.Flux; +import reactor.Mono; import reactor.rx.Streams; import reactor.rx.stream.Signal; @@ -61,7 +61,7 @@ public class InvocableHandlerMethodTests { InvocableHandlerMethod hm = createHandlerMethod("noArgs"); Publisher publisher = hm.invokeForRequest(this.request); - List values = Streams.wrap(publisher).toList().await(5, TimeUnit.SECONDS); + List values = Streams.from(publisher).toList().get(); assertEquals(1, values.size()); assertEquals("success", values.get(0).getResult()); @@ -74,7 +74,7 @@ public class InvocableHandlerMethodTests { hm.setHandlerMethodArgumentResolvers(Collections.singletonList(new RequestParamArgumentResolver())); Publisher publisher = hm.invokeForRequest(this.request); - List values = Streams.wrap(publisher).toList().await(5, TimeUnit.SECONDS); + List values = Streams.from(publisher).toList().get(); assertEquals(1, values.size()); assertEquals("success:null", values.get(0).getResult()); @@ -83,10 +83,10 @@ public class InvocableHandlerMethodTests { @Test public void resolveArgToOneValue() throws Exception { InvocableHandlerMethod hm = createHandlerMethod("singleArg", String.class); - addResolver(hm, Publishers.just("value1")); + addResolver(hm, Mono.just("value1")); Publisher publisher = hm.invokeForRequest(this.request); - List values = Streams.wrap(publisher).toList().await(5, TimeUnit.SECONDS); + List values = Streams.from(publisher).toList().get(); assertEquals(1, values.size()); assertEquals("success:value1", values.get(0).getResult()); @@ -95,10 +95,10 @@ public class InvocableHandlerMethodTests { @Test public void resolveArgToMultipleValues() throws Exception { InvocableHandlerMethod hm = createHandlerMethod("singleArg", String.class); - addResolver(hm, Publishers.from(Arrays.asList("value1", "value2", "value3"))); + addResolver(hm, Flux.fromIterable(Arrays.asList("value1", "value2", "value3"))); Publisher publisher = hm.invokeForRequest(this.request); - List values = Streams.wrap(publisher).toList().await(5, TimeUnit.SECONDS); + List values = Streams.from(publisher).toList().get(); assertEquals(1, values.size()); assertEquals("success:value1", values.get(0).getResult()); @@ -137,7 +137,7 @@ public class InvocableHandlerMethodTests { @Test public void resolveArgumentWithErrorSignal() throws Exception { InvocableHandlerMethod hm = createHandlerMethod("singleArg", String.class); - addResolver(hm, Publishers.error(new IllegalStateException("boo"))); + addResolver(hm, Mono.error(new IllegalStateException("boo"))); Publisher publisher = hm.invokeForRequest(this.request); Throwable ex = awaitErrorSignal(publisher); @@ -151,7 +151,7 @@ public class InvocableHandlerMethodTests { @Test public void illegalArgumentExceptionIsWrappedWithHelpfulDetails() throws Exception { InvocableHandlerMethod hm = createHandlerMethod("singleArg", String.class); - addResolver(hm, Publishers.just(1)); + addResolver(hm, Mono.just(1)); Publisher publisher = hm.invokeForRequest(this.request); Throwable ex = awaitErrorSignal(publisher); @@ -183,12 +183,12 @@ public class InvocableHandlerMethodTests { private void addResolver(InvocableHandlerMethod handlerMethod, Publisher resolvedValue) { HandlerMethodArgumentResolver resolver = mock(HandlerMethodArgumentResolver.class); when(resolver.supportsParameter(any())).thenReturn(true); - when(resolver.resolveArgument(any(), any())).thenReturn(resolvedValue); + when(resolver.resolveArgument(any(), any())).thenReturn(Mono.from(resolvedValue)); handlerMethod.setHandlerMethodArgumentResolvers(Collections.singletonList(resolver)); } private Throwable awaitErrorSignal(Publisher publisher) throws Exception { - Signal signal = Streams.wrap(publisher).materialize().toList().await(5, TimeUnit.SECONDS).get(0); + Signal signal = Streams.from(publisher).materialize().toList().get().get(0); assertEquals("Unexpected signal: " + signal, Signal.Type.ERROR, signal.getType()); return signal.getThrowable(); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java index 4bbe2cd443..0af8d8eadc 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java @@ -18,7 +18,6 @@ package org.springframework.web.reactive.method.annotation; import java.net.URI; import java.util.List; -import java.util.concurrent.TimeUnit; import org.junit.Before; import org.junit.Test; @@ -78,7 +77,7 @@ public class RequestMappingHandlerMappingTests { private HandlerMethod toHandlerMethod(Publisher handlerPublisher) throws InterruptedException { assertNotNull(handlerPublisher); - List handlerList = Streams.wrap(handlerPublisher).toList().await(5, TimeUnit.SECONDS); + List handlerList = Streams.from(handlerPublisher).toList().get(); assertEquals(1, handlerList.size()); return (HandlerMethod) handlerList.get(0); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java index 50ac1bd873..ea6cc95f34 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java @@ -25,10 +25,9 @@ import java.util.concurrent.CompletableFuture; import org.junit.Test; import org.reactivestreams.Publisher; -import reactor.Publishers; +import reactor.Mono; import reactor.io.buffer.Buffer; import reactor.rx.Promise; -import reactor.rx.Promises; import reactor.rx.Stream; import reactor.rx.Streams; import rx.Observable; @@ -46,7 +45,7 @@ import org.springframework.core.codec.support.StringEncoder; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter; -import org.springframework.core.convert.support.ReactiveStreamsToReactorConverter; +import org.springframework.core.convert.support.ReactiveStreamsToReactorStreamConverter; import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -321,7 +320,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati // TODO: test failures with DefaultConversionService GenericConversionService service = new GenericConversionService(); service.addConverter(new ReactiveStreamsToCompletableFutureConverter()); - service.addConverter(new ReactiveStreamsToReactorConverter()); + service.addConverter(new ReactiveStreamsToReactorStreamConverter()); service.addConverter(new ReactiveStreamsToRxJava1Converter()); return service; } @@ -398,7 +397,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @RequestMapping("/promise") @ResponseBody public Promise promiseResponseBody() { - return Promises.success(new Person("Robert")); + return Promise.success(new Person("Robert")); } @RequestMapping("/list") @@ -428,7 +427,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @RequestMapping("/publisher-capitalize") @ResponseBody public Publisher publisherCapitalize(@RequestBody Publisher persons) { - return Streams.wrap(persons).map(person -> { + return Streams.from(persons).map(person -> { person.setName(person.getName().toUpperCase()); return person; }); @@ -482,20 +481,20 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @RequestMapping("/promise-capitalize") @ResponseBody public Promise promiseCapitalize(@RequestBody Promise personFuture) { - return personFuture.map(person -> { + return Streams.from(personFuture.map(person -> { person.setName(person.getName().toUpperCase()); return person; - }); + })).promise(); } @RequestMapping("/publisher-create") public Publisher publisherCreate(@RequestBody Publisher personStream) { - return Streams.wrap(personStream).toList().onSuccess(persons::addAll).after(); + return Streams.from(personStream).toList().doOnSuccess(persons::addAll).after(); } @RequestMapping("/stream-create") - public Promise streamCreate(@RequestBody Stream personStream) { - return personStream.toList().onSuccess(persons::addAll).after(); + public Publisher streamCreate(@RequestBody Stream personStream) { + return Streams.from(personStream.toList().doOnSuccess(persons::addAll).after()).promise(); } @RequestMapping("/observable-create") @@ -512,7 +511,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @RequestMapping("/error-signal") @ResponseBody public Publisher handleWithError() { - return Publishers.error(new IllegalStateException("Boo")); + return Mono.error(new IllegalStateException("Boo")); } @ExceptionHandler From 327c4204096c9f2e7f3f17801f11393fb9d5e9e6 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Thu, 7 Jan 2016 14:51:43 +0100 Subject: [PATCH 140/498] Remove mandatory dependency on Reactor Stream with JarJar --- spring-web-reactive/build.gradle | 48 ++++++++++++++++--- .../util/ByteBufferPublisherInputStream.java | 6 ++- .../http/server/reactive/XmlHandler.java | 3 +- 3 files changed, 46 insertions(+), 11 deletions(-) diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index 117cab0e2a..704f8abdfa 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -12,8 +12,45 @@ apply plugin: 'propdeps' apply plugin: 'propdeps-idea' apply plugin: 'propdeps-maven' +ext { + springVersion = '4.2.3.RELEASE' + reactorVersion = '2.5.0.BUILD-SNAPSHOT' + tomcatVersion = '8.0.28' + jettyVersion = '9.3.5.v20151012' +} + +configurations { + jarjar + reactorstream +} + +task reactorstreamRepackJar(type: Jar) { repackJar -> + repackJar.baseName = "spring-reactive-reactorstream-repack" + repackJar.version = reactorVersion + + doLast() { + project.ant { + taskdef name: "jarjar", classname: "com.tonicsystems.jarjar.JarJarTask", + classpath: configurations.jarjar.asPath + jarjar(destfile: repackJar.archivePath) { + configurations.reactorstream.each { originalJar -> + zipfileset(src: originalJar) + } + // repackage reactor. => org.springframework.reactor + rule(pattern: "reactor.rx.**", result: "org.springframework.reactor.rx.@1") + } + } + } +} + + jar { baseName = 'spring-reactive' + + dependsOn reactorstreamRepackJar + from(zipTree(reactorstreamRepackJar.archivePath)) { + include "reactor/rx/subscriber/BlockingQueueSubscriber.java" + } } group = 'org.springframework.reactive' @@ -30,19 +67,14 @@ configurations.all { resolutionStrategy.cacheChangingModulesFor 0, 'seconds' } -ext { - springVersion = '4.2.3.RELEASE' - reactorVersion = '2.5.0.BUILD-SNAPSHOT' - tomcatVersion = '8.0.28' - jettyVersion = '9.3.5.v20151012' -} - dependencies { compile "org.springframework:spring-core:${springVersion}" compile "org.springframework:spring-web:${springVersion}" compile "org.reactivestreams:reactive-streams:1.0.0" compile "io.projectreactor:reactor-core:${reactorVersion}" compile "commons-logging:commons-logging:1.2" + reactorstream("io.projectreactor:reactor-stream:${reactorVersion}@jar") + compile(files(reactorstreamRepackJar)) optional 'io.reactivex:rxjava:1.1.0' optional "io.reactivex:rxnetty-http:0.5.0-SNAPSHOT" @@ -57,6 +89,8 @@ dependencies { provided "javax.servlet:javax.servlet-api:3.1.0" + jarjar("com.googlecode.jarjar:jarjar:1.3") + testCompile "junit:junit:4.12" testCompile "org.springframework:spring-test:${springVersion}" testCompile "org.slf4j:slf4j-jcl:1.7.12" diff --git a/spring-web-reactive/src/main/java/org/springframework/util/ByteBufferPublisherInputStream.java b/spring-web-reactive/src/main/java/org/springframework/util/ByteBufferPublisherInputStream.java index 10aa4c23d2..90d6b422ff 100644 --- a/spring-web-reactive/src/main/java/org/springframework/util/ByteBufferPublisherInputStream.java +++ b/spring-web-reactive/src/main/java/org/springframework/util/ByteBufferPublisherInputStream.java @@ -19,11 +19,13 @@ package org.springframework.util; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; +import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; -import reactor.rx.Streams; + +import org.springframework.reactor.rx.subscriber.BlockingQueueSubscriber; /** * {@code InputStream} implementation based on a byte array {@link Publisher}. @@ -60,7 +62,7 @@ public class ByteBufferPublisherInputStream extends InputStream { public ByteBufferPublisherInputStream(Publisher publisher, int requestSize) { Assert.notNull(publisher, "'publisher' must not be null"); - this.queue = Streams.from(publisher).toBlockingQueue(requestSize); + this.queue = new BlockingQueueSubscriber<>(publisher, null, new ArrayBlockingQueue<>(requestSize), false, requestSize); } diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/XmlHandler.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/XmlHandler.java index dd1a4278de..39e7be5c9a 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/XmlHandler.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/XmlHandler.java @@ -24,7 +24,6 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import reactor.Mono; import reactor.io.buffer.Buffer; -import reactor.rx.Streams; import org.springframework.http.MediaType; import org.springframework.util.BufferOutputStream; @@ -73,7 +72,7 @@ public class XmlHandler implements HttpHandler { bos.close(); buffer.flip(); - return response.setBody(Streams.just(buffer.byteBuffer())); + return response.setBody(Mono.just(buffer.byteBuffer())); } catch (Exception ex) { logger.error(ex, ex); From 318092cfd04023e3fefea268d7031d28aa90a615 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Thu, 7 Jan 2016 15:58:46 +0100 Subject: [PATCH 141/498] Use Mono#defaultIfEmpty instead of Flux#defaultIfEmpty --- .../web/reactive/method/InvocableHandlerMethod.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java index a1c3babd4a..116036fd67 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java @@ -132,8 +132,7 @@ public class InvocableHandlerMethod extends HandlerMethod { .orElseThrow(() -> getArgError("No resolver for ", parameter, null)); try { return resolver.resolveArgument(parameter, request) - // TODO Add a defaultIfEmpty alias to Mono to avoid conversion to Flux - .flux().defaultIfEmpty(NO_VALUE).next() + .defaultIfEmpty(NO_VALUE) .otherwise(ex -> Mono.error(getArgError("Error resolving ", parameter, ex))); } catch (Exception ex) { From c3a8bf4d171b01ab1e937c7e39adb64ee2b5b662 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 7 Jan 2016 15:23:55 -0500 Subject: [PATCH 142/498] Revert "Remove mandatory dependency on Reactor Stream" This reverts commit d5e6f70483d4a6c8af3cc5e97e52a54e98199169. --- spring-web-reactive/build.gradle | 48 +++---------------- .../util/ByteBufferPublisherInputStream.java | 6 +-- .../http/server/reactive/XmlHandler.java | 3 +- 3 files changed, 11 insertions(+), 46 deletions(-) diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index 704f8abdfa..117cab0e2a 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -12,45 +12,8 @@ apply plugin: 'propdeps' apply plugin: 'propdeps-idea' apply plugin: 'propdeps-maven' -ext { - springVersion = '4.2.3.RELEASE' - reactorVersion = '2.5.0.BUILD-SNAPSHOT' - tomcatVersion = '8.0.28' - jettyVersion = '9.3.5.v20151012' -} - -configurations { - jarjar - reactorstream -} - -task reactorstreamRepackJar(type: Jar) { repackJar -> - repackJar.baseName = "spring-reactive-reactorstream-repack" - repackJar.version = reactorVersion - - doLast() { - project.ant { - taskdef name: "jarjar", classname: "com.tonicsystems.jarjar.JarJarTask", - classpath: configurations.jarjar.asPath - jarjar(destfile: repackJar.archivePath) { - configurations.reactorstream.each { originalJar -> - zipfileset(src: originalJar) - } - // repackage reactor. => org.springframework.reactor - rule(pattern: "reactor.rx.**", result: "org.springframework.reactor.rx.@1") - } - } - } -} - - jar { baseName = 'spring-reactive' - - dependsOn reactorstreamRepackJar - from(zipTree(reactorstreamRepackJar.archivePath)) { - include "reactor/rx/subscriber/BlockingQueueSubscriber.java" - } } group = 'org.springframework.reactive' @@ -67,14 +30,19 @@ configurations.all { resolutionStrategy.cacheChangingModulesFor 0, 'seconds' } +ext { + springVersion = '4.2.3.RELEASE' + reactorVersion = '2.5.0.BUILD-SNAPSHOT' + tomcatVersion = '8.0.28' + jettyVersion = '9.3.5.v20151012' +} + dependencies { compile "org.springframework:spring-core:${springVersion}" compile "org.springframework:spring-web:${springVersion}" compile "org.reactivestreams:reactive-streams:1.0.0" compile "io.projectreactor:reactor-core:${reactorVersion}" compile "commons-logging:commons-logging:1.2" - reactorstream("io.projectreactor:reactor-stream:${reactorVersion}@jar") - compile(files(reactorstreamRepackJar)) optional 'io.reactivex:rxjava:1.1.0' optional "io.reactivex:rxnetty-http:0.5.0-SNAPSHOT" @@ -89,8 +57,6 @@ dependencies { provided "javax.servlet:javax.servlet-api:3.1.0" - jarjar("com.googlecode.jarjar:jarjar:1.3") - testCompile "junit:junit:4.12" testCompile "org.springframework:spring-test:${springVersion}" testCompile "org.slf4j:slf4j-jcl:1.7.12" diff --git a/spring-web-reactive/src/main/java/org/springframework/util/ByteBufferPublisherInputStream.java b/spring-web-reactive/src/main/java/org/springframework/util/ByteBufferPublisherInputStream.java index 90d6b422ff..10aa4c23d2 100644 --- a/spring-web-reactive/src/main/java/org/springframework/util/ByteBufferPublisherInputStream.java +++ b/spring-web-reactive/src/main/java/org/springframework/util/ByteBufferPublisherInputStream.java @@ -19,13 +19,11 @@ package org.springframework.util; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; -import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; - -import org.springframework.reactor.rx.subscriber.BlockingQueueSubscriber; +import reactor.rx.Streams; /** * {@code InputStream} implementation based on a byte array {@link Publisher}. @@ -62,7 +60,7 @@ public class ByteBufferPublisherInputStream extends InputStream { public ByteBufferPublisherInputStream(Publisher publisher, int requestSize) { Assert.notNull(publisher, "'publisher' must not be null"); - this.queue = new BlockingQueueSubscriber<>(publisher, null, new ArrayBlockingQueue<>(requestSize), false, requestSize); + this.queue = Streams.from(publisher).toBlockingQueue(requestSize); } diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/XmlHandler.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/XmlHandler.java index 39e7be5c9a..dd1a4278de 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/XmlHandler.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/XmlHandler.java @@ -24,6 +24,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import reactor.Mono; import reactor.io.buffer.Buffer; +import reactor.rx.Streams; import org.springframework.http.MediaType; import org.springframework.util.BufferOutputStream; @@ -72,7 +73,7 @@ public class XmlHandler implements HttpHandler { bos.close(); buffer.flip(); - return response.setBody(Mono.just(buffer.byteBuffer())); + return response.setBody(Streams.just(buffer.byteBuffer())); } catch (Exception ex) { logger.error(ex, ex); From a712f436543c82728f9859ed3b09f45184c4556e Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 7 Jan 2016 15:26:11 -0500 Subject: [PATCH 143/498] Polish --- .../springframework/util/ByteBufferPublisherInputStream.java | 4 ++-- .../org/springframework/http/server/reactive/XmlHandler.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/util/ByteBufferPublisherInputStream.java b/spring-web-reactive/src/main/java/org/springframework/util/ByteBufferPublisherInputStream.java index 10aa4c23d2..92165fac16 100644 --- a/spring-web-reactive/src/main/java/org/springframework/util/ByteBufferPublisherInputStream.java +++ b/spring-web-reactive/src/main/java/org/springframework/util/ByteBufferPublisherInputStream.java @@ -23,7 +23,7 @@ import java.util.concurrent.BlockingQueue; import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; -import reactor.rx.Streams; +import reactor.rx.Stream; /** * {@code InputStream} implementation based on a byte array {@link Publisher}. @@ -60,7 +60,7 @@ public class ByteBufferPublisherInputStream extends InputStream { public ByteBufferPublisherInputStream(Publisher publisher, int requestSize) { Assert.notNull(publisher, "'publisher' must not be null"); - this.queue = Streams.from(publisher).toBlockingQueue(requestSize); + this.queue = Stream.from(publisher).toBlockingQueue(requestSize); } diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/XmlHandler.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/XmlHandler.java index dd1a4278de..90c8a75998 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/XmlHandler.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/XmlHandler.java @@ -22,9 +22,9 @@ import javax.xml.bind.Unmarshaller; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import reactor.Flux; import reactor.Mono; import reactor.io.buffer.Buffer; -import reactor.rx.Streams; import org.springframework.http.MediaType; import org.springframework.util.BufferOutputStream; @@ -73,7 +73,7 @@ public class XmlHandler implements HttpHandler { bos.close(); buffer.flip(); - return response.setBody(Streams.just(buffer.byteBuffer())); + return response.setBody(Flux.just(buffer.byteBuffer())); } catch (Exception ex) { logger.error(ex, ex); From 9a1492e4010bc322c2fb00174e13da79d1c95da3 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 7 Jan 2016 17:09:49 -0500 Subject: [PATCH 144/498] Polish --- .../reactive/ErrorHandlingHttpHandler.java | 16 ++-- .../server/reactive/HttpExceptionHandler.java | 2 +- .../http/server/reactive/HttpFilter.java | 2 +- .../http/server/reactive/HttpFilterChain.java | 2 +- .../http/server/reactive/HttpHandler.java | 2 +- .../web/reactive/DispatcherHandler.java | 15 ++-- .../web/reactive/HandlerAdapter.java | 2 +- .../web/reactive/HandlerMapping.java | 3 +- .../web/reactive/HandlerResultHandler.java | 13 +-- .../reactive/handler/HttpHandlerAdapter.java | 4 +- .../handler/SimpleHandlerResultHandler.java | 3 +- .../method/HandlerMethodArgumentResolver.java | 10 +-- .../method/InvocableHandlerMethod.java | 83 +++++++++---------- .../RequestBodyArgumentResolver.java | 8 +- .../RequestMappingHandlerAdapter.java | 16 ++-- 15 files changed, 81 insertions(+), 100 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ErrorHandlingHttpHandler.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ErrorHandlingHttpHandler.java index 92e2fa2c9d..09cf6a6413 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ErrorHandlingHttpHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ErrorHandlingHttpHandler.java @@ -44,23 +44,23 @@ public class ErrorHandlingHttpHandler extends HttpHandlerDecorator { @Override public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { - Mono publisher; + Mono mono; try { - publisher = getDelegate().handle(request, response); + mono = getDelegate().handle(request, response); } catch (Throwable ex) { - publisher = Mono.error(ex); + mono = Mono.error(ex); } for (HttpExceptionHandler handler : this.exceptionHandlers) { - publisher = applyExceptionHandler(publisher, handler, request, response); + mono = applyExceptionHandler(mono, handler, request, response); } - return publisher; + return mono; } - private static Mono applyExceptionHandler(Mono publisher, - HttpExceptionHandler handler, ServerHttpRequest request, ServerHttpResponse response) { + private static Mono applyExceptionHandler(Mono mono, HttpExceptionHandler handler, + ServerHttpRequest request, ServerHttpResponse response) { - return publisher.flux().onErrorResumeWith(ex -> handler.handle(request, response, ex)).after(); + return mono.otherwise(ex -> handler.handle(request, response, ex)).after(); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpExceptionHandler.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpExceptionHandler.java index 5258d02564..d982c115a7 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpExceptionHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpExceptionHandler.java @@ -36,7 +36,7 @@ public interface HttpExceptionHandler { * @param request the current request * @param response the current response * @param ex the exception to handle - * @return Publisher to indicate when exception handling is complete. + * @return {@code Mono} to indicate when exception handling is complete. */ Mono handle(ServerHttpRequest request, ServerHttpResponse response, Throwable ex); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpFilter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpFilter.java index b53fb8639b..b21b1c2b0f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpFilter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpFilter.java @@ -39,7 +39,7 @@ public interface HttpFilter { * @param request current HTTP request. * @param response current HTTP response. * @param chain provides a way to delegate to the next HttpFilter. - * @return Publisher to indicate when request processing is complete. + * @return {@code Mono} to indicate when request processing is complete. */ Mono filter(ServerHttpRequest request, ServerHttpResponse response, HttpFilterChain chain); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpFilterChain.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpFilterChain.java index 7650014cad..0bb70708bc 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpFilterChain.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpFilterChain.java @@ -29,7 +29,7 @@ public interface HttpFilterChain { * * @param request current HTTP request. * @param response current HTTP response. - * @return Publisher to indicate when request handling is complete. + * @return {@code Mono} to indicate when request handling is complete. */ Mono filter(ServerHttpRequest request, ServerHttpResponse response); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpHandler.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpHandler.java index 3d90ca5289..328cd89185 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpHandler.java @@ -33,7 +33,7 @@ public interface HttpHandler { * * @param request current HTTP request. * @param response current HTTP response. - * @return Publisher to indicate when request handling is complete. + * @return {@code Mono} to indicate when request handling is complete. */ Mono handle(ServerHttpRequest request, ServerHttpResponse response); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java index 93e1aa3031..8b5c676d95 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java @@ -121,14 +121,13 @@ public class DispatcherHandler implements HttpHandler, ApplicationContextAware { .next() .then(handler -> getHandlerAdapter(handler).handle(request, response, handler)) .then(result -> { - Mono publisher = (result.hasError() ? Mono.error(result.getError()) : - getResultHandler(result).handleResult(request, response, result)); + Mono mono = (result.hasError() ? Mono.error(result.getError()) : + handleResult(request, response, result)); if (result.hasExceptionMapper()) { - return publisher - .otherwise(ex -> result.getExceptionMapper().apply(ex) - .then(errorResult -> getResultHandler(errorResult).handleResult(request, response, errorResult))); + return mono.otherwise(ex -> result.getExceptionMapper().apply(ex) + .then(exResult -> handleResult(request, response, exResult))); } - return publisher; + return mono; }) .otherwise(ex -> Mono.error(this.errorMapper.apply(ex))); } @@ -142,6 +141,10 @@ public class DispatcherHandler implements HttpHandler, ApplicationContextAware { throw new IllegalStateException("No HandlerAdapter: " + handler); } + protected Mono handleResult(ServerHttpRequest request, ServerHttpResponse response, HandlerResult result) { + return getResultHandler(result).handleResult(request, response, result); + } + protected HandlerResultHandler getResultHandler(HandlerResult handlerResult) { for (HandlerResultHandler resultHandler : resultHandlers) { if (resultHandler.supports(handlerResult)) { diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerAdapter.java index c6f5c3541c..b66676d537 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerAdapter.java @@ -53,7 +53,7 @@ public interface HandlerAdapter { * @param handler handler to use. This object must have previously been passed * to the {@code supports} method of this interface, which must have * returned {@code true}. - * @return A {@link Publisher} object that produces a single {@link HandlerResult} element + * @return A {@link Mono} that emits a single {@link HandlerResult} element */ Mono handle(ServerHttpRequest request, ServerHttpResponse response, Object handler); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerMapping.java index ae276d6230..b8a164421a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerMapping.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerMapping.java @@ -32,7 +32,8 @@ public interface HandlerMapping { /** * Return a handler for this request. * @param request current HTTP request - * @return A {@link Mono} object that produces a single handler element + * @return A {@link Mono} that emits one value or none in case the request + * cannot be resolved to a handler. */ Mono getHandler(ServerHttpRequest request); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResultHandler.java index e121bdff98..fe55814ac3 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResultHandler.java @@ -30,8 +30,7 @@ import org.springframework.http.server.reactive.ServerHttpResponse; public interface HandlerResultHandler { /** - * Given a handler instance, return whether or not this {@code HandlerResultHandler} - * can support it. + * Whether this handler supports the given {@link HandlerResult}. * * @param result result object to check * @return whether or not this object can use the given result @@ -39,14 +38,10 @@ public interface HandlerResultHandler { boolean supports(HandlerResult result); /** - * Process the given result in an asynchronous non blocking way, by eventually modifying - * response headers, or writing some data stream into the response. - * Implementations should not throw exceptions but signal them via the returned - * {@code Mono}. + * Process the given result modifying response headers and/or writing data + * to the response. * - * @return A {@code Mono} used to signal the demand, and receive a notification - * when the handling is complete (success or error) including the flush of the data on the - * network. + * @return {@code Mono} to indicate when request handling is complete. */ Mono handleResult(ServerHttpRequest request, ServerHttpResponse response, HandlerResult result); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/HttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/HttpHandlerAdapter.java index 853f522f75..13694ce1d7 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/HttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/HttpHandlerAdapter.java @@ -50,9 +50,7 @@ public class HttpHandlerAdapter implements HandlerAdapter { } @Override - public Mono handle(ServerHttpRequest request, - ServerHttpResponse response, Object handler) { - + public Mono handle(ServerHttpRequest request, ServerHttpResponse response, Object handler) { HttpHandler httpHandler = (HttpHandler)handler; Mono completion = httpHandler.handle(request, response); return Mono.just(new HandlerResult(httpHandler, completion, PUBLISHER_VOID)); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java index 0e27adc704..ba03013652 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java @@ -69,7 +69,8 @@ public class SimpleHandlerResultHandler implements Ordered, HandlerResultHandler private boolean isConvertibleToPublisher(ResolvableType type) { return Publisher.class.isAssignableFrom(type.getRawClass()) || - ((this.conversionService != null) && this.conversionService.canConvert(type.getRawClass(), Publisher.class)); + ((this.conversionService != null) && + this.conversionService.canConvert(type.getRawClass(), Publisher.class)); } @SuppressWarnings("unchecked") diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/HandlerMethodArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/HandlerMethodArgumentResolver.java index f7c9c6e190..b4e7fd68e7 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/HandlerMethodArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/HandlerMethodArgumentResolver.java @@ -31,13 +31,9 @@ public interface HandlerMethodArgumentResolver { boolean supportsParameter(MethodParameter parameter); /** - * The returned Publisher is expected to produce a single value -- i.e. the - * value to use to invoke the handler method. Any additional values will be - * ignored. - * - *

    The publisher may also produce zero values if the argument does not - * resolve to any value which will result in passing {@code null} as the - * argument value. + * The returned {@link Mono} may produce one or zero values if the argument + * does not resolve to any value, which will result in {@code null} passed + * as the argument value. */ Mono resolveArgument(MethodParameter parameter, ServerHttpRequest request); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java index 116036fd67..0aaa7a6757 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java @@ -25,9 +25,7 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; -import reactor.Flux; import reactor.Mono; -import reactor.fn.tuple.Tuple; import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.GenericTypeResolver; @@ -46,7 +44,7 @@ import org.springframework.web.reactive.HandlerResult; */ public class InvocableHandlerMethod extends HandlerMethod { - public static final Mono NO_ARGS = Mono.just(new Object[0]); + private static final Mono NO_ARGS = Mono.just(new Object[0]); private final static Object NO_VALUE = new Object(); @@ -85,19 +83,7 @@ public class InvocableHandlerMethod extends HandlerMethod { * never throws an exception. */ public Mono invokeForRequest(ServerHttpRequest request, Object... providedArgs) { - - Mono argsPublisher = NO_ARGS; - try { - if (!ObjectUtils.isEmpty(getMethodParameters())) { - List> publishers = resolveArguments(request, providedArgs); - argsPublisher = Flux.zip(publishers, this::initArgs).next(); - } - } - catch (Throwable ex) { - return Mono.error(ex); - } - - return Flux.from(argsPublisher).concatMap(args -> { + return resolveArguments(request, providedArgs).then(args -> { try { Object value = doInvoke(args); ResolvableType type = ResolvableType.forMethodParameter(getReturnType()); @@ -111,35 +97,46 @@ public class InvocableHandlerMethod extends HandlerMethod { String s = getInvocationErrorMessage(args); return Mono.error(new IllegalStateException(s)); } - }).next(); + }); } - private List> resolveArguments(ServerHttpRequest request, Object... providedArgs) { - return Stream.of(getMethodParameters()) - .map(parameter -> { - parameter.initParameterNameDiscovery(this.parameterNameDiscoverer); - GenericTypeResolver.resolveParameterType(parameter, getBean().getClass()); - if (!ObjectUtils.isEmpty(providedArgs)) { - for (Object providedArg : providedArgs) { - if (parameter.getParameterType().isInstance(providedArg)) { - return Mono.just(providedArg); + private Mono resolveArguments(ServerHttpRequest request, Object... providedArgs) { + if (ObjectUtils.isEmpty(getMethodParameters())) { + return NO_ARGS; + } + try { + List> monos = Stream.of(getMethodParameters()) + .map(param -> { + param.initParameterNameDiscovery(this.parameterNameDiscoverer); + GenericTypeResolver.resolveParameterType(param, getBean().getClass()); + if (!ObjectUtils.isEmpty(providedArgs)) { + for (Object providedArg : providedArgs) { + if (param.getParameterType().isInstance(providedArg)) { + return Mono.just(providedArg); + } } } - } - HandlerMethodArgumentResolver resolver = this.resolvers.stream() - .filter(r -> r.supportsParameter(parameter)) - .findFirst() - .orElseThrow(() -> getArgError("No resolver for ", parameter, null)); - try { - return resolver.resolveArgument(parameter, request) - .defaultIfEmpty(NO_VALUE) - .otherwise(ex -> Mono.error(getArgError("Error resolving ", parameter, ex))); - } - catch (Exception ex) { - throw getArgError("Error resolving ", parameter, ex); - } - }) - .collect(Collectors.toList()); + HandlerMethodArgumentResolver resolver = this.resolvers.stream() + .filter(r -> r.supportsParameter(param)) + .findFirst() + .orElseThrow(() -> getArgError("No resolver for ", param, null)); + try { + return resolver.resolveArgument(param, request) + .defaultIfEmpty(NO_VALUE) + .otherwise(ex -> Mono.error(getArgError("Error resolving ", param, ex))); + } + catch (Exception ex) { + throw getArgError("Error resolving ", param, ex); + } + }) + .collect(Collectors.toList()); + + return Mono.when(monos).map(args -> + Stream.of(args.toArray()).map(o -> o != NO_VALUE ? o : null).toArray()); + } + catch (Throwable ex) { + return Mono.error(ex); + } } private IllegalStateException getArgError(String message, MethodParameter param, Throwable cause) { @@ -173,8 +170,4 @@ public class InvocableHandlerMethod extends HandlerMethod { "on method [" + getBridgedMethod().toGenericString() + "]"; } - private Object[] initArgs(Tuple tuple) { - return Stream.of(tuple.toArray()).map(o -> o != NO_VALUE ? o : null).toArray(); - } - } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestBodyArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestBodyArgumentResolver.java index 2bb4cdafa1..9302e333a6 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestBodyArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestBodyArgumentResolver.java @@ -65,19 +65,19 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve } ResolvableType type = ResolvableType.forMethodParameter(parameter); Flux body = request.getBody(); - Flux elementStream = body; + Flux elementFlux = body; ResolvableType elementType = type.hasGenerics() ? type.getGeneric(0) : type; Decoder decoder = resolveDecoder(elementType, mediaType); if (decoder != null) { - elementStream = decoder.decode(body, elementType, mediaType); + elementFlux = decoder.decode(body, elementType, mediaType); } if (this.conversionService.canConvert(Publisher.class, type.getRawClass())) { - return Mono.just(this.conversionService.convert(elementStream, type.getRawClass())); + return Mono.just(this.conversionService.convert(elementFlux, type.getRawClass())); } - return (Mono)Mono.from(elementStream); + return elementFlux.next().map(o -> o); } private Decoder resolveDecoder(ResolvableType type, MediaType mediaType, Object... hints) { diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java index d1409e4e93..a6e67927a4 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java @@ -25,7 +25,6 @@ import java.util.concurrent.ConcurrentHashMap; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import reactor.Flux; import reactor.Mono; import org.springframework.beans.factory.InitializingBean; @@ -106,23 +105,18 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Initializin } @Override - public Mono handle(ServerHttpRequest request, - ServerHttpResponse response, Object handler) { + public Mono handle(ServerHttpRequest request, ServerHttpResponse response, + Object handler) { HandlerMethod handlerMethod = (HandlerMethod) handler; InvocableHandlerMethod invocable = new InvocableHandlerMethod(handlerMethod); invocable.setHandlerMethodArgumentResolvers(this.argumentResolvers); - Flux publisher = invocable.invokeForRequest(request).flux(); - publisher = publisher.onErrorResumeWith(ex -> Mono.just(new HandlerResult(handler, ex))); - - - publisher = publisher.map( - result -> result.setExceptionMapper( + return invocable.invokeForRequest(request) + .otherwise(ex -> Mono.just(new HandlerResult(handler, ex))) + .map(result -> result.setExceptionMapper( ex -> mapException(ex, handlerMethod, request, response))); - - return publisher.next(); } private Mono mapException(Throwable ex, HandlerMethod handlerMethod, From 70b8848492429399b76b7bb1f7ace64ac35c1d54 Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Thu, 7 Jan 2016 22:28:35 +0000 Subject: [PATCH 145/498] fix Streams to Stream in test --- .../RequestMappingIntegrationTests.java | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java index ea6cc95f34..5c82eadaf4 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java @@ -29,7 +29,6 @@ import reactor.Mono; import reactor.io.buffer.Buffer; import reactor.rx.Promise; import reactor.rx.Stream; -import reactor.rx.Streams; import rx.Observable; import rx.Single; @@ -359,7 +358,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @RequestMapping("/param") @ResponseBody public Publisher handleWithParam(@RequestParam String name) { - return Streams.just("Hello ", name, "!"); + return Stream.just("Hello ", name, "!"); } @RequestMapping("/person") @@ -378,7 +377,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @ResponseBody public Publisher rawResponseBody() { JacksonJsonEncoder encoder = new JacksonJsonEncoder(); - return encoder.encode(Streams.just(new Person("Robert")), + return encoder.encode(Stream.just(new Person("Robert")), ResolvableType.forClass(Person.class), MediaType.APPLICATION_JSON); } @@ -409,7 +408,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @RequestMapping("/publisher") @ResponseBody public Publisher publisherResponseBody() { - return Streams.just(new Person("Robert"), new Person("Marie")); + return Stream.just(new Person("Robert"), new Person("Marie")); } @RequestMapping("/observable") @@ -421,13 +420,13 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @RequestMapping("/stream") @ResponseBody public Stream reactorStreamResponseBody() { - return Streams.just(new Person("Robert"), new Person("Marie")); + return Stream.just(new Person("Robert"), new Person("Marie")); } @RequestMapping("/publisher-capitalize") @ResponseBody public Publisher publisherCapitalize(@RequestBody Publisher persons) { - return Streams.from(persons).map(person -> { + return Stream.from(persons).map(person -> { person.setName(person.getName().toUpperCase()); return person; }); @@ -481,7 +480,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @RequestMapping("/promise-capitalize") @ResponseBody public Promise promiseCapitalize(@RequestBody Promise personFuture) { - return Streams.from(personFuture.map(person -> { + return Stream.from(personFuture.map(person -> { person.setName(person.getName().toUpperCase()); return person; })).promise(); @@ -489,12 +488,12 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @RequestMapping("/publisher-create") public Publisher publisherCreate(@RequestBody Publisher personStream) { - return Streams.from(personStream).toList().doOnSuccess(persons::addAll).after(); + return Stream.from(personStream).toList().doOnSuccess(persons::addAll).after(); } @RequestMapping("/stream-create") public Publisher streamCreate(@RequestBody Stream personStream) { - return Streams.from(personStream.toList().doOnSuccess(persons::addAll).after()).promise(); + return Stream.from(personStream.toList().doOnSuccess(persons::addAll).after()).promise(); } @RequestMapping("/observable-create") @@ -517,7 +516,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @ExceptionHandler @ResponseBody public Publisher handleException(IllegalStateException ex) { - return Streams.just("Recovered from error: " + ex.getMessage()); + return Stream.just("Recovered from error: " + ex.getMessage()); } //TODO add mixed and T request mappings tests From 97af9998d5d4dbaa885776ae71b863afa8c89263 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 7 Jan 2016 18:24:29 -0500 Subject: [PATCH 146/498] Refine exception handling Refine exception handling responsibilities between the DispatcherHandler and the HandlerAdapter. --- .../web/reactive/DispatcherHandler.java | 28 ++++------ .../web/reactive/HandlerAdapter.java | 45 ++++++++-------- .../web/reactive/HandlerResult.java | 51 +++++-------------- .../RequestMappingHandlerAdapter.java | 8 ++- 4 files changed, 52 insertions(+), 80 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java index 8b5c676d95..d5fbe85169 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java @@ -117,35 +117,29 @@ public class DispatcherHandler implements HttpHandler, ApplicationContextAware { logger.debug("Processing " + request.getMethod() + " request for [" + request.getURI() + "]"); } return Flux.fromIterable(this.handlerMappings) - .concatMap(m -> m.getHandler(request)) + .concatMap(mapping -> mapping.getHandler(request)) .next() - .then(handler -> getHandlerAdapter(handler).handle(request, response, handler)) - .then(result -> { - Mono mono = (result.hasError() ? Mono.error(result.getError()) : - handleResult(request, response, result)); - if (result.hasExceptionMapper()) { - return mono.otherwise(ex -> result.getExceptionMapper().apply(ex) - .then(exResult -> handleResult(request, response, exResult))); - } - return mono; - }) + .then(handler -> invokeHandler(request, response, handler)) + .then(result -> handleResult(request, response, result)) .otherwise(ex -> Mono.error(this.errorMapper.apply(ex))); } - protected HandlerAdapter getHandlerAdapter(Object handler) { + private Mono invokeHandler(ServerHttpRequest request, ServerHttpResponse response, Object handler) { for (HandlerAdapter handlerAdapter : this.handlerAdapters) { if (handlerAdapter.supports(handler)) { - return handlerAdapter; + return handlerAdapter.handle(request, response, handler); } } - throw new IllegalStateException("No HandlerAdapter: " + handler); + return Mono.error(new IllegalStateException("No HandlerAdapter: " + handler)); } - protected Mono handleResult(ServerHttpRequest request, ServerHttpResponse response, HandlerResult result) { - return getResultHandler(result).handleResult(request, response, result); + private Mono handleResult(ServerHttpRequest request, ServerHttpResponse response, HandlerResult result) { + return getResultHandler(result).handleResult(request, response, result) + .otherwise(ex -> result.applyExceptionHandler(ex).then(exceptionResult -> + getResultHandler(result).handleResult(request, response, exceptionResult))); } - protected HandlerResultHandler getResultHandler(HandlerResult handlerResult) { + private HandlerResultHandler getResultHandler(HandlerResult handlerResult) { for (HandlerResultHandler resultHandler : resultHandlers) { if (resultHandler.supports(handlerResult)) { return resultHandler; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerAdapter.java index b66676d537..1262303380 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerAdapter.java @@ -16,17 +16,16 @@ package org.springframework.web.reactive; -import org.reactivestreams.Publisher; +import java.util.function.Function; + import reactor.Mono; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; /** - * Interface that must be implemented for each handler type to handle an HTTP request. - * This interface is used to allow the {@link DispatcherHandler} to be indefinitely - * extensible. The {@code DispatcherHandler} accesses all installed handlers through - * this interface, meaning that it does not contain code specific to any handler type. + * Contract that decouples the {@link DispatcherHandler} from the details of + * invoking a handler and makes it possible to support any handler type. * * @author Rossen Stoyanchev * @author Sebastien Deleuze @@ -34,26 +33,32 @@ import org.springframework.http.server.reactive.ServerHttpResponse; public interface HandlerAdapter { /** - * Given a handler instance, return whether or not this {@code HandlerAdapter} - * can support it. Typical HandlerAdapters will base the decision on the handler - * type. HandlerAdapters will usually only support one handler type each. - *

    A typical implementation: - *

    {@code - * return (handler instanceof MyHandler); - * } + * Whether this {@code HandlerAdapter} supports the given {@code handler}. + * * @param handler handler object to check - * @return whether or not this object can use the given handler + * @return whether or not the handler is supported */ boolean supports(Object handler); /** - * Use the given handler to handle this request. - * @param request current HTTP request - * @param response current HTTP response - * @param handler handler to use. This object must have previously been passed - * to the {@code supports} method of this interface, which must have - * returned {@code true}. - * @return A {@link Mono} that emits a single {@link HandlerResult} element + * Handle the request with the given handler. + * + *

    Implementations are encouraged to handle exceptions resulting from the + * invocation of a handler in order and if necessary to return an alternate + * result that represents an error response. + * + *

    Furthermore since an async {@code HandlerResult} may produce an error + * later during result handling implementations are also encouraged to + * {@link HandlerResult#setExceptionHandler(Function) set an exception + * handler} on the {@code HandlerResult} so that may also be applied later + * after result handling. + * + * @param request current request + * @param response current response + * @param handler the selected handler which must have been previously + * checked via {@link #supports(Object)} + * @return {@link Mono} that emits a single {@code HandlerResult} or none if + * the request has been fully handled and doesn't require further handling. */ Mono handle(ServerHttpRequest request, ServerHttpResponse response, Object handler); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java index ff84c5caa5..1e5660838a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java @@ -36,9 +36,7 @@ public class HandlerResult { private final ResolvableType resultType; - private final Throwable error; - - private Function> exceptionMapper; + private Function> exceptionHandler; public HandlerResult(Object handler, Object result, ResolvableType resultType) { @@ -47,16 +45,6 @@ public class HandlerResult { this.handler = handler; this.result = result; this.resultType = resultType; - this.error = null; - } - - public HandlerResult(Object handler, Throwable error) { - Assert.notNull(handler, "'handler' is required"); - Assert.notNull(error, "'error' is required"); - this.handler = handler; - this.result = null; - this.resultType = null; - this.error = error; } @@ -72,38 +60,25 @@ public class HandlerResult { return this.resultType; } - public Throwable getError() { - return this.error; - } - /** - * Whether handler invocation produced a result or failed with an error. - *

    If {@code true} the {@link #getError()} returns the error while - * {@link #getResult()} and {@link #getResultType()} return {@code null} - * and vice versa. - * @return whether this instance contains a result or an error. + * For an async result, failures may occur later during result handling. + * Use this property to configure an exception handler to be invoked if + * result handling fails. + * + * @param function a function to map the the error to an alternative result. + * @return the current instance */ - public boolean hasError() { - return (this.error != null); - } - - /** - * Configure a function for selecting an alternate {@code HandlerResult} in - * case of an {@link #hasError() error result} or in case of an async result - * that results in an error. - * @param function the exception resolving function - */ - public HandlerResult setExceptionMapper(Function> function) { - this.exceptionMapper = function; + public HandlerResult setExceptionHandler(Function> function) { + this.exceptionHandler = function; return this; } - public Function> getExceptionMapper() { - return this.exceptionMapper; + public boolean hasExceptionHandler() { + return (this.exceptionHandler != null); } - public boolean hasExceptionMapper() { - return (this.exceptionMapper != null); + public Mono applyExceptionHandler(Throwable ex) { + return (hasExceptionHandler() ? this.exceptionHandler.apply(ex) : Mono.error(ex)); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java index a6e67927a4..b9ebe323ea 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java @@ -109,17 +109,15 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Initializin Object handler) { HandlerMethod handlerMethod = (HandlerMethod) handler; - InvocableHandlerMethod invocable = new InvocableHandlerMethod(handlerMethod); invocable.setHandlerMethodArgumentResolvers(this.argumentResolvers); return invocable.invokeForRequest(request) - .otherwise(ex -> Mono.just(new HandlerResult(handler, ex))) - .map(result -> result.setExceptionMapper( - ex -> mapException(ex, handlerMethod, request, response))); + .map(result -> result.setExceptionHandler(ex -> handleException(ex, handlerMethod, request, response))) + .otherwise(ex -> handleException(ex, handlerMethod, request, response)); } - private Mono mapException(Throwable ex, HandlerMethod handlerMethod, + private Mono handleException(Throwable ex, HandlerMethod handlerMethod, ServerHttpRequest request, ServerHttpResponse response) { if (ex instanceof Exception) { From b4c3a67d2c9a70db5910390b4020f5c0259c63e9 Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Thu, 7 Jan 2016 23:28:44 +0000 Subject: [PATCH 147/498] Use Stream instead of Streams and uncomment reactor test --- .../reactive/AbstractHttpHandlerIntegrationTests.java | 2 +- .../handler/SimpleUrlHandlerMappingIntegrationTests.java | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AbstractHttpHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AbstractHttpHandlerIntegrationTests.java index 5b4b4da4be..83781e29f9 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AbstractHttpHandlerIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AbstractHttpHandlerIntegrationTests.java @@ -44,7 +44,7 @@ public abstract class AbstractHttpHandlerIntegrationTests { return new Object[][] { {new JettyHttpServer()}, {new RxNettyHttpServer()}, -// {new ReactorHttpServer()}, + {new ReactorHttpServer()}, {new TomcatHttpServer()}, {new UndertowHttpServer()} }; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java index 82690ca99d..5b7a76143a 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java @@ -24,7 +24,7 @@ import java.util.Map; import org.junit.Test; import reactor.Mono; import reactor.io.buffer.Buffer; -import reactor.rx.Streams; +import reactor.rx.Stream; import org.springframework.context.support.StaticApplicationContext; import org.springframework.http.HttpStatus; @@ -136,7 +136,7 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler @Override public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { - return response.setBody(Streams.just(Buffer.wrap("foo").byteBuffer())); + return response.setBody(Stream.just(Buffer.wrap("foo").byteBuffer())); } } @@ -144,7 +144,7 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler @Override public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { - return response.setBody(Streams.just(Buffer.wrap("bar").byteBuffer())); + return response.setBody(Stream.just(Buffer.wrap("bar").byteBuffer())); } } From 94be412327a1e65e58c933748c7db321a889b51a Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Thu, 7 Jan 2016 23:41:48 +0000 Subject: [PATCH 148/498] Remove last Stream reference --- .../ReactiveStreamsToReactorStreamConverter.java | 5 ++--- .../server/reactive/WriteWithOperatorTests.java | 16 +++++++--------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToReactorStreamConverter.java b/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToReactorStreamConverter.java index 9ae1c8d19c..3cc34af221 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToReactorStreamConverter.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToReactorStreamConverter.java @@ -22,7 +22,6 @@ import java.util.Set; import org.reactivestreams.Publisher; import reactor.rx.Promise; import reactor.rx.Stream; -import reactor.rx.Streams; import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.GenericConverter; @@ -52,13 +51,13 @@ public final class ReactiveStreamsToReactorStreamConverter implements GenericCon return source; } else if (Stream.class.isAssignableFrom(targetType.getResolvableType().getRawClass())) { - return Streams.from((Publisher)source); + return Stream.from((Publisher)source); } else if (Promise.class.isAssignableFrom(source.getClass())) { return source; } else if (Promise.class.isAssignableFrom(targetType.getResolvableType().getRawClass())) { - return Streams.from((Publisher)source).promise(); + return Stream.from((Publisher)source).promise(); } return null; } diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/WriteWithOperatorTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/WriteWithOperatorTests.java index 12b06a3d2e..c4b8d22385 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/WriteWithOperatorTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/WriteWithOperatorTests.java @@ -29,12 +29,10 @@ import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import reactor.Flux; import reactor.core.subscriber.SubscriberBarrier; -import reactor.rx.Streams; +import reactor.rx.Stream; import reactor.rx.stream.Signal; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; /** * @author Rossen Stoyanchev @@ -57,7 +55,7 @@ public class WriteWithOperatorTests { public void errorBeforeFirstItem() throws Exception { IllegalStateException error = new IllegalStateException("boo"); Publisher completion = Flux.error(error).lift(this.operator); - List> signals = Streams.from(completion).materialize().toList().get(); + List> signals = Stream.from(completion).materialize().toList().get(); assertEquals(1, signals.size()); assertSame("Unexpected signal: " + signals.get(0), error, signals.get(0).getThrowable()); @@ -66,7 +64,7 @@ public class WriteWithOperatorTests { @Test public void completionBeforeFirstItem() throws Exception { Publisher completion = Flux.empty().lift(this.operator); - List> signals = Streams.from(completion).materialize().toList().get(); + List> signals = Stream.from(completion).materialize().toList().get(); assertEquals(1, signals.size()); assertTrue("Unexpected signal: " + signals.get(0), signals.get(0).isOnComplete()); @@ -78,7 +76,7 @@ public class WriteWithOperatorTests { @Test public void writeOneItem() throws Exception { Publisher completion = Flux.just("one").lift(this.operator); - List> signals = Streams.from(completion).materialize().toList().get(); + List> signals = Stream.from(completion).materialize().toList().get(); assertEquals(1, signals.size()); assertTrue("Unexpected signal: " + signals.get(0), signals.get(0).isOnComplete()); @@ -93,7 +91,7 @@ public class WriteWithOperatorTests { public void writeMultipleItems() throws Exception { List items = Arrays.asList("one", "two", "three"); Publisher completion = Flux.fromIterable(items).lift(this.operator); - List> signals = Streams.from(completion).materialize().toList().get(); + List> signals = Stream.from(completion).materialize().toList().get(); assertEquals(1, signals.size()); assertTrue("Unexpected signal: " + signals.get(0), signals.get(0).isOnComplete()); @@ -116,7 +114,7 @@ public class WriteWithOperatorTests { } }, subscriber -> new AtomicInteger()); Publisher completion = publisher.lift(this.operator); - List> signals = Streams.from(completion).materialize().toList().get(); + List> signals = Stream.from(completion).materialize().toList().get(); assertEquals(1, signals.size()); assertSame("Unexpected signal: " + signals.get(0), error, signals.get(0).getThrowable()); From 54e4e012b2b7afee291f11374ccdaecf39c92e2d Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 7 Jan 2016 21:44:40 -0500 Subject: [PATCH 149/498] Separate out web.server from http.server.reactive This change separates out lower-level HTTP adapter code from the more (framework-specific) HTTP processing into a separate package under org.springframework.web.server (not under org.springframework.http). --- .../http/server/reactive/HttpHandler.java | 3 --- .../web/reactive/ResponseStatusExceptionHandler.java | 2 +- .../server}/ErrorHandlingHttpHandler.java | 5 ++++- .../server}/FilterChainHttpHandler.java | 6 +++++- .../reactive => web/server}/HttpExceptionHandler.java | 6 +++++- .../server/reactive => web/server}/HttpFilter.java | 7 +++++-- .../reactive => web/server}/HttpFilterChain.java | 5 ++++- .../reactive => web/server}/HttpHandlerDecorator.java | 5 ++++- .../server}/InternalServerErrorExceptionHandler.java | 4 +++- .../web/reactive/DispatcherHandlerErrorTests.java | 10 +++++----- .../SimpleUrlHandlerMappingIntegrationTests.java | 2 +- .../server}/ErrorHandlingHttpHandlerTests.java | 10 +++++++++- .../server}/FilterChainHttpHandlerTests.java | 6 +++++- 13 files changed, 51 insertions(+), 20 deletions(-) rename spring-web-reactive/src/main/java/org/springframework/{http/server/reactive => web/server}/ErrorHandlingHttpHandler.java (89%) rename spring-web-reactive/src/main/java/org/springframework/{http/server/reactive => web/server}/FilterChainHttpHandler.java (88%) rename spring-web-reactive/src/main/java/org/springframework/{http/server/reactive => web/server}/HttpExceptionHandler.java (85%) rename spring-web-reactive/src/main/java/org/springframework/{http/server/reactive => web/server}/HttpFilter.java (86%) rename spring-web-reactive/src/main/java/org/springframework/{http/server/reactive => web/server}/HttpFilterChain.java (86%) rename spring-web-reactive/src/main/java/org/springframework/{http/server/reactive => web/server}/HttpHandlerDecorator.java (84%) rename spring-web-reactive/src/main/java/org/springframework/{http/server/reactive => web/server}/InternalServerErrorExceptionHandler.java (86%) rename spring-web-reactive/src/test/java/org/springframework/{http/server/reactive => web/server}/ErrorHandlingHttpHandlerTests.java (88%) rename spring-web-reactive/src/test/java/org/springframework/{http/server/reactive => web/server}/FilterChainHttpHandlerTests.java (94%) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpHandler.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpHandler.java index 328cd89185..e04512dceb 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpHandler.java @@ -22,9 +22,6 @@ import reactor.Mono; * Contract for handling HTTP requests in a non-blocking way. * * @author Arjen Poutsma - * @author Rossen Stoyanchev - * @author Sebastien Deleuze - * @see HttpFilter */ public interface HttpHandler { diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/ResponseStatusExceptionHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/ResponseStatusExceptionHandler.java index 713a692871..2312778496 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/ResponseStatusExceptionHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/ResponseStatusExceptionHandler.java @@ -17,7 +17,7 @@ package org.springframework.web.reactive; import reactor.Mono; -import org.springframework.http.server.reactive.HttpExceptionHandler; +import org.springframework.web.server.HttpExceptionHandler; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.web.ResponseStatusException; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ErrorHandlingHttpHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/server/ErrorHandlingHttpHandler.java similarity index 89% rename from spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ErrorHandlingHttpHandler.java rename to spring-web-reactive/src/main/java/org/springframework/web/server/ErrorHandlingHttpHandler.java index 09cf6a6413..3edbcfc50e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ErrorHandlingHttpHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/ErrorHandlingHttpHandler.java @@ -13,13 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.http.server.reactive; +package org.springframework.web.server; import java.util.Arrays; import java.util.List; import reactor.Mono; +import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.util.Assert; /** diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/FilterChainHttpHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/server/FilterChainHttpHandler.java similarity index 88% rename from spring-web-reactive/src/main/java/org/springframework/http/server/reactive/FilterChainHttpHandler.java rename to spring-web-reactive/src/main/java/org/springframework/web/server/FilterChainHttpHandler.java index 80ac46057b..2026ab3c07 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/FilterChainHttpHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/FilterChainHttpHandler.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.http.server.reactive; +package org.springframework.web.server; import java.util.Arrays; import java.util.Collections; @@ -21,6 +21,10 @@ import java.util.List; import reactor.Mono; +import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; + /** * {@link HttpHandler} that delegates to a chain of {@link HttpFilter}s followed * by a target {@link HttpHandler}. diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpExceptionHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/server/HttpExceptionHandler.java similarity index 85% rename from spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpExceptionHandler.java rename to spring-web-reactive/src/main/java/org/springframework/web/server/HttpExceptionHandler.java index d982c115a7..2d0dc80113 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpExceptionHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/HttpExceptionHandler.java @@ -13,10 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.http.server.reactive; +package org.springframework.web.server; import reactor.Mono; +import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; + /** * A contract for resolving exceptions from HTTP request handling. * diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpFilter.java b/spring-web-reactive/src/main/java/org/springframework/web/server/HttpFilter.java similarity index 86% rename from spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpFilter.java rename to spring-web-reactive/src/main/java/org/springframework/web/server/HttpFilter.java index b21b1c2b0f..e1433a26f7 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpFilter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/HttpFilter.java @@ -14,11 +14,14 @@ * limitations under the License. */ -package org.springframework.http.server.reactive; +package org.springframework.web.server; -import org.reactivestreams.Publisher; import reactor.Mono; +import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; + /** * Contract for interception-style, chained processing of HTTP requests. * diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpFilterChain.java b/spring-web-reactive/src/main/java/org/springframework/web/server/HttpFilterChain.java similarity index 86% rename from spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpFilterChain.java rename to spring-web-reactive/src/main/java/org/springframework/web/server/HttpFilterChain.java index 0bb70708bc..5188785610 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpFilterChain.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/HttpFilterChain.java @@ -13,10 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.http.server.reactive; +package org.springframework.web.server; import reactor.Mono; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; + /** * Represents a chain of {@link HttpFilter}s allowing each {@link HttpFilter} to * delegate to the next in the chain. diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpHandlerDecorator.java b/spring-web-reactive/src/main/java/org/springframework/web/server/HttpHandlerDecorator.java similarity index 84% rename from spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpHandlerDecorator.java rename to spring-web-reactive/src/main/java/org/springframework/web/server/HttpHandlerDecorator.java index 49190be05d..c2062755ce 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpHandlerDecorator.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/HttpHandlerDecorator.java @@ -13,10 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.http.server.reactive; +package org.springframework.web.server; import reactor.Mono; +import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.util.Assert; /** diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/InternalServerErrorExceptionHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/server/InternalServerErrorExceptionHandler.java similarity index 86% rename from spring-web-reactive/src/main/java/org/springframework/http/server/reactive/InternalServerErrorExceptionHandler.java rename to spring-web-reactive/src/main/java/org/springframework/web/server/InternalServerErrorExceptionHandler.java index 8d6cc4a22a..a5580d794a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/InternalServerErrorExceptionHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/InternalServerErrorExceptionHandler.java @@ -13,11 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.http.server.reactive; +package org.springframework.web.server; import reactor.Mono; import org.springframework.http.HttpStatus; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; /** * Handle any exception by setting the response status to 500. diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java index 5a43444de4..5f7c3e3197 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java @@ -37,11 +37,11 @@ import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.web.ResponseStatusException; -import org.springframework.http.server.reactive.ErrorHandlingHttpHandler; -import org.springframework.http.server.reactive.FilterChainHttpHandler; -import org.springframework.http.server.reactive.HttpExceptionHandler; -import org.springframework.http.server.reactive.HttpFilter; -import org.springframework.http.server.reactive.HttpFilterChain; +import org.springframework.web.server.ErrorHandlingHttpHandler; +import org.springframework.web.server.FilterChainHttpHandler; +import org.springframework.web.server.HttpExceptionHandler; +import org.springframework.web.server.HttpFilter; +import org.springframework.web.server.HttpFilterChain; import org.springframework.http.server.reactive.HttpHandler; import org.springframework.http.server.reactive.MockServerHttpRequest; import org.springframework.http.server.reactive.MockServerHttpResponse; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java index 5b7a76143a..3507f005f4 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java @@ -31,7 +31,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; import org.springframework.http.server.reactive.AbstractHttpHandlerIntegrationTests; -import org.springframework.http.server.reactive.ErrorHandlingHttpHandler; +import org.springframework.web.server.ErrorHandlingHttpHandler; import org.springframework.http.server.reactive.HttpHandler; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ErrorHandlingHttpHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/server/ErrorHandlingHttpHandlerTests.java similarity index 88% rename from spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ErrorHandlingHttpHandlerTests.java rename to spring-web-reactive/src/test/java/org/springframework/web/server/ErrorHandlingHttpHandlerTests.java index 925eeacc7c..43b431c90d 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ErrorHandlingHttpHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/server/ErrorHandlingHttpHandlerTests.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.http.server.reactive; +package org.springframework.web.server; import java.net.URI; @@ -27,6 +27,14 @@ import reactor.rx.stream.Signal; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; +import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.web.server.ErrorHandlingHttpHandler; +import org.springframework.web.server.HttpExceptionHandler; +import org.springframework.web.server.InternalServerErrorExceptionHandler; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/FilterChainHttpHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/server/FilterChainHttpHandlerTests.java similarity index 94% rename from spring-web-reactive/src/test/java/org/springframework/http/server/reactive/FilterChainHttpHandlerTests.java rename to spring-web-reactive/src/test/java/org/springframework/web/server/FilterChainHttpHandlerTests.java index 3f88b4b97a..370dd644db 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/FilterChainHttpHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/server/FilterChainHttpHandlerTests.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.http.server.reactive; +package org.springframework.web.server; import org.apache.commons.logging.Log; @@ -22,6 +22,10 @@ import org.junit.Before; import org.junit.Test; import reactor.Mono; +import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; + import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; From 4f614fa0fd74ce29c5822e70f4815cc40745a2be Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 8 Jan 2016 12:24:34 -0500 Subject: [PATCH 150/498] Add WebServerExchange This change adds a WebServerExchange and updates all contracts at the the same level (i.e. org.springframework.web.server) as well as the org.springframework.web.reactive level to use it so that all framework-related code will have access to server-side processing features such as request attributes (and others to come). --- .../core/codec/support/Jaxb2Decoder.java | 2 +- .../core/codec/support/Jaxb2Encoder.java | 2 +- .../reactive/RxNettyServerHttpRequest.java | 1 - .../server/reactive/boot/JettyHttpServer.java | 2 +- .../reactive/boot/ReactorHttpServer.java | 2 +- .../reactive/boot/RxNettyHttpServer.java | 2 +- .../reactive/boot/TomcatHttpServer.java | 2 +- .../reactive/boot/UndertowHttpServer.java | 8 +- .../web/reactive/DispatcherHandler.java | 27 +-- .../DispatcherHandlerExceptionMapper.java | 2 +- .../web/reactive/HandlerAdapter.java | 9 +- .../web/reactive/HandlerMapping.java | 8 +- .../web/reactive/HandlerResultHandler.java | 8 +- .../ResponseStatusExceptionHandler.java | 11 +- ...er.java => HttpHandlerHandlerAdapter.java} | 27 ++- .../handler/SimpleHandlerResultHandler.java | 9 +- .../handler/SimpleUrlHandlerMapping.java | 6 +- .../method/HandlerMethodArgumentResolver.java | 4 +- .../method/InvocableHandlerMethod.java | 14 +- .../RequestBodyArgumentResolver.java | 12 +- .../RequestMappingHandlerAdapter.java | 17 +- .../RequestMappingHandlerMapping.java | 13 +- .../RequestParamArgumentResolver.java | 8 +- .../annotation/ResponseBodyResultHandler.java | 9 +- .../web/server/DefaultWebServerExchange.java | 62 +++++++ .../web/server/ErrorHandlingHttpHandler.java | 69 -------- .../server/ExceptionHandlingWebHandler.java | 74 +++++++++ .../web/server/FilterChainHttpHandler.java | 67 -------- .../web/server/FilteringWebHandler.java | 74 +++++++++ .../web/server/HttpFilter.java | 50 ------ .../web/server/WebExceptionHandler.java | 38 +++++ .../springframework/web/server/WebFilter.java | 40 +++++ ...tpFilterChain.java => WebFilterChain.java} | 16 +- .../web/server/WebHandler.java | 42 +++++ ...ecorator.java => WebHandlerDecorator.java} | 16 +- ...ionHandler.java => WebServerExchange.java} | 27 ++- ...dler.java => WebToHttpHandlerAdapter.java} | 34 ++-- .../web/server/WebToHttpHandlerBuilder.java | 84 ++++++++++ .../reactive/DispatcherHandlerErrorTests.java | 68 ++++---- .../ResponseStatusExceptionHandlerTests.java | 20 ++- ...mpleUrlHandlerMappingIntegrationTests.java | 33 ++-- .../method/InvocableHandlerMethodTests.java | 36 ++-- .../RequestMappingHandlerMappingTests.java | 16 +- .../RequestMappingIntegrationTests.java | 9 +- .../server/ErrorHandlingHttpHandlerTests.java | 154 ------------------ .../ExceptionHandlingHttpHandlerTests.java | 140 ++++++++++++++++ ...sts.java => FilteringWebHandlerTests.java} | 71 ++++---- 47 files changed, 831 insertions(+), 614 deletions(-) rename spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/{HttpHandlerAdapter.java => HttpHandlerHandlerAdapter.java} (56%) create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/server/DefaultWebServerExchange.java delete mode 100644 spring-web-reactive/src/main/java/org/springframework/web/server/ErrorHandlingHttpHandler.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/server/ExceptionHandlingWebHandler.java delete mode 100644 spring-web-reactive/src/main/java/org/springframework/web/server/FilterChainHttpHandler.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/server/FilteringWebHandler.java delete mode 100644 spring-web-reactive/src/main/java/org/springframework/web/server/HttpFilter.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/server/WebExceptionHandler.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/server/WebFilter.java rename spring-web-reactive/src/main/java/org/springframework/web/server/{HttpFilterChain.java => WebFilterChain.java} (63%) create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/server/WebHandler.java rename spring-web-reactive/src/main/java/org/springframework/web/server/{HttpHandlerDecorator.java => WebHandlerDecorator.java} (66%) rename spring-web-reactive/src/main/java/org/springframework/web/server/{InternalServerErrorExceptionHandler.java => WebServerExchange.java} (60%) rename spring-web-reactive/src/main/java/org/springframework/web/server/{HttpExceptionHandler.java => WebToHttpHandlerAdapter.java} (54%) create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/server/WebToHttpHandlerBuilder.java delete mode 100644 spring-web-reactive/src/test/java/org/springframework/web/server/ErrorHandlingHttpHandlerTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/server/ExceptionHandlingHttpHandlerTests.java rename spring-web-reactive/src/test/java/org/springframework/web/server/{FilterChainHttpHandlerTests.java => FilteringWebHandlerTests.java} (61%) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Decoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Decoder.java index 337cb24512..b17150ed9b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Decoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Decoder.java @@ -38,8 +38,8 @@ import reactor.Flux; import org.springframework.core.ResolvableType; import org.springframework.core.codec.CodecException; -import org.springframework.util.ByteBufferPublisherInputStream; import org.springframework.util.Assert; +import org.springframework.util.ByteBufferPublisherInputStream; import org.springframework.util.MimeType; import org.springframework.util.MimeTypeUtils; diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Encoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Encoder.java index f518d9f5e2..d692c744ff 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Encoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Encoder.java @@ -31,8 +31,8 @@ import reactor.io.buffer.Buffer; import org.springframework.core.ResolvableType; import org.springframework.core.codec.CodecException; -import org.springframework.util.BufferOutputStream; import org.springframework.util.Assert; +import org.springframework.util.BufferOutputStream; import org.springframework.util.ClassUtils; import org.springframework.util.MimeType; import org.springframework.util.MimeTypeUtils; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java index f4ee9f3dcf..7e4683eed5 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java @@ -22,7 +22,6 @@ import java.nio.ByteBuffer; import io.netty.buffer.ByteBuf; import io.reactivex.netty.protocol.http.server.HttpServerRequest; -import org.reactivestreams.Publisher; import reactor.Flux; import reactor.core.publisher.convert.RxJava1Converter; import rx.Observable; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/JettyHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/JettyHttpServer.java index d6780ae19c..f462a8ad20 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/JettyHttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/JettyHttpServer.java @@ -22,9 +22,9 @@ import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder; import org.springframework.beans.factory.InitializingBean; +import org.springframework.http.server.reactive.ServletHttpHandlerAdapter; import org.springframework.util.Assert; import org.springframework.util.SocketUtils; -import org.springframework.http.server.reactive.ServletHttpHandlerAdapter; /** * @author Rossen Stoyanchev diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java index c335796d0c..e575fc82b8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java @@ -21,8 +21,8 @@ import reactor.io.buffer.Buffer; import reactor.io.net.ReactiveNet; import org.springframework.beans.factory.InitializingBean; -import org.springframework.util.Assert; import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter; +import org.springframework.util.Assert; /** * @author Stephane Maldini diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/RxNettyHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/RxNettyHttpServer.java index 84525ac0d5..24c056f3f0 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/RxNettyHttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/RxNettyHttpServer.java @@ -19,8 +19,8 @@ package org.springframework.http.server.reactive.boot; import io.netty.buffer.ByteBuf; import org.springframework.beans.factory.InitializingBean; -import org.springframework.util.Assert; import org.springframework.http.server.reactive.RxNettyHttpHandlerAdapter; +import org.springframework.util.Assert; /** diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/TomcatHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/TomcatHttpServer.java index b91c3aa49a..90c75057c8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/TomcatHttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/TomcatHttpServer.java @@ -23,9 +23,9 @@ import org.apache.catalina.LifecycleException; import org.apache.catalina.startup.Tomcat; import org.springframework.beans.factory.InitializingBean; +import org.springframework.http.server.reactive.ServletHttpHandlerAdapter; import org.springframework.util.Assert; import org.springframework.util.SocketUtils; -import org.springframework.http.server.reactive.ServletHttpHandlerAdapter; /** diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/UndertowHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/UndertowHttpServer.java index 68af14ae4a..3de5b5da1e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/UndertowHttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/UndertowHttpServer.java @@ -16,13 +16,13 @@ package org.springframework.http.server.reactive.boot; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.util.Assert; -import org.springframework.http.server.reactive.UndertowHttpHandlerAdapter; - import io.undertow.Undertow; import io.undertow.server.HttpHandler; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.http.server.reactive.UndertowHttpHandlerAdapter; +import org.springframework.util.Assert; + /** * @author Marek Hawrylczak */ diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java index d5fbe85169..cb99099a71 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java @@ -31,9 +31,9 @@ import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.core.annotation.AnnotationAwareOrderComparator; -import org.springframework.http.server.reactive.HttpHandler; import org.springframework.http.server.reactive.ServerHttpRequest; -import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.web.server.WebHandler; +import org.springframework.web.server.WebServerExchange; /** * Central dispatcher for HTTP request handlers/controllers. Dispatches to registered @@ -53,7 +53,7 @@ import org.springframework.http.server.reactive.ServerHttpResponse; * @author Rossen Stoyanchev * @author Sebastien Deleuze */ -public class DispatcherHandler implements HttpHandler, ApplicationContextAware { +public class DispatcherHandler implements WebHandler, ApplicationContextAware { private static final Log logger = LogFactory.getLog(DispatcherHandler.class); @@ -112,31 +112,32 @@ public class DispatcherHandler implements HttpHandler, ApplicationContextAware { @Override - public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { + public Mono handle(WebServerExchange exchange) { if (logger.isDebugEnabled()) { + ServerHttpRequest request = exchange.getRequest(); logger.debug("Processing " + request.getMethod() + " request for [" + request.getURI() + "]"); } return Flux.fromIterable(this.handlerMappings) - .concatMap(mapping -> mapping.getHandler(request)) + .concatMap(mapping -> mapping.getHandler(exchange)) .next() - .then(handler -> invokeHandler(request, response, handler)) - .then(result -> handleResult(request, response, result)) + .then(handler -> invokeHandler(exchange, handler)) + .then(result -> handleResult(exchange, result)) .otherwise(ex -> Mono.error(this.errorMapper.apply(ex))); } - private Mono invokeHandler(ServerHttpRequest request, ServerHttpResponse response, Object handler) { + private Mono invokeHandler(WebServerExchange exchange, Object handler) { for (HandlerAdapter handlerAdapter : this.handlerAdapters) { if (handlerAdapter.supports(handler)) { - return handlerAdapter.handle(request, response, handler); + return handlerAdapter.handle(exchange, handler); } } return Mono.error(new IllegalStateException("No HandlerAdapter: " + handler)); } - private Mono handleResult(ServerHttpRequest request, ServerHttpResponse response, HandlerResult result) { - return getResultHandler(result).handleResult(request, response, result) + private Mono handleResult(WebServerExchange exchange, HandlerResult result) { + return getResultHandler(result).handleResult(exchange, result) .otherwise(ex -> result.applyExceptionHandler(ex).then(exceptionResult -> - getResultHandler(result).handleResult(request, response, exceptionResult))); + getResultHandler(result).handleResult(exchange, exceptionResult))); } private HandlerResultHandler getResultHandler(HandlerResult handlerResult) { @@ -156,7 +157,7 @@ public class DispatcherHandler implements HttpHandler, ApplicationContextAware { @Override - public Mono getHandler(ServerHttpRequest request) { + public Mono getHandler(WebServerExchange exchange) { return Mono.error(HANDLER_NOT_FOUND_EXCEPTION); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandlerExceptionMapper.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandlerExceptionMapper.java index 9884aa122d..07a39d735f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandlerExceptionMapper.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandlerExceptionMapper.java @@ -19,8 +19,8 @@ import java.util.function.Function; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.http.HttpStatus; -import org.springframework.web.ResponseStatusException; import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.ResponseStatusException; import org.springframework.web.bind.annotation.ResponseStatus; /** diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerAdapter.java index 1262303380..ad96158264 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerAdapter.java @@ -20,8 +20,7 @@ import java.util.function.Function; import reactor.Mono; -import org.springframework.http.server.reactive.ServerHttpRequest; -import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.web.server.WebServerExchange; /** * Contract that decouples the {@link DispatcherHandler} from the details of @@ -53,14 +52,12 @@ public interface HandlerAdapter { * handler} on the {@code HandlerResult} so that may also be applied later * after result handling. * - * @param request current request - * @param response current response + * @param exchange current server exchange * @param handler the selected handler which must have been previously * checked via {@link #supports(Object)} * @return {@link Mono} that emits a single {@code HandlerResult} or none if * the request has been fully handled and doesn't require further handling. */ - Mono handle(ServerHttpRequest request, ServerHttpResponse response, - Object handler); + Mono handle(WebServerExchange exchange, Object handler); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerMapping.java index b8a164421a..d3c6f9ab6e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerMapping.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerMapping.java @@ -18,7 +18,7 @@ package org.springframework.web.reactive; import reactor.Mono; -import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.web.server.WebServerExchange; /** * Interface to be implemented by objects that define a mapping between @@ -31,10 +31,10 @@ public interface HandlerMapping { /** * Return a handler for this request. - * @param request current HTTP request + * @param exchange current server exchange * @return A {@link Mono} that emits one value or none in case the request - * cannot be resolved to a handler. + * cannot be resolved to a handler */ - Mono getHandler(ServerHttpRequest request); + Mono getHandler(WebServerExchange exchange); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResultHandler.java index fe55814ac3..d5a668a1ef 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResultHandler.java @@ -18,8 +18,7 @@ package org.springframework.web.reactive; import reactor.Mono; -import org.springframework.http.server.reactive.ServerHttpRequest; -import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.web.server.WebServerExchange; /** * Process the {@link HandlerResult}, usually returned by an {@link HandlerAdapter}. @@ -41,9 +40,10 @@ public interface HandlerResultHandler { * Process the given result modifying response headers and/or writing data * to the response. * + * @param exchange current server exchange + * @param result the result from the handling * @return {@code Mono} to indicate when request handling is complete. */ - Mono handleResult(ServerHttpRequest request, ServerHttpResponse response, - HandlerResult result); + Mono handleResult(WebServerExchange exchange, HandlerResult result); } \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/ResponseStatusExceptionHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/ResponseStatusExceptionHandler.java index 2312778496..8c19629d6c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/ResponseStatusExceptionHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/ResponseStatusExceptionHandler.java @@ -17,23 +17,22 @@ package org.springframework.web.reactive; import reactor.Mono; -import org.springframework.web.server.HttpExceptionHandler; -import org.springframework.http.server.reactive.ServerHttpRequest; -import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.web.ResponseStatusException; +import org.springframework.web.server.WebExceptionHandler; +import org.springframework.web.server.WebServerExchange; /** * Handle {@link ResponseStatusException} by setting the response status. * * @author Rossen Stoyanchev */ -public class ResponseStatusExceptionHandler implements HttpExceptionHandler { +public class ResponseStatusExceptionHandler implements WebExceptionHandler { @Override - public Mono handle(ServerHttpRequest request, ServerHttpResponse response, Throwable ex) { + public Mono handle(WebServerExchange exchange, Throwable ex) { if (ex instanceof ResponseStatusException) { - response.setStatusCode(((ResponseStatusException) ex).getHttpStatus()); + exchange.getResponse().setStatusCode(((ResponseStatusException) ex).getHttpStatus()); return Mono.empty(); } return Mono.error(ex); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/HttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/HttpHandlerHandlerAdapter.java similarity index 56% rename from spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/HttpHandlerAdapter.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/HttpHandlerHandlerAdapter.java index 13694ce1d7..b4994deb70 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/HttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/HttpHandlerHandlerAdapter.java @@ -20,25 +20,20 @@ import org.reactivestreams.Publisher; import reactor.Mono; import org.springframework.core.ResolvableType; -import org.springframework.http.server.reactive.ServerHttpRequest; -import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.web.reactive.DispatcherHandler; import org.springframework.web.reactive.HandlerAdapter; import org.springframework.web.reactive.HandlerResult; -import org.springframework.http.server.reactive.HttpHandler; -import org.springframework.web.reactive.DispatcherHandler; +import org.springframework.web.server.WebHandler; +import org.springframework.web.server.WebServerExchange; /** - * Support use of {@link HttpHandler} with - * {@link DispatcherHandler - * DispatcherHandler} (which implements the same contract). - * The use of {@code DispatcherHandler} this way enables routing requests to - * one of many {@code HttpHandler} instances depending on the configured - * handler mappings. + * Support use of {@link org.springframework.web.server.WebHandler} through the + * {@link DispatcherHandler}. * * @author Rossen Stoyanchev * @author Sebastien Deleuze */ -public class HttpHandlerAdapter implements HandlerAdapter { +public class HttpHandlerHandlerAdapter implements HandlerAdapter { private static final ResolvableType PUBLISHER_VOID = ResolvableType.forClassWithGenerics( Publisher.class, Void.class); @@ -46,14 +41,14 @@ public class HttpHandlerAdapter implements HandlerAdapter { @Override public boolean supports(Object handler) { - return HttpHandler.class.isAssignableFrom(handler.getClass()); + return WebHandler.class.isAssignableFrom(handler.getClass()); } @Override - public Mono handle(ServerHttpRequest request, ServerHttpResponse response, Object handler) { - HttpHandler httpHandler = (HttpHandler)handler; - Mono completion = httpHandler.handle(request, response); - return Mono.just(new HandlerResult(httpHandler, completion, PUBLISHER_VOID)); + public Mono handle(WebServerExchange exchange, Object handler) { + WebHandler webHandler = (WebHandler) handler; + Mono completion = webHandler.handle(exchange); + return Mono.just(new HandlerResult(webHandler, completion, PUBLISHER_VOID)); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java index ba03013652..6c8494ff1d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java @@ -22,11 +22,10 @@ import reactor.Mono; import org.springframework.core.Ordered; import org.springframework.core.ResolvableType; import org.springframework.core.convert.ConversionService; -import org.springframework.http.server.reactive.ServerHttpRequest; -import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.util.Assert; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.HandlerResultHandler; +import org.springframework.web.server.WebServerExchange; /** * Supports {@link HandlerResult} with a {@code void} or {@code Publisher} value. @@ -75,15 +74,11 @@ public class SimpleHandlerResultHandler implements Ordered, HandlerResultHandler @SuppressWarnings("unchecked") @Override - public Mono handleResult(ServerHttpRequest request, - ServerHttpResponse response, HandlerResult result) { - + public Mono handleResult(WebServerExchange exchange, HandlerResult result) { Object value = result.getResult(); - if (Void.TYPE.equals(result.getResultType().getRawClass())) { return Mono.empty(); } - return (value instanceof Mono ? (Mono)value : Mono.from(this.conversionService.convert(value, Publisher.class))); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMapping.java index 0ef5e9c286..5ff03ffdaf 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMapping.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMapping.java @@ -22,8 +22,8 @@ import java.util.Map; import reactor.Flux; import reactor.Mono; -import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.web.reactive.HandlerMapping; +import org.springframework.web.server.WebServerExchange; /** * @author Rossen Stoyanchev @@ -42,9 +42,9 @@ public class SimpleUrlHandlerMapping implements HandlerMapping { @Override - public Mono getHandler(ServerHttpRequest request) { + public Mono getHandler(WebServerExchange exchange) { return Flux.create(subscriber -> { - String path = request.getURI().getPath(); + String path = exchange.getRequest().getURI().getPath(); Object handler = this.handlerMap.get(path); if (handler != null) { subscriber.onNext(handler); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/HandlerMethodArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/HandlerMethodArgumentResolver.java index b4e7fd68e7..c9fc9397c1 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/HandlerMethodArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/HandlerMethodArgumentResolver.java @@ -19,7 +19,7 @@ package org.springframework.web.reactive.method; import reactor.Mono; import org.springframework.core.MethodParameter; -import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.web.server.WebServerExchange; /** @@ -35,6 +35,6 @@ public interface HandlerMethodArgumentResolver { * does not resolve to any value, which will result in {@code null} passed * as the argument value. */ - Mono resolveArgument(MethodParameter parameter, ServerHttpRequest request); + Mono resolveArgument(MethodParameter parameter, WebServerExchange exchange); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java index 0aaa7a6757..1b30d6da7e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java @@ -32,11 +32,11 @@ import org.springframework.core.GenericTypeResolver; import org.springframework.core.MethodParameter; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.ResolvableType; -import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerResult; +import org.springframework.web.server.WebServerExchange; /** @@ -76,14 +76,14 @@ public class InvocableHandlerMethod extends HandlerMethod { /** * Invoke the method and return a Publisher for the return value. - * @param request the current request + * @param exchange the current exchange * @param providedArgs optional list of argument values to check by type * (via {@code instanceof}) for resolving method arguments. * @return Publisher that produces a single HandlerResult or an error signal; - * never throws an exception. + * never throws an exception */ - public Mono invokeForRequest(ServerHttpRequest request, Object... providedArgs) { - return resolveArguments(request, providedArgs).then(args -> { + public Mono invokeForRequest(WebServerExchange exchange, Object... providedArgs) { + return resolveArguments(exchange, providedArgs).then(args -> { try { Object value = doInvoke(args); ResolvableType type = ResolvableType.forMethodParameter(getReturnType()); @@ -100,7 +100,7 @@ public class InvocableHandlerMethod extends HandlerMethod { }); } - private Mono resolveArguments(ServerHttpRequest request, Object... providedArgs) { + private Mono resolveArguments(WebServerExchange exchange, Object... providedArgs) { if (ObjectUtils.isEmpty(getMethodParameters())) { return NO_ARGS; } @@ -121,7 +121,7 @@ public class InvocableHandlerMethod extends HandlerMethod { .findFirst() .orElseThrow(() -> getArgError("No resolver for ", param, null)); try { - return resolver.resolveArgument(param, request) + return resolver.resolveArgument(param, exchange) .defaultIfEmpty(NO_VALUE) .otherwise(ex -> Mono.error(getArgError("Error resolving ", param, ex))); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestBodyArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestBodyArgumentResolver.java index 9302e333a6..3e5c01306b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestBodyArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestBodyArgumentResolver.java @@ -25,13 +25,13 @@ import reactor.Mono; import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; +import org.springframework.core.codec.Decoder; import org.springframework.core.convert.ConversionService; import org.springframework.http.MediaType; -import org.springframework.http.server.reactive.ServerHttpRequest; -import org.springframework.core.codec.Decoder; -import org.springframework.web.reactive.method.HandlerMethodArgumentResolver; import org.springframework.util.Assert; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.reactive.method.HandlerMethodArgumentResolver; +import org.springframework.web.server.WebServerExchange; /** * @author Sebastien Deleuze @@ -58,13 +58,13 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve } @Override - public Mono resolveArgument(MethodParameter parameter, ServerHttpRequest request) { - MediaType mediaType = request.getHeaders().getContentType(); + public Mono resolveArgument(MethodParameter parameter, WebServerExchange exchange) { + MediaType mediaType = exchange.getRequest().getHeaders().getContentType(); if (mediaType == null) { mediaType = MediaType.APPLICATION_OCTET_STREAM; } ResolvableType type = ResolvableType.forMethodParameter(parameter); - Flux body = request.getBody(); + Flux body = exchange.getRequest().getBody(); Flux elementFlux = body; ResolvableType elementType = type.hasGenerics() ? type.getGeneric(0) : type; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java index b9ebe323ea..51d6539d30 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java @@ -35,8 +35,6 @@ import org.springframework.core.codec.support.JsonObjectDecoder; import org.springframework.core.codec.support.StringDecoder; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.DefaultConversionService; -import org.springframework.http.server.reactive.ServerHttpRequest; -import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.util.ObjectUtils; import org.springframework.web.method.HandlerMethod; import org.springframework.web.method.annotation.ExceptionHandlerMethodResolver; @@ -44,6 +42,7 @@ import org.springframework.web.reactive.HandlerAdapter; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.method.HandlerMethodArgumentResolver; import org.springframework.web.reactive.method.InvocableHandlerMethod; +import org.springframework.web.server.WebServerExchange; /** @@ -105,20 +104,18 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Initializin } @Override - public Mono handle(ServerHttpRequest request, ServerHttpResponse response, - Object handler) { - + public Mono handle(WebServerExchange exchange, Object handler) { HandlerMethod handlerMethod = (HandlerMethod) handler; InvocableHandlerMethod invocable = new InvocableHandlerMethod(handlerMethod); invocable.setHandlerMethodArgumentResolvers(this.argumentResolvers); - return invocable.invokeForRequest(request) - .map(result -> result.setExceptionHandler(ex -> handleException(ex, handlerMethod, request, response))) - .otherwise(ex -> handleException(ex, handlerMethod, request, response)); + return invocable.invokeForRequest(exchange) + .map(result -> result.setExceptionHandler(ex -> handleException(ex, handlerMethod, exchange))) + .otherwise(ex -> handleException(ex, handlerMethod, exchange)); } private Mono handleException(Throwable ex, HandlerMethod handlerMethod, - ServerHttpRequest request, ServerHttpResponse response) { + WebServerExchange exchange) { if (ex instanceof Exception) { InvocableHandlerMethod invocable = findExceptionHandler(handlerMethod, (Exception) ex); @@ -128,7 +125,7 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Initializin logger.debug("Invoking @ExceptionHandler method: " + invocable); } invocable.setHandlerMethodArgumentResolvers(getArgumentResolvers()); - return invocable.invokeForRequest(request, response, ex); + return invocable.invokeForRequest(exchange, ex); } catch (Exception invocationEx) { if (logger.isErrorEnabled()) { diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMapping.java index 476b0bc8f9..dff2c391e7 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMapping.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMapping.java @@ -41,6 +41,7 @@ import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.method.HandlerMethod; import org.springframework.web.method.HandlerMethodSelector; import org.springframework.web.reactive.HandlerMapping; +import org.springframework.web.server.WebServerExchange; /** @@ -65,9 +66,7 @@ public class RequestMappingHandlerMapping implements HandlerMapping, @Override public void afterPropertiesSet() throws Exception { - for (Object bean : this.applicationContext.getBeansOfType(Object.class).values()) { - detectHandlerMethods(bean); - } + this.applicationContext.getBeansOfType(Object.class).values().forEach(this::detectHandlerMethods); } protected void detectHandlerMethods(final Object bean) { @@ -94,15 +93,15 @@ public class RequestMappingHandlerMapping implements HandlerMapping, } @Override - public Mono getHandler(ServerHttpRequest request) { + public Mono getHandler(WebServerExchange exchange) { return Flux.create(subscriber -> { for (Map.Entry entry : this.methodMap.entrySet()) { RequestMappingInfo info = entry.getKey(); - if (info.matchesRequest(request)) { + if (info.matchesRequest(exchange.getRequest())) { HandlerMethod handlerMethod = entry.getValue(); if (logger.isDebugEnabled()) { - logger.debug("Mapped " + request.getMethod() + " " + - request.getURI().getPath() + " to [" + handlerMethod + "]"); + logger.debug("Mapped " + exchange.getRequest().getMethod() + " " + + exchange.getRequest().getURI().getPath() + " to [" + handlerMethod + "]"); } subscriber.onNext(handlerMethod); break; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestParamArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestParamArgumentResolver.java index 8ba175976b..8c056f4c37 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestParamArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestParamArgumentResolver.java @@ -19,9 +19,9 @@ package org.springframework.web.reactive.method.annotation; import reactor.Mono; import org.springframework.core.MethodParameter; -import org.springframework.http.server.reactive.ServerHttpRequest; -import org.springframework.web.reactive.method.HandlerMethodArgumentResolver; import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.reactive.method.HandlerMethodArgumentResolver; +import org.springframework.web.server.WebServerExchange; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; @@ -41,10 +41,10 @@ public class RequestParamArgumentResolver implements HandlerMethodArgumentResolv @Override - public Mono resolveArgument(MethodParameter param, ServerHttpRequest request) { + public Mono resolveArgument(MethodParameter param, WebServerExchange exchange) { RequestParam annotation = param.getParameterAnnotation(RequestParam.class); String name = (annotation.value().length() != 0 ? annotation.value() : param.getParameterName()); - UriComponents uriComponents = UriComponentsBuilder.fromUri(request.getURI()).build(); + UriComponents uriComponents = UriComponentsBuilder.fromUri(exchange.getRequest().getURI()).build(); String value = uriComponents.getQueryParams().getFirst(name); return (value != null ? Mono.just(value) : Mono.empty()); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java index 05e3a2adc0..0d9422266f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java @@ -33,11 +33,11 @@ import reactor.Mono; import org.springframework.core.Ordered; import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.codec.Encoder; import org.springframework.core.convert.ConversionService; import org.springframework.http.MediaType; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; -import org.springframework.core.codec.Encoder; import org.springframework.util.Assert; import org.springframework.util.MimeType; import org.springframework.web.HttpMediaTypeNotAcceptableException; @@ -45,6 +45,7 @@ import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.HandlerResultHandler; +import org.springframework.web.server.WebServerExchange; /** @@ -127,8 +128,7 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered @Override @SuppressWarnings("unchecked") - public Mono handleResult(ServerHttpRequest request, - ServerHttpResponse response, HandlerResult result) { + public Mono handleResult(WebServerExchange exchange, HandlerResult result) { Object value = result.getResult(); if (value == null) { @@ -147,7 +147,7 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered elementType = returnType; } - List requestedMediaTypes = getAcceptableMediaTypes(request); + List requestedMediaTypes = getAcceptableMediaTypes(exchange.getRequest()); List producibleMediaTypes = getProducibleMediaTypes(elementType); if (producibleMediaTypes.isEmpty()) { @@ -184,6 +184,7 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered if (selectedMediaType != null) { Encoder encoder = resolveEncoder(elementType, selectedMediaType); if (encoder != null) { + ServerHttpResponse response = exchange.getResponse(); response.getHeaders().setContentType(selectedMediaType); return response.setBody(encoder.encode((Publisher) publisher, elementType, selectedMediaType)); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/DefaultWebServerExchange.java b/spring-web-reactive/src/main/java/org/springframework/web/server/DefaultWebServerExchange.java new file mode 100644 index 0000000000..fe5cb4f5db --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/DefaultWebServerExchange.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2015 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.web.server; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.util.Assert; + +/** + * Default implementation of {@link WebServerExchange}. + * + * @author Rossen Stoyanchev + */ +public class DefaultWebServerExchange implements WebServerExchange { + + private final ServerHttpRequest request; + + private final ServerHttpResponse response; + + private final Map attributes = new ConcurrentHashMap<>(); + + + public DefaultWebServerExchange(ServerHttpRequest request, ServerHttpResponse response) { + Assert.notNull(request, "'request' is required."); + Assert.notNull(response, "'response' is required."); + this.request = request; + this.response = response; + } + + + @Override + public ServerHttpRequest getRequest() { + return this.request; + } + + @Override + public ServerHttpResponse getResponse() { + return this.response; + } + + @Override + public Map getAttributes() { + return this.attributes; + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/ErrorHandlingHttpHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/server/ErrorHandlingHttpHandler.java deleted file mode 100644 index 3edbcfc50e..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/ErrorHandlingHttpHandler.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2002-2015 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.web.server; - -import java.util.Arrays; -import java.util.List; - -import reactor.Mono; - -import org.springframework.http.server.reactive.HttpHandler; -import org.springframework.http.server.reactive.ServerHttpRequest; -import org.springframework.http.server.reactive.ServerHttpResponse; -import org.springframework.util.Assert; - -/** - * {@link HttpHandler} that delegates to a target {@link HttpHandler} and handles - * any errors from it by invoking one or more {@link HttpExceptionHandler}s - * sequentially until one of them completes successfully. - * - * @author Rossen Stoyanchev - * @author Stephane Maldini - */ -public class ErrorHandlingHttpHandler extends HttpHandlerDecorator { - - private final List exceptionHandlers; - - - public ErrorHandlingHttpHandler(HttpHandler targetHandler, HttpExceptionHandler... exceptionHandlers) { - super(targetHandler); - Assert.notEmpty(exceptionHandlers, "At least one exception handler is required"); - this.exceptionHandlers = Arrays.asList(exceptionHandlers); - } - - - @Override - public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { - Mono mono; - try { - mono = getDelegate().handle(request, response); - } - catch (Throwable ex) { - mono = Mono.error(ex); - } - for (HttpExceptionHandler handler : this.exceptionHandlers) { - mono = applyExceptionHandler(mono, handler, request, response); - } - return mono; - } - - private static Mono applyExceptionHandler(Mono mono, HttpExceptionHandler handler, - ServerHttpRequest request, ServerHttpResponse response) { - - return mono.otherwise(ex -> handler.handle(request, response, ex)).after(); - } - -} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/ExceptionHandlingWebHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/server/ExceptionHandlingWebHandler.java new file mode 100644 index 0000000000..2842bc801b --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/ExceptionHandlingWebHandler.java @@ -0,0 +1,74 @@ +/* + * Copyright 2002-2015 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.web.server; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import reactor.Mono; + +import org.springframework.http.HttpStatus; + +/** + * {@code WebHandler} that decorates another with exception handling using one + * or more instances of {@link WebExceptionHandler}. + * + * @author Rossen Stoyanchev + */ +public class ExceptionHandlingWebHandler extends WebHandlerDecorator { + + private final List exceptionHandlers; + + + public ExceptionHandlingWebHandler(WebHandler delegate, WebExceptionHandler... exceptionHandlers) { + super(delegate); + this.exceptionHandlers = initList(exceptionHandlers); + } + + private static List initList(WebExceptionHandler[] list) { + return (list != null ? Collections.unmodifiableList(Arrays.asList(list)): Collections.emptyList()); + } + + + /** + * @return a read-only list of the configured exception handlers. + */ + public List getExceptionHandlers() { + return this.exceptionHandlers; + } + + @Override + public Mono handle(WebServerExchange exchange) { + Mono mono; + try { + mono = getDelegate().handle(exchange); + } + catch (Throwable ex) { + mono = Mono.error(ex); + } + for (WebExceptionHandler exceptionHandler : this.exceptionHandlers) { + mono = mono.otherwise(ex -> exceptionHandler.handle(exchange, ex)); + } + return mono.otherwise(ex -> handleUnresolvedException(exchange)); + } + + private Mono handleUnresolvedException(WebServerExchange exchange) { + exchange.getResponse().setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR); + return Mono.empty(); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/FilterChainHttpHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/server/FilterChainHttpHandler.java deleted file mode 100644 index 2026ab3c07..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/FilterChainHttpHandler.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2002-2015 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.web.server; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -import reactor.Mono; - -import org.springframework.http.server.reactive.HttpHandler; -import org.springframework.http.server.reactive.ServerHttpRequest; -import org.springframework.http.server.reactive.ServerHttpResponse; - -/** - * {@link HttpHandler} that delegates to a chain of {@link HttpFilter}s followed - * by a target {@link HttpHandler}. - * - * @author Rossen Stoyanchev - */ -public class FilterChainHttpHandler extends HttpHandlerDecorator { - - private final List filters; - - - public FilterChainHttpHandler(HttpHandler targetHandler, HttpFilter... filters) { - super(targetHandler); - this.filters = (filters != null ? Arrays.asList(filters) : Collections.emptyList()); - } - - - @Override - public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { - return new DefaultHttpFilterChain().filter(request, response); - } - - - private class DefaultHttpFilterChain implements HttpFilterChain { - - private int index; - - @Override - public Mono filter(ServerHttpRequest request, ServerHttpResponse response) { - if (this.index < filters.size()) { - HttpFilter filter = filters.get(this.index++); - return filter.filter(request, response, this); - } - else { - return getDelegate().handle(request, response); - } - } - } - -} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/FilteringWebHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/server/FilteringWebHandler.java new file mode 100644 index 0000000000..128bbed47e --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/FilteringWebHandler.java @@ -0,0 +1,74 @@ +/* + * Copyright 2002-2015 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.web.server; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import reactor.Mono; + +/** + * {@code WebHandler} that decorates another with a chain of {@link WebFilter}s. + * + * @author Rossen Stoyanchev + */ +public class FilteringWebHandler extends WebHandlerDecorator { + + private final List filters; + + + public FilteringWebHandler(WebHandler targetHandler, WebFilter... filters) { + super(targetHandler); + this.filters = initList(filters); + } + + private static List initList(WebFilter[] list) { + return (list != null ? Collections.unmodifiableList(Arrays.asList(list)): Collections.emptyList()); + } + + + /** + * @return a read-only list of the configured filters. + */ + public List getFilters() { + return this.filters; + } + + @Override + public Mono handle(WebServerExchange exchange) { + return new DefaultWebFilterChain().filter(exchange); + } + + + private class DefaultWebFilterChain implements WebFilterChain { + + private int index; + + + @Override + public Mono filter(WebServerExchange exchange) { + if (this.index < filters.size()) { + WebFilter filter = filters.get(this.index++); + return filter.filter(exchange, this); + } + else { + return getDelegate().handle(exchange); + } + } + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/HttpFilter.java b/spring-web-reactive/src/main/java/org/springframework/web/server/HttpFilter.java deleted file mode 100644 index e1433a26f7..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/HttpFilter.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2002-2015 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.web.server; - -import reactor.Mono; - -import org.springframework.http.server.reactive.HttpHandler; -import org.springframework.http.server.reactive.ServerHttpRequest; -import org.springframework.http.server.reactive.ServerHttpResponse; - -/** - * Contract for interception-style, chained processing of HTTP requests. - * - *

    Filters may be used to implement cross-cutting, application-agnostic - * requirements such as security, timeouts, and others. - * - *

    {@link FilterChainHttpHandler} provides a way of constructing a chain of - * {@link HttpFilter}s followed by a target {@link HttpHandler}. - * - * @author Rossen Stoyanchev - * @see FilterChainHttpHandler - */ -public interface HttpFilter { - - /** - * Process the given request and optionally delegate to the next HttpFilter. - * - * @param request current HTTP request. - * @param response current HTTP response. - * @param chain provides a way to delegate to the next HttpFilter. - * @return {@code Mono} to indicate when request processing is complete. - */ - Mono filter(ServerHttpRequest request, ServerHttpResponse response, - HttpFilterChain chain); - -} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/WebExceptionHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/server/WebExceptionHandler.java new file mode 100644 index 0000000000..84c12a65c5 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/WebExceptionHandler.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2015 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.web.server; + +import reactor.Mono; + +/** + * Contract for handling exceptions during web server exchange processing. + * + * @author Rossen Stoyanchev + */ +public interface WebExceptionHandler { + + /** + * Handle the given exception. A completion signal through the return value + * indicates error handling is complete while an error signal indicates the + * exception is still not handled. + * + * @param exchange the current exchange + * @param ex the exception to handle + * @return {@code Mono} to indicate when exception handling is complete + */ + Mono handle(WebServerExchange exchange, Throwable ex); + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/WebFilter.java b/spring-web-reactive/src/main/java/org/springframework/web/server/WebFilter.java new file mode 100644 index 0000000000..06c8b5e85c --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/WebFilter.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2015 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.web.server; + +import reactor.Mono; + +/** + * Contract for interception-style, chained processing of Web requests that may + * be used to implement cross-cutting, application-agnostic requirements such + * as security, timeouts, and others. + * + * @author Rossen Stoyanchev + */ +public interface WebFilter { + + /** + * Process the Web request and (optionally) delegate to the next + * {@code WebFilter} through the given {@link WebFilterChain}. + * + * @param exchange the current server exchange + * @param chain provides a way to delegate to the next filter + * @return {@code Mono} to indicate when request processing is complete + */ + Mono filter(WebServerExchange exchange, WebFilterChain chain); + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/HttpFilterChain.java b/spring-web-reactive/src/main/java/org/springframework/web/server/WebFilterChain.java similarity index 63% rename from spring-web-reactive/src/main/java/org/springframework/web/server/HttpFilterChain.java rename to spring-web-reactive/src/main/java/org/springframework/web/server/WebFilterChain.java index 5188785610..b071f97e73 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/HttpFilterChain.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/WebFilterChain.java @@ -17,23 +17,19 @@ package org.springframework.web.server; import reactor.Mono; -import org.springframework.http.server.reactive.ServerHttpRequest; -import org.springframework.http.server.reactive.ServerHttpResponse; - /** - * Represents a chain of {@link HttpFilter}s allowing each {@link HttpFilter} to - * delegate to the next in the chain. + * Contract to allow a {@link WebFilter} to delegate to the next in the chain. * * @author Rossen Stoyanchev */ -public interface HttpFilterChain { +public interface WebFilterChain { /** + * Delegate to the next {@code WebFilter} in the chain. * - * @param request current HTTP request. - * @param response current HTTP response. - * @return {@code Mono} to indicate when request handling is complete. + * @param exchange the current server exchange + * @return {@code Mono} to indicate when request handling is complete */ - Mono filter(ServerHttpRequest request, ServerHttpResponse response); + Mono filter(WebServerExchange exchange); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/WebHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/server/WebHandler.java new file mode 100644 index 0000000000..58cb9286fb --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/WebHandler.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2015 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.web.server; + +import reactor.Mono; + +/** + * Contract to handle a web server exchange. + * + *

    Use {@link WebToHttpHandlerAdapter} to adapt a {@code WebHandler} to an + * {@link org.springframework.http.server.reactive.HttpHandler HttpHandler}. + * The {@link WebToHttpHandlerBuilder} provides a convenient way to do that while + * also optionally configuring one or more filters and/or exception handlers. + * + * @author Rossen Stoyanchev + * @see WebToHttpHandlerBuilder + */ +public interface WebHandler { + + /** + * Handle the web server exchange. + * + * @param exchange the current server exchange + * @return {@code Mono} to indicate when request handling is complete + */ + Mono handle(WebServerExchange exchange); + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/HttpHandlerDecorator.java b/spring-web-reactive/src/main/java/org/springframework/web/server/WebHandlerDecorator.java similarity index 66% rename from spring-web-reactive/src/main/java/org/springframework/web/server/HttpHandlerDecorator.java rename to spring-web-reactive/src/main/java/org/springframework/web/server/WebHandlerDecorator.java index c2062755ce..42cc20e3ec 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/HttpHandlerDecorator.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/WebHandlerDecorator.java @@ -17,34 +17,32 @@ package org.springframework.web.server; import reactor.Mono; -import org.springframework.http.server.reactive.HttpHandler; -import org.springframework.http.server.reactive.ServerHttpRequest; -import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.util.Assert; /** + * Base class for a {@link WebHandler} that decorates and delegates to another. * * @author Rossen Stoyanchev */ -public class HttpHandlerDecorator implements HttpHandler { +public class WebHandlerDecorator implements WebHandler { - private final HttpHandler delegate; + private final WebHandler delegate; - public HttpHandlerDecorator(HttpHandler delegate) { + public WebHandlerDecorator(WebHandler delegate) { Assert.notNull(delegate, "'delegate' must not be null"); this.delegate = delegate; } - public HttpHandler getDelegate() { + public WebHandler getDelegate() { return this.delegate; } @Override - public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { - return this.delegate.handle(request, response); + public Mono handle(WebServerExchange exchange) { + return this.delegate.handle(exchange); } @Override diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/InternalServerErrorExceptionHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/server/WebServerExchange.java similarity index 60% rename from spring-web-reactive/src/main/java/org/springframework/web/server/InternalServerErrorExceptionHandler.java rename to spring-web-reactive/src/main/java/org/springframework/web/server/WebServerExchange.java index a5580d794a..87fbf7ac3c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/InternalServerErrorExceptionHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/WebServerExchange.java @@ -15,24 +15,33 @@ */ package org.springframework.web.server; -import reactor.Mono; +import java.util.Map; -import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; /** - * Handle any exception by setting the response status to 500. + * Contract for an HTTP request-response interaction. Provides access to the HTTP + * request and response and also exposes additional server-side processing + * related properties and features such as request attributes. * * @author Rossen Stoyanchev */ -public class InternalServerErrorExceptionHandler implements HttpExceptionHandler { +public interface WebServerExchange { + /** + * @return the current HTTP request + */ + ServerHttpRequest getRequest(); - @Override - public Mono handle(ServerHttpRequest request, ServerHttpResponse response, Throwable ex) { - response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR); - return Mono.empty(); - } + /** + * @return the current HTTP response + */ + ServerHttpResponse getResponse(); + + /** + * @return mutable map of request attributes for the current exchange + */ + Map getAttributes(); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/HttpExceptionHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/server/WebToHttpHandlerAdapter.java similarity index 54% rename from spring-web-reactive/src/main/java/org/springframework/web/server/HttpExceptionHandler.java rename to spring-web-reactive/src/main/java/org/springframework/web/server/WebToHttpHandlerAdapter.java index 2d0dc80113..b2d45e7b6f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/HttpExceptionHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/WebToHttpHandlerAdapter.java @@ -22,26 +22,26 @@ import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; /** - * A contract for resolving exceptions from HTTP request handling. - * - *

    {@link ErrorHandlingHttpHandler} provides a way of applying a list - * {@link HttpExceptionHandler}s to a target {@link HttpHandler}. + * Adapt {@link WebHandler} to {@link HttpHandler} also creating the + * {@link WebServerExchange} before invoking the target {@code WebHandler}. * * @author Rossen Stoyanchev - * @see ErrorHandlingHttpHandler */ -public interface HttpExceptionHandler { +public class WebToHttpHandlerAdapter extends WebHandlerDecorator implements HttpHandler { - /** - * Handle the given exception and return a completion Publisher to indicate - * when error handling is complete, or send an error signal if the exception - * was not handled. - * - * @param request the current request - * @param response the current response - * @param ex the exception to handle - * @return {@code Mono} to indicate when exception handling is complete. - */ - Mono handle(ServerHttpRequest request, ServerHttpResponse response, Throwable ex); + + public WebToHttpHandlerAdapter(WebHandler delegate) { + super(delegate); + } + + @Override + public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { + WebServerExchange exchange = createWebServerExchange(request, response); + return getDelegate().handle(exchange); + } + + protected WebServerExchange createWebServerExchange(ServerHttpRequest request, ServerHttpResponse response) { + return new DefaultWebServerExchange(request, response); + } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/WebToHttpHandlerBuilder.java b/spring-web-reactive/src/main/java/org/springframework/web/server/WebToHttpHandlerBuilder.java new file mode 100644 index 0000000000..62a133a35e --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/WebToHttpHandlerBuilder.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2015 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.web.server; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * Assist with building an + * {@link org.springframework.http.server.reactive.HttpHandler HttpHandler} to + * invoke a target {@link WebHandler} with an optional chain of + * {@link WebFilter}s and one or more {@link WebExceptionHandler}s. + * + *

    Effective this sets up the following {@code WebHandler} delegation:
    + * {@link WebToHttpHandlerAdapter} {@code -->} + * {@link ExceptionHandlingWebHandler} {@code -->} + * {@link FilteringWebHandler} + * + * @author Rossen Stoyanchev + */ +public class WebToHttpHandlerBuilder { + + private final WebHandler targetHandler; + + private final List filters = new ArrayList<>(); + + private final List exceptionHandlers = new ArrayList<>(); + + + private WebToHttpHandlerBuilder(WebHandler targetHandler) { + Assert.notNull(targetHandler, "'targetHandler' must not be null"); + this.targetHandler = targetHandler; + } + + + public static WebToHttpHandlerBuilder webHandler(WebHandler webHandler) { + return new WebToHttpHandlerBuilder(webHandler); + } + + public WebToHttpHandlerBuilder filters(WebFilter... filters) { + if (!ObjectUtils.isEmpty(filters)) { + this.filters.addAll(Arrays.asList(filters)); + } + return this; + } + + public WebToHttpHandlerBuilder exceptionHandlers(WebExceptionHandler... exceptionHandlers) { + if (!ObjectUtils.isEmpty(exceptionHandlers)) { + this.exceptionHandlers.addAll(Arrays.asList(exceptionHandlers)); + } + return this; + } + + public WebToHttpHandlerAdapter build() { + WebHandler webHandler = this.targetHandler; + if (!this.exceptionHandlers.isEmpty()) { + WebExceptionHandler[] array = new WebExceptionHandler[this.exceptionHandlers.size()]; + webHandler = new ExceptionHandlingWebHandler(webHandler, this.exceptionHandlers.toArray(array)); + } + if (!this.filters.isEmpty()) { + WebFilter[] array = new WebFilter[this.filters.size()]; + webHandler = new FilteringWebHandler(webHandler, this.filters.toArray(array)); + } + return new WebToHttpHandlerAdapter(webHandler); + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java index 5f7c3e3197..216fb15c2e 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java @@ -24,7 +24,7 @@ import org.junit.Before; import org.junit.Test; import org.reactivestreams.Publisher; import reactor.Mono; -import reactor.rx.Streams; +import reactor.rx.Stream; import reactor.rx.stream.Signal; import org.springframework.context.annotation.AnnotationConfigApplicationContext; @@ -36,25 +36,25 @@ import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.web.ResponseStatusException; -import org.springframework.web.server.ErrorHandlingHttpHandler; -import org.springframework.web.server.FilterChainHttpHandler; -import org.springframework.web.server.HttpExceptionHandler; -import org.springframework.web.server.HttpFilter; -import org.springframework.web.server.HttpFilterChain; -import org.springframework.http.server.reactive.HttpHandler; import org.springframework.http.server.reactive.MockServerHttpRequest; import org.springframework.http.server.reactive.MockServerHttpResponse; -import org.springframework.http.server.reactive.ServerHttpRequest; -import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.stereotype.Controller; import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.ResponseStatusException; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.reactive.method.annotation.RequestMappingHandlerAdapter; import org.springframework.web.reactive.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.reactive.method.annotation.ResponseBodyResultHandler; +import org.springframework.web.server.DefaultWebServerExchange; +import org.springframework.web.server.ExceptionHandlingWebHandler; +import org.springframework.web.server.FilteringWebHandler; +import org.springframework.web.server.WebExceptionHandler; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; +import org.springframework.web.server.WebHandler; +import org.springframework.web.server.WebServerExchange; import static org.hamcrest.CoreMatchers.startsWith; import static org.junit.Assert.assertEquals; @@ -80,6 +80,8 @@ public class DispatcherHandlerErrorTests { private MockServerHttpResponse response; + private WebServerExchange exchange; + @Before public void setUp() throws Exception { @@ -92,6 +94,7 @@ public class DispatcherHandlerErrorTests { this.request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); this.response = new MockServerHttpResponse(); + this.exchange = new DefaultWebServerExchange(this.request, this.response); } @@ -99,7 +102,7 @@ public class DispatcherHandlerErrorTests { public void noHandler() throws Exception { this.request.setUri(new URI("/does-not-exist")); - Publisher publisher = this.dispatcherHandler.handle(this.request, this.response); + Publisher publisher = this.dispatcherHandler.handle(this.exchange); Throwable ex = awaitErrorSignal(publisher); assertEquals(ResponseStatusException.class, ex.getClass()); @@ -111,7 +114,7 @@ public class DispatcherHandlerErrorTests { public void noResolverForArgument() throws Exception { this.request.setUri(new URI("/uknown-argument-type")); - Publisher publisher = this.dispatcherHandler.handle(this.request, this.response); + Publisher publisher = this.dispatcherHandler.handle(this.exchange); Throwable ex = awaitErrorSignal(publisher); assertEquals(IllegalStateException.class, ex.getClass()); @@ -122,7 +125,7 @@ public class DispatcherHandlerErrorTests { public void controllerMethodError() throws Exception { this.request.setUri(new URI("/error-signal")); - Publisher publisher = this.dispatcherHandler.handle(this.request, this.response); + Publisher publisher = this.dispatcherHandler.handle(this.exchange); Throwable ex = awaitErrorSignal(publisher); assertSame(EXCEPTION, ex); @@ -132,7 +135,7 @@ public class DispatcherHandlerErrorTests { public void controllerMethodWithThrownException() throws Exception { this.request.setUri(new URI("/raise-exception")); - Publisher publisher = this.dispatcherHandler.handle(this.request, this.response); + Publisher publisher = this.dispatcherHandler.handle(this.exchange); Throwable ex = awaitErrorSignal(publisher); assertSame(EXCEPTION, ex); @@ -142,7 +145,7 @@ public class DispatcherHandlerErrorTests { public void noHandlerResultHandler() throws Exception { this.request.setUri(new URI("/unknown-return-type")); - Publisher publisher = this.dispatcherHandler.handle(this.request, this.response); + Publisher publisher = this.dispatcherHandler.handle(this.exchange); Throwable ex = awaitErrorSignal(publisher); assertEquals(IllegalStateException.class, ex.getClass()); @@ -155,7 +158,7 @@ public class DispatcherHandlerErrorTests { this.request.getHeaders().setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); this.request.setBody(Mono.just(ByteBuffer.wrap("body".getBytes("UTF-8")))); - Publisher publisher = this.dispatcherHandler.handle(this.request, this.response); + Publisher publisher = this.dispatcherHandler.handle(this.exchange); Throwable ex = awaitErrorSignal(publisher); assertEquals(ResponseStatusException.class, ex.getClass()); @@ -168,7 +171,7 @@ public class DispatcherHandlerErrorTests { this.request.setUri(new URI("/request-body")); this.request.setBody(Mono.error(EXCEPTION)); - Publisher publisher = this.dispatcherHandler.handle(this.request, this.response); + Publisher publisher = this.dispatcherHandler.handle(this.exchange); Throwable ex = awaitErrorSignal(publisher); ex.printStackTrace(); @@ -180,11 +183,11 @@ public class DispatcherHandlerErrorTests { public void dispatcherHandlerWithHttpExceptionHandler() throws Exception { this.request.setUri(new URI("/uknown-argument-type")); - HttpExceptionHandler exceptionHandler = new ServerError500ExceptionHandler(); - HttpHandler httpHandler = new ErrorHandlingHttpHandler(this.dispatcherHandler, exceptionHandler); - Publisher publisher = httpHandler.handle(this.request, this.response); + WebExceptionHandler exceptionHandler = new ServerError500ExceptionHandler(); + WebHandler webHandler = new ExceptionHandlingWebHandler(this.dispatcherHandler, exceptionHandler); + Publisher publisher = webHandler.handle(this.exchange); - Streams.from(publisher).toList().get(); + Stream.from(publisher).toList().get(); assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, this.response.getStatus()); } @@ -192,18 +195,17 @@ public class DispatcherHandlerErrorTests { public void filterChainWithHttpExceptionHandler() throws Exception { this.request.setUri(new URI("/uknown-argument-type")); - HttpHandler httpHandler; - httpHandler = new FilterChainHttpHandler(this.dispatcherHandler, new TestHttpFilter()); - httpHandler = new ErrorHandlingHttpHandler(httpHandler, new ServerError500ExceptionHandler()); - Publisher publisher = httpHandler.handle(this.request, this.response); + WebHandler webHandler = new FilteringWebHandler(this.dispatcherHandler, new TestWebFilter()); + webHandler = new ExceptionHandlingWebHandler(webHandler, new ServerError500ExceptionHandler()); + Publisher publisher = webHandler.handle(this.exchange); - Streams.from(publisher).toList().get(); + Stream.from(publisher).toList().get(); assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, this.response.getStatus()); } private Throwable awaitErrorSignal(Publisher publisher) throws Exception { - Signal signal = Streams.from(publisher).materialize().toList().get().get(0); + Signal signal = Stream.from(publisher).materialize().toList().get().get(0); assertEquals("Unexpected signal: " + signal, Signal.Type.ERROR, signal.getType()); return signal.getThrowable(); } @@ -269,20 +271,20 @@ public class DispatcherHandlerErrorTests { private static class Foo { } - private static class ServerError500ExceptionHandler implements HttpExceptionHandler { + private static class ServerError500ExceptionHandler implements WebExceptionHandler { @Override - public Mono handle(ServerHttpRequest request, ServerHttpResponse response, Throwable ex) { - response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR); + public Mono handle(WebServerExchange exchange, Throwable ex) { + exchange.getResponse().setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR); return Mono.empty(); } } - private static class TestHttpFilter implements HttpFilter { + private static class TestWebFilter implements WebFilter { @Override - public Mono filter(ServerHttpRequest req, ServerHttpResponse res, HttpFilterChain chain) { - return chain.filter(req, res); + public Mono filter(WebServerExchange exchange, WebFilterChain chain) { + return chain.filter(exchange); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java index 3744ecaa6e..68a2ce3c72 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java @@ -17,12 +17,11 @@ package org.springframework.web.reactive; import java.net.URI; import java.util.List; -import java.util.concurrent.TimeUnit; import org.junit.Before; import org.junit.Test; import org.reactivestreams.Publisher; -import reactor.rx.Streams; +import reactor.rx.Stream; import reactor.rx.stream.Signal; import org.springframework.http.HttpMethod; @@ -30,6 +29,8 @@ import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.MockServerHttpRequest; import org.springframework.http.server.reactive.MockServerHttpResponse; import org.springframework.web.ResponseStatusException; +import org.springframework.web.server.DefaultWebServerExchange; +import org.springframework.web.server.WebServerExchange; import static junit.framework.TestCase.assertSame; import static org.junit.Assert.assertEquals; @@ -42,34 +43,35 @@ public class ResponseStatusExceptionHandlerTests { private ResponseStatusExceptionHandler handler; - private MockServerHttpRequest request; - private MockServerHttpResponse response; + private WebServerExchange exchange; + @Before public void setUp() throws Exception { this.handler = new ResponseStatusExceptionHandler(); - this.request = new MockServerHttpRequest(HttpMethod.GET, new URI("/path")); + MockServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/path")); this.response = new MockServerHttpResponse(); + this.exchange = new DefaultWebServerExchange(request, this.response); } @Test public void handleException() throws Exception { Throwable ex = new ResponseStatusException(HttpStatus.BAD_REQUEST); - Publisher publisher = this.handler.handle(this.request, this.response, ex); + Publisher publisher = this.handler.handle(this.exchange, ex); - Streams.from(publisher).toList().get(); + Stream.from(publisher).toList().get(); assertEquals(HttpStatus.BAD_REQUEST, this.response.getStatus()); } @Test public void unresolvedException() throws Exception { Throwable ex = new IllegalStateException(); - Publisher publisher = this.handler.handle(this.request, this.response, ex); + Publisher publisher = this.handler.handle(this.exchange, ex); - List> signals = Streams.from(publisher).materialize().toList().get(); + List> signals = Stream.from(publisher).materialize().toList().get(); assertEquals(1, signals.size()); assertTrue(signals.get(0).hasError()); assertSame(ex, signals.get(0).getThrowable()); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java index 3507f005f4..8bd43ba830 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java @@ -31,7 +31,6 @@ import org.springframework.http.HttpStatus; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; import org.springframework.http.server.reactive.AbstractHttpHandlerIntegrationTests; -import org.springframework.web.server.ErrorHandlingHttpHandler; import org.springframework.http.server.reactive.HttpHandler; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; @@ -39,6 +38,9 @@ import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestTemplate; import org.springframework.web.reactive.DispatcherHandler; import org.springframework.web.reactive.ResponseStatusExceptionHandler; +import org.springframework.web.server.WebHandler; +import org.springframework.web.server.WebServerExchange; +import org.springframework.web.server.WebToHttpHandlerBuilder; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; @@ -57,13 +59,16 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler StaticApplicationContext wac = new StaticApplicationContext(); wac.registerSingleton("hm", TestHandlerMapping.class); - wac.registerSingleton("ha", HttpHandlerAdapter.class); + wac.registerSingleton("ha", HttpHandlerHandlerAdapter.class); wac.registerSingleton("rh", SimpleHandlerResultHandler.class); wac.refresh(); - DispatcherHandler dispatcherHandler = new DispatcherHandler(); - dispatcherHandler.setApplicationContext(wac); - return new ErrorHandlingHttpHandler(dispatcherHandler, new ResponseStatusExceptionHandler()); + DispatcherHandler webHandler = new DispatcherHandler(); + webHandler.setApplicationContext(wac); + + return WebToHttpHandlerBuilder.webHandler(webHandler) + .exceptionHandlers(new ResponseStatusExceptionHandler()) + .build(); } @Test @@ -132,27 +137,27 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler } } - private static class FooHandler implements HttpHandler { + private static class FooHandler implements WebHandler { @Override - public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { - return response.setBody(Stream.just(Buffer.wrap("foo").byteBuffer())); + public Mono handle(WebServerExchange exchange) { + return exchange.getResponse().setBody(Stream.just(Buffer.wrap("foo").byteBuffer())); } } - private static class BarHandler implements HttpHandler { + private static class BarHandler implements WebHandler { @Override - public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { - return response.setBody(Stream.just(Buffer.wrap("bar").byteBuffer())); + public Mono handle(WebServerExchange exchange) { + return exchange.getResponse().setBody(Stream.just(Buffer.wrap("bar").byteBuffer())); } } - private static class HeaderSettingHandler implements HttpHandler { + private static class HeaderSettingHandler implements WebHandler { @Override - public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { - response.getHeaders().add("foo", "bar"); + public Mono handle(WebServerExchange exchange) { + exchange.getResponse().getHeaders().add("foo", "bar"); return Mono.empty(); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java index 65a31fbd3a..fef38f5552 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java @@ -27,14 +27,17 @@ import org.junit.Test; import org.reactivestreams.Publisher; import reactor.Flux; import reactor.Mono; -import reactor.rx.Streams; +import reactor.rx.Stream; import reactor.rx.stream.Signal; import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.method.annotation.RequestParamArgumentResolver; +import org.springframework.web.server.DefaultWebServerExchange; +import org.springframework.web.server.WebServerExchange; import static org.junit.Assert.assertEquals; import static org.mockito.Matchers.any; @@ -49,10 +52,13 @@ public class InvocableHandlerMethodTests { private ServerHttpRequest request; + private WebServerExchange exchange; + @Before public void setUp() throws Exception { this.request = mock(ServerHttpRequest.class); + this.exchange = new DefaultWebServerExchange(request, mock(ServerHttpResponse.class)); } @@ -60,8 +66,8 @@ public class InvocableHandlerMethodTests { public void noArgsMethod() throws Exception { InvocableHandlerMethod hm = createHandlerMethod("noArgs"); - Publisher publisher = hm.invokeForRequest(this.request); - List values = Streams.from(publisher).toList().get(); + Publisher publisher = hm.invokeForRequest(this.exchange); + List values = Stream.from(publisher).toList().get(); assertEquals(1, values.size()); assertEquals("success", values.get(0).getResult()); @@ -73,8 +79,8 @@ public class InvocableHandlerMethodTests { InvocableHandlerMethod hm = createHandlerMethod("singleArg", String.class); hm.setHandlerMethodArgumentResolvers(Collections.singletonList(new RequestParamArgumentResolver())); - Publisher publisher = hm.invokeForRequest(this.request); - List values = Streams.from(publisher).toList().get(); + Publisher publisher = hm.invokeForRequest(this.exchange); + List values = Stream.from(publisher).toList().get(); assertEquals(1, values.size()); assertEquals("success:null", values.get(0).getResult()); @@ -85,8 +91,8 @@ public class InvocableHandlerMethodTests { InvocableHandlerMethod hm = createHandlerMethod("singleArg", String.class); addResolver(hm, Mono.just("value1")); - Publisher publisher = hm.invokeForRequest(this.request); - List values = Streams.from(publisher).toList().get(); + Publisher publisher = hm.invokeForRequest(this.exchange); + List values = Stream.from(publisher).toList().get(); assertEquals(1, values.size()); assertEquals("success:value1", values.get(0).getResult()); @@ -97,8 +103,8 @@ public class InvocableHandlerMethodTests { InvocableHandlerMethod hm = createHandlerMethod("singleArg", String.class); addResolver(hm, Flux.fromIterable(Arrays.asList("value1", "value2", "value3"))); - Publisher publisher = hm.invokeForRequest(this.request); - List values = Streams.from(publisher).toList().get(); + Publisher publisher = hm.invokeForRequest(this.exchange); + List values = Stream.from(publisher).toList().get(); assertEquals(1, values.size()); assertEquals("success:value1", values.get(0).getResult()); @@ -108,7 +114,7 @@ public class InvocableHandlerMethodTests { public void noResolverForArg() throws Exception { InvocableHandlerMethod hm = createHandlerMethod("singleArg", String.class); - Publisher publisher = hm.invokeForRequest(this.request); + Publisher publisher = hm.invokeForRequest(this.exchange); Throwable ex = awaitErrorSignal(publisher); assertEquals(IllegalStateException.class, ex.getClass()); @@ -125,7 +131,7 @@ public class InvocableHandlerMethodTests { InvocableHandlerMethod hm = createHandlerMethod("singleArg", String.class); hm.setHandlerMethodArgumentResolvers(Collections.singletonList(resolver)); - Publisher publisher = hm.invokeForRequest(this.request); + Publisher publisher = hm.invokeForRequest(this.exchange); Throwable ex = awaitErrorSignal(publisher); assertEquals(IllegalStateException.class, ex.getClass()); @@ -139,7 +145,7 @@ public class InvocableHandlerMethodTests { InvocableHandlerMethod hm = createHandlerMethod("singleArg", String.class); addResolver(hm, Mono.error(new IllegalStateException("boo"))); - Publisher publisher = hm.invokeForRequest(this.request); + Publisher publisher = hm.invokeForRequest(this.exchange); Throwable ex = awaitErrorSignal(publisher); assertEquals(IllegalStateException.class, ex.getClass()); @@ -153,7 +159,7 @@ public class InvocableHandlerMethodTests { InvocableHandlerMethod hm = createHandlerMethod("singleArg", String.class); addResolver(hm, Mono.just(1)); - Publisher publisher = hm.invokeForRequest(this.request); + Publisher publisher = hm.invokeForRequest(this.exchange); Throwable ex = awaitErrorSignal(publisher); assertEquals(IllegalStateException.class, ex.getClass()); @@ -166,7 +172,7 @@ public class InvocableHandlerMethodTests { public void invocationTargetExceptionIsUnwrapped() throws Exception { InvocableHandlerMethod hm = createHandlerMethod("exceptionMethod"); - Publisher publisher = hm.invokeForRequest(this.request); + Publisher publisher = hm.invokeForRequest(this.exchange); Throwable ex = awaitErrorSignal(publisher); assertEquals(IllegalStateException.class, ex.getClass()); @@ -188,7 +194,7 @@ public class InvocableHandlerMethodTests { } private Throwable awaitErrorSignal(Publisher publisher) throws Exception { - Signal signal = Streams.from(publisher).materialize().toList().get().get(0); + Signal signal = Stream.from(publisher).materialize().toList().get().get(0); assertEquals("Unexpected signal: " + signal, Signal.Type.ERROR, signal.getType()); return signal.getThrowable(); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java index 0af8d8eadc..945c2b6ff2 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java @@ -22,16 +22,19 @@ import java.util.List; import org.junit.Before; import org.junit.Test; import org.reactivestreams.Publisher; -import reactor.rx.Streams; +import reactor.rx.Stream; import org.springframework.context.support.StaticApplicationContext; import org.springframework.http.HttpMethod; import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.method.HandlerMethod; +import org.springframework.web.server.DefaultWebServerExchange; +import org.springframework.web.server.WebServerExchange; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -57,7 +60,8 @@ public class RequestMappingHandlerMappingTests { @Test public void path() throws Exception { ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("boo")); - Publisher handlerPublisher = this.mapping.getHandler(request); + WebServerExchange exchange = new DefaultWebServerExchange(request, new MockServerHttpResponse()); + Publisher handlerPublisher = this.mapping.getHandler(exchange); HandlerMethod handlerMethod = toHandlerMethod(handlerPublisher); assertEquals(TestController.class.getMethod("boo"), handlerMethod.getMethod()); } @@ -65,19 +69,21 @@ public class RequestMappingHandlerMappingTests { @Test public void method() throws Exception { ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.POST, new URI("foo")); - Publisher handlerPublisher = this.mapping.getHandler(request); + WebServerExchange exchange = new DefaultWebServerExchange(request, new MockServerHttpResponse()); + Publisher handlerPublisher = this.mapping.getHandler(exchange); HandlerMethod handlerMethod = toHandlerMethod(handlerPublisher); assertEquals(TestController.class.getMethod("postFoo"), handlerMethod.getMethod()); request = new MockServerHttpRequest(HttpMethod.GET, new URI("foo")); - handlerPublisher = this.mapping.getHandler(request); + exchange = new DefaultWebServerExchange(request, new MockServerHttpResponse()); + handlerPublisher = this.mapping.getHandler(exchange); handlerMethod = toHandlerMethod(handlerPublisher); assertEquals(TestController.class.getMethod("getFoo"), handlerMethod.getMethod()); } private HandlerMethod toHandlerMethod(Publisher handlerPublisher) throws InterruptedException { assertNotNull(handlerPublisher); - List handlerList = Streams.from(handlerPublisher).toList().get(); + List handlerList = Stream.from(handlerPublisher).toList().get(); assertEquals(1, handlerList.size()); return (HandlerMethod) handlerList.get(0); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java index 5c82eadaf4..1175641212 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java @@ -61,6 +61,8 @@ import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.client.RestTemplate; import org.springframework.web.reactive.DispatcherHandler; import org.springframework.web.reactive.handler.SimpleHandlerResultHandler; +import org.springframework.web.server.WebToHttpHandlerAdapter; +import org.springframework.web.server.WebToHttpHandlerBuilder; import static org.junit.Assert.assertEquals; @@ -80,9 +82,10 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati this.wac.register(FrameworkConfig.class, ApplicationConfig.class); this.wac.refresh(); - DispatcherHandler dispatcherHandler = new DispatcherHandler(); - dispatcherHandler.setApplicationContext(this.wac); - return dispatcherHandler; + DispatcherHandler webHandler = new DispatcherHandler(); + webHandler.setApplicationContext(this.wac); + + return WebToHttpHandlerBuilder.webHandler(webHandler).build(); } @Test diff --git a/spring-web-reactive/src/test/java/org/springframework/web/server/ErrorHandlingHttpHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/server/ErrorHandlingHttpHandlerTests.java deleted file mode 100644 index 43b431c90d..0000000000 --- a/spring-web-reactive/src/test/java/org/springframework/web/server/ErrorHandlingHttpHandlerTests.java +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright 2002-2015 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.web.server; - - -import java.net.URI; - -import org.junit.Before; -import org.junit.Test; -import org.reactivestreams.Publisher; -import reactor.Mono; -import reactor.rx.Streams; -import reactor.rx.stream.Signal; - -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; -import org.springframework.http.server.reactive.HttpHandler; -import org.springframework.http.server.reactive.MockServerHttpRequest; -import org.springframework.http.server.reactive.MockServerHttpResponse; -import org.springframework.http.server.reactive.ServerHttpRequest; -import org.springframework.http.server.reactive.ServerHttpResponse; -import org.springframework.web.server.ErrorHandlingHttpHandler; -import org.springframework.web.server.HttpExceptionHandler; -import org.springframework.web.server.InternalServerErrorExceptionHandler; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; - -/** - * @author Rossen Stoyanchev - */ -@SuppressWarnings("ThrowableResultOfMethodCallIgnored") -public class ErrorHandlingHttpHandlerTests { - - private MockServerHttpRequest request; - - private MockServerHttpResponse response; - - - @Before - public void setUp() throws Exception { - this.request = new MockServerHttpRequest(HttpMethod.GET, new URI("http://localhost:8080")); - this.response = new MockServerHttpResponse(); - } - - - @Test - public void handleErrorSignal() throws Exception { - HttpExceptionHandler exceptionHandler = new InternalServerErrorExceptionHandler(); - HttpHandler targetHandler = new TestHttpHandler(new IllegalStateException("boo")); - HttpHandler handler = new ErrorHandlingHttpHandler(targetHandler, exceptionHandler); - - handler.handle(this.request, this.response).get(); - - assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, this.response.getStatus()); - } - - @Test - public void handleErrorSignalWithMultipleHttpErrorHandlers() throws Exception { - HttpExceptionHandler[] exceptionHandlers = new HttpExceptionHandler[] { - new UnresolvedExceptionHandler(), - new UnresolvedExceptionHandler(), - new InternalServerErrorExceptionHandler(), - new UnresolvedExceptionHandler() - }; - HttpHandler targetHandler = new TestHttpHandler(new IllegalStateException("boo")); - HttpHandler httpHandler = new ErrorHandlingHttpHandler(targetHandler, exceptionHandlers); - - httpHandler.handle(this.request, this.response).get(); - - assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, this.response.getStatus()); - } - - @Test - public void unresolvedException() throws Exception { - HttpExceptionHandler exceptionHandler = new UnresolvedExceptionHandler(); - HttpHandler targetHandler = new TestHttpHandler(new IllegalStateException("boo")); - HttpHandler httpHandler = new ErrorHandlingHttpHandler(targetHandler, exceptionHandler); - - Publisher publisher = httpHandler.handle(this.request, this.response); - Throwable ex = awaitErrorSignal(publisher); - - assertEquals("boo", ex.getMessage()); - assertNull(this.response.getStatus()); - } - - @Test - public void thrownExceptionBecomesErrorSignal() throws Exception { - HttpExceptionHandler exceptionHandler = new InternalServerErrorExceptionHandler(); - HttpHandler targetHandler = new TestHttpHandler(new IllegalStateException("boo"), true); - HttpHandler handler = new ErrorHandlingHttpHandler(targetHandler, exceptionHandler); - - handler.handle(this.request, this.response).get(); - - assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, this.response.getStatus()); - } - - - private Throwable awaitErrorSignal(Publisher publisher) throws Exception { - Signal signal = Streams.from(publisher).materialize().toList().get().get(0); - assertEquals("Unexpected signal: " + signal, Signal.Type.ERROR, signal.getType()); - return signal.getThrowable(); - } - - - private static class TestHttpHandler implements HttpHandler { - - private final RuntimeException exception; - - private final boolean raise; - - - public TestHttpHandler(RuntimeException exception) { - this(exception, false); - } - - public TestHttpHandler(RuntimeException exception, boolean raise) { - this.exception = exception; - this.raise = raise; - } - - @Override - public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { - if (this.raise) { - throw this.exception; - } - return Mono.error(this.exception); - } - } - - - /** Leave the exception unresolved. */ - private static class UnresolvedExceptionHandler implements HttpExceptionHandler { - - @Override - public Mono handle(ServerHttpRequest request, ServerHttpResponse response, Throwable ex) { - return Mono.error(ex); - } - } - -} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/server/ExceptionHandlingHttpHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/server/ExceptionHandlingHttpHandlerTests.java new file mode 100644 index 0000000000..48cf44bdd0 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/server/ExceptionHandlingHttpHandlerTests.java @@ -0,0 +1,140 @@ +/* + * Copyright 2002-2015 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.web.server; + + +import java.net.URI; + +import org.junit.Before; +import org.junit.Test; +import reactor.Mono; + +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; + +import static org.junit.Assert.assertEquals; + +/** + * @author Rossen Stoyanchev + */ +@SuppressWarnings("ThrowableResultOfMethodCallIgnored") +public class ExceptionHandlingHttpHandlerTests { + + private MockServerHttpResponse response; + + private WebServerExchange exchange; + + private WebHandler targetHandler; + + + @Before + public void setUp() throws Exception { + URI uri = new URI("http://localhost:8080"); + MockServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, uri); + this.response = new MockServerHttpResponse(); + this.exchange = new DefaultWebServerExchange(request, this.response); + this.targetHandler = new StubWebHandler(new IllegalStateException("boo")); + } + + + @Test + public void handleErrorSignal() throws Exception { + WebExceptionHandler exceptionHandler = new BadRequestExceptionHandler(); + createWebHandler(exceptionHandler).handle(this.exchange).get(); + + assertEquals(HttpStatus.BAD_REQUEST, this.response.getStatus()); + } + + @Test + public void handleErrorSignalWithMultipleHttpErrorHandlers() throws Exception { + WebExceptionHandler[] exceptionHandlers = new WebExceptionHandler[] { + new UnresolvedExceptionHandler(), + new UnresolvedExceptionHandler(), + new BadRequestExceptionHandler(), + new UnresolvedExceptionHandler() + }; + createWebHandler(exceptionHandlers).handle(this.exchange).get(); + + assertEquals(HttpStatus.BAD_REQUEST, this.response.getStatus()); + } + + @Test + public void unresolvedException() throws Exception { + WebExceptionHandler exceptionHandler = new UnresolvedExceptionHandler(); + createWebHandler(exceptionHandler).handle(this.exchange).get(); + + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, this.response.getStatus()); + } + + @Test + public void thrownExceptionBecomesErrorSignal() throws Exception { + WebExceptionHandler exceptionHandler = new BadRequestExceptionHandler(); + createWebHandler(exceptionHandler).handle(this.exchange).get(); + + assertEquals(HttpStatus.BAD_REQUEST, this.response.getStatus()); + } + + private WebHandler createWebHandler(WebExceptionHandler... handlers) { + return new ExceptionHandlingWebHandler(this.targetHandler, handlers); + } + + + private static class StubWebHandler implements WebHandler { + + private final RuntimeException exception; + + private final boolean raise; + + + public StubWebHandler(RuntimeException exception) { + this(exception, false); + } + + public StubWebHandler(RuntimeException exception, boolean raise) { + this.exception = exception; + this.raise = raise; + } + + @Override + public Mono handle(WebServerExchange exchange) { + if (this.raise) { + throw this.exception; + } + return Mono.error(this.exception); + } + } + + private static class BadRequestExceptionHandler implements WebExceptionHandler { + + @Override + public Mono handle(WebServerExchange exchange, Throwable ex) { + exchange.getResponse().setStatusCode(HttpStatus.BAD_REQUEST); + return Mono.empty(); + } + } + + /** Leave the exception unresolved. */ + private static class UnresolvedExceptionHandler implements WebExceptionHandler { + + @Override + public Mono handle(WebServerExchange exchange, Throwable ex) { + return Mono.error(ex); + } + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/server/FilterChainHttpHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/server/FilteringWebHandlerTests.java similarity index 61% rename from spring-web-reactive/src/test/java/org/springframework/web/server/FilterChainHttpHandlerTests.java rename to spring-web-reactive/src/test/java/org/springframework/web/server/FilteringWebHandlerTests.java index 370dd644db..e06a9104a2 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/server/FilterChainHttpHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/server/FilteringWebHandlerTests.java @@ -33,9 +33,9 @@ import static org.mockito.Mockito.mock; /** * @author Rossen Stoyanchev */ -public class FilterChainHttpHandlerTests { +public class FilteringWebHandlerTests { - private static Log logger = LogFactory.getLog(FilterChainHttpHandlerTests.class); + private static Log logger = LogFactory.getLog(FilteringWebHandlerTests.class); private ServerHttpRequest request; @@ -51,61 +51,60 @@ public class FilterChainHttpHandlerTests { @Test public void multipleFilters() throws Exception { - StubHandler handler = new StubHandler(); + StubWebHandler webHandler = new StubWebHandler(); TestFilter filter1 = new TestFilter(); TestFilter filter2 = new TestFilter(); TestFilter filter3 = new TestFilter(); - FilterChainHttpHandler filterHandler = new FilterChainHttpHandler(handler, filter1, filter2, filter3); - - filterHandler.handle(this.request, this.response).get(); + HttpHandler httpHandler = createHttpHandler(webHandler, filter1, filter2, filter3); + httpHandler.handle(this.request, this.response).get(); assertTrue(filter1.invoked()); assertTrue(filter2.invoked()); assertTrue(filter3.invoked()); - assertTrue(handler.invoked()); + assertTrue(webHandler.invoked()); } @Test public void zeroFilters() throws Exception { - StubHandler handler = new StubHandler(); - FilterChainHttpHandler filterHandler = new FilterChainHttpHandler(handler); + StubWebHandler webHandler = new StubWebHandler(); + HttpHandler httpHandler = createHttpHandler(webHandler); + httpHandler.handle(this.request, this.response).get(); - filterHandler.handle(this.request, this.response).get(); - - assertTrue(handler.invoked()); + assertTrue(webHandler.invoked()); } @Test public void shortcircuitFilter() throws Exception { - StubHandler handler = new StubHandler(); + StubWebHandler webHandler = new StubWebHandler(); TestFilter filter1 = new TestFilter(); ShortcircuitingFilter filter2 = new ShortcircuitingFilter(); TestFilter filter3 = new TestFilter(); - FilterChainHttpHandler filterHandler = new FilterChainHttpHandler(handler, filter1, filter2, filter3); - - filterHandler.handle(this.request, this.response).get(); + HttpHandler httpHandler = createHttpHandler(webHandler, filter1, filter2, filter3); + httpHandler.handle(this.request, this.response).get(); assertTrue(filter1.invoked()); assertTrue(filter2.invoked()); assertFalse(filter3.invoked()); - assertFalse(handler.invoked()); + assertFalse(webHandler.invoked()); } @Test public void asyncFilter() throws Exception { - StubHandler handler = new StubHandler(); + StubWebHandler webHandler = new StubWebHandler(); AsyncFilter filter = new AsyncFilter(); - FilterChainHttpHandler filterHandler = new FilterChainHttpHandler(handler, filter); - - filterHandler.handle(this.request, this.response).get(); + HttpHandler httpHandler = createHttpHandler(webHandler, filter); + httpHandler.handle(this.request, this.response).get(); assertTrue(filter.invoked()); - assertTrue(handler.invoked()); + assertTrue(webHandler.invoked()); + } + + private WebToHttpHandlerAdapter createHttpHandler(StubWebHandler webHandler, WebFilter... filters) { + return WebToHttpHandlerBuilder.webHandler(webHandler).filters(filters).build(); } - - private static class TestFilter implements HttpFilter { + private static class TestFilter implements WebFilter { private volatile boolean invoked; @@ -115,26 +114,20 @@ public class FilterChainHttpHandlerTests { } @Override - public Mono filter(ServerHttpRequest req, ServerHttpResponse res, - HttpFilterChain chain) { - + public Mono filter(WebServerExchange exchange, WebFilterChain chain) { this.invoked = true; - return doFilter(req, res, chain); + return doFilter(exchange, chain); } - public Mono doFilter(ServerHttpRequest req, ServerHttpResponse res, - HttpFilterChain chain) { - - return chain.filter(req, res); + public Mono doFilter(WebServerExchange exchange, WebFilterChain chain) { + return chain.filter(exchange); } } private static class ShortcircuitingFilter extends TestFilter { @Override - public Mono doFilter(ServerHttpRequest req, ServerHttpResponse res, - HttpFilterChain chain) { - + public Mono doFilter(WebServerExchange exchange, WebFilterChain chain) { return Mono.empty(); } } @@ -142,10 +135,10 @@ public class FilterChainHttpHandlerTests { private static class AsyncFilter extends TestFilter { @Override - public Mono doFilter(ServerHttpRequest req, ServerHttpResponse res, HttpFilterChain chain) { + public Mono doFilter(WebServerExchange exchange, WebFilterChain chain) { return doAsyncWork().then(asyncResult -> { logger.debug("Async result: " + asyncResult); - return chain.filter(req, res); + return chain.filter(exchange); }); } @@ -155,7 +148,7 @@ public class FilterChainHttpHandlerTests { } - private static class StubHandler implements HttpHandler { + private static class StubWebHandler implements WebHandler { private volatile boolean invoked; @@ -164,7 +157,7 @@ public class FilterChainHttpHandlerTests { } @Override - public Mono handle(ServerHttpRequest req, ServerHttpResponse res) { + public Mono handle(WebServerExchange exchange) { logger.trace("StubHandler invoked."); this.invoked = true; return Mono.empty(); From eedc90818f5b42d43d3486d37e5831ed20712274 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Sun, 10 Jan 2016 06:25:12 -0500 Subject: [PATCH 151/498] Re-introduce writeHeaders() in ServerHttpResponse This commit brings back the writeHeaders method on ServerHttpResponse that was once added (2a6a4f) and then removed (9c7151). This version is a little simpler since writeHeaders doesn't explicitly flush/send headers which runtimes are expected to do by default. Instead the main purpose of writeHeaders now is to ensure changes made via HttpHeaders are applied to the underlying runtime response at some point and we now do that once at the very end. This approach provides the most flexibility (vs keeping HttpHeaders in sync) because it allows a full and consistent set of mutative operations for both headers and cookies (to be added) regardless of the API exposed by the underlying runtime. --- .../http/ExtendedHttpHeaders.java | 103 ------------------ .../reactive/ReactorServerHttpResponse.java | 35 +++--- .../reactive/RxNettyServerHttpResponse.java | 36 +++--- .../server/reactive/ServerHttpResponse.java | 16 ++- .../reactive/ServletServerHttpResponse.java | 48 ++++---- .../reactive/UndertowServerHttpResponse.java | 35 +++--- .../web/server/WebToHttpHandlerAdapter.java | 2 +- .../reactive/MockServerHttpResponse.java | 4 + ...mpleUrlHandlerMappingIntegrationTests.java | 2 - 9 files changed, 87 insertions(+), 194 deletions(-) delete mode 100644 spring-web-reactive/src/main/java/org/springframework/http/ExtendedHttpHeaders.java diff --git a/spring-web-reactive/src/main/java/org/springframework/http/ExtendedHttpHeaders.java b/spring-web-reactive/src/main/java/org/springframework/http/ExtendedHttpHeaders.java deleted file mode 100644 index f5fa19922b..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/http/ExtendedHttpHeaders.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright 2002-2015 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; - -import java.util.ArrayList; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; - -/** - * Variant of HttpHeaders (to be merged into HttpHeaders) that supports the - * registration of {@link HeaderChangeListener}s. - * - *

    For use with HTTP server response implementations that wish to propagate - * header header changes to the underlying runtime as they occur. - * - * @author Rossen Stoyanchev - */ -public class ExtendedHttpHeaders extends HttpHeaders { - - private final List listeners = new ArrayList<>(1); - - - public ExtendedHttpHeaders() { - } - - public ExtendedHttpHeaders(HeaderChangeListener listener) { - this.listeners.add(listener); - } - - - @Override - public void add(String name, String value) { - for (HeaderChangeListener listener : this.listeners) { - listener.headerAdded(name, value); - } - super.add(name, value); - } - - @Override - public void set(String name, String value) { - List values = new LinkedList<>(); - values.add(value); - put(name, values); - } - - @Override - public List put(String key, List values) { - for (HeaderChangeListener listener : this.listeners) { - listener.headerPut(key, values); - } - return super.put(key, values); - } - - @Override - public List remove(Object key) { - for (HeaderChangeListener listener : this.listeners) { - listener.headerRemoved((String) key); - } - return super.remove(key); - } - - @Override - public void putAll(Map> map) { - for (Entry> entry : map.entrySet()) { - put(entry.getKey(), entry.getValue()); - } - super.putAll(map); - } - - @Override - public void clear() { - for (Entry> entry : super.entrySet()) { - remove(entry.getKey(), entry.getValue()); - } - super.clear(); - } - - - public interface HeaderChangeListener { - - void headerAdded(String name, String value); - - void headerPut(String key, List values); - - void headerRemoved(String key); - - } - -} diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java index 58a8947440..f1c7af00ef 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java @@ -16,7 +16,6 @@ package org.springframework.http.server.reactive; import java.nio.ByteBuffer; -import java.util.List; import org.reactivestreams.Publisher; import reactor.Flux; @@ -25,7 +24,6 @@ import reactor.io.buffer.Buffer; import reactor.io.net.http.HttpChannel; import reactor.io.net.http.model.Status; -import org.springframework.http.ExtendedHttpHeaders; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.util.Assert; @@ -42,11 +40,13 @@ public class ReactorServerHttpResponse implements ServerHttpResponse { private final HttpHeaders headers; + private boolean headersWritten = false; + public ReactorServerHttpResponse(HttpChannel response) { Assert.notNull("'response' must not be null."); this.channel = response; - this.headers = new ExtendedHttpHeaders(new ReactorHeaderChangeListener()); + this.headers = new HttpHeaders(); } @@ -61,7 +61,7 @@ public class ReactorServerHttpResponse implements ServerHttpResponse { @Override public HttpHeaders getHeaders() { - return this.headers; + return (this.headersWritten ? HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers); } @Override @@ -70,26 +70,19 @@ public class ReactorServerHttpResponse implements ServerHttpResponse { } protected Mono setBodyInternal(Publisher publisher) { + writeHeaders(); return Mono.from(getReactorChannel().writeWith(Flux.from(publisher).map(Buffer::new))); } - - private class ReactorHeaderChangeListener implements ExtendedHttpHeaders.HeaderChangeListener { - - @Override - public void headerAdded(String name, String value) { - getReactorChannel().responseHeaders().add(name, value); - } - - @Override - public void headerPut(String key, List values) { - getReactorChannel().responseHeaders().remove(key); - getReactorChannel().responseHeaders().add(key, values); - } - - @Override - public void headerRemoved(String key) { - getReactorChannel().responseHeaders().remove(key); + @Override + public void writeHeaders() { + if (!this.headersWritten) { + for (String name : this.headers.keySet()) { + for (String value : this.headers.get(name)) { + this.channel.responseHeaders().add(name, value); + } + } + this.headersWritten = true; } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java index 05ed55e8c3..c60eaaded8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java @@ -17,7 +17,6 @@ package org.springframework.http.server.reactive; import java.nio.ByteBuffer; -import java.util.List; import io.netty.handler.codec.http.HttpResponseStatus; import io.reactivex.netty.protocol.http.server.HttpServerResponse; @@ -27,7 +26,6 @@ import reactor.Mono; import reactor.core.publisher.convert.RxJava1Converter; import rx.Observable; -import org.springframework.http.ExtendedHttpHeaders; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.util.Assert; @@ -44,11 +42,13 @@ public class RxNettyServerHttpResponse implements ServerHttpResponse { private final HttpHeaders headers; + private boolean headersWritten = false; + public RxNettyServerHttpResponse(HttpServerResponse response) { Assert.notNull("'response', response must not be null."); this.response = response; - this.headers = new ExtendedHttpHeaders(new RxNettyHeaderChangeListener()); + this.headers = new HttpHeaders(); } @@ -63,7 +63,7 @@ public class RxNettyServerHttpResponse implements ServerHttpResponse { @Override public HttpHeaders getHeaders() { - return this.headers; + return (this.headersWritten ? HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers); } @Override @@ -72,6 +72,7 @@ public class RxNettyServerHttpResponse implements ServerHttpResponse { } protected Mono setBodyInternal(Publisher publisher) { + writeHeaders(); Observable content = RxJava1Converter.from(publisher).map(this::toBytes); Observable completion = getRxNettyResponse().writeBytes(content); return RxJava1Converter.from(completion).after(); @@ -83,26 +84,15 @@ public class RxNettyServerHttpResponse implements ServerHttpResponse { return bytes; } - - private class RxNettyHeaderChangeListener implements ExtendedHttpHeaders.HeaderChangeListener { - - @Override - public void headerAdded(String name, String value) { - getRxNettyResponse().addHeader(name, value); - } - - @Override - public void headerPut(String key, List values) { - getRxNettyResponse().removeHeader(key); - for (String value : values) { - getRxNettyResponse().addHeader(key, value); + @Override + public void writeHeaders() { + if (!this.headersWritten) { + for (String name : this.headers.keySet()) { + for (String value : this.headers.get(name)) + this.response.addHeader(name, value); } - } - - @Override - public void headerRemoved(String key) { - getRxNettyResponse().removeHeader(key); + this.headersWritten = true; } } -} +} \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java index 4b31cf19c3..82320e9858 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java @@ -16,6 +16,8 @@ package org.springframework.http.server.reactive; +import org.reactivestreams.Publisher; + import org.springframework.http.HttpStatus; import org.springframework.http.ReactiveHttpOutputMessage; @@ -31,5 +33,17 @@ public interface ServerHttpResponse extends ReactiveHttpOutputMessage { * @param status the HTTP status as an {@link HttpStatus} enum value */ void setStatusCode(HttpStatus status); - + + /** + * Use this method to apply header changes made via {@link #getHeaders()} to + * the underlying server response. By default changes made via + * {@link #getHeaders()} are cached until a call to {@link #setBody} + * implicitly applies header changes or until this method is called. + * + *

    Note: After this method is called, + * {@link #getHeaders() headers} become read-only and any additional calls + * to this method are ignored. + */ + void writeHeaders(); + } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java index 0fc838af41..27b6798955 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java @@ -17,7 +17,9 @@ package org.springframework.http.server.reactive; import java.nio.ByteBuffer; +import java.nio.charset.Charset; import java.util.List; +import java.util.Map; import java.util.function.Function; import javax.servlet.http.HttpServletResponse; @@ -25,9 +27,9 @@ import org.reactivestreams.Publisher; import reactor.Flux; import reactor.Mono; -import org.springframework.http.ExtendedHttpHeaders; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.util.Assert; /** @@ -43,6 +45,8 @@ public class ServletServerHttpResponse implements ServerHttpResponse { private final HttpHeaders headers; + private boolean headersWritten = false; + public ServletServerHttpResponse(HttpServletResponse response, Function, Mono> responseBodyWriter) { @@ -51,7 +55,7 @@ public class ServletServerHttpResponse implements ServerHttpResponse { Assert.notNull(responseBodyWriter, "'responseBodyWriter' must not be null"); this.response = response; this.responseBodyWriter = responseBodyWriter; - this.headers = new ExtendedHttpHeaders(new ServletHeaderChangeListener()); + this.headers = new HttpHeaders(); } @@ -66,7 +70,7 @@ public class ServletServerHttpResponse implements ServerHttpResponse { @Override public HttpHeaders getHeaders() { - return this.headers; + return (this.headersWritten ? HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers); } @Override @@ -75,29 +79,29 @@ public class ServletServerHttpResponse implements ServerHttpResponse { } protected Mono setBodyInternal(Publisher publisher) { + writeHeaders(); return this.responseBodyWriter.apply(publisher); } - - private class ServletHeaderChangeListener implements ExtendedHttpHeaders.HeaderChangeListener { - - @Override - public void headerAdded(String name, String value) { - getServletResponse().addHeader(name, value); - } - - @Override - public void headerPut(String key, List values) { - // We can only add but not remove - for (String value : values) { - getServletResponse().addHeader(key, value); + @Override + public void writeHeaders() { + if (!this.headersWritten) { + for (Map.Entry> entry : this.headers.entrySet()) { + String headerName = entry.getKey(); + for (String headerValue : entry.getValue()) { + this.response.addHeader(headerName, headerValue); + } } - } - - @Override - public void headerRemoved(String key) { - // No Servlet support for removing headers + MediaType contentType = this.headers.getContentType(); + if (this.response.getContentType() == null && contentType != null) { + this.response.setContentType(contentType.toString()); + } + Charset charset = (contentType != null ? contentType.getCharSet() : null); + if (this.response.getCharacterEncoding() == null && charset != null) { + this.response.setCharacterEncoding(charset.name()); + } + this.headersWritten = true; } } -} +} \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java index 2d303c4cd0..08032cf1e2 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java @@ -18,6 +18,7 @@ package org.springframework.http.server.reactive; import java.nio.ByteBuffer; import java.util.List; +import java.util.Map; import java.util.function.Function; import io.undertow.server.HttpServerExchange; @@ -26,7 +27,6 @@ import org.reactivestreams.Publisher; import reactor.Flux; import reactor.Mono; -import org.springframework.http.ExtendedHttpHeaders; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.util.Assert; @@ -45,6 +45,8 @@ public class UndertowServerHttpResponse implements ServerHttpResponse { private final HttpHeaders headers; + private boolean headersWritten = false; + public UndertowServerHttpResponse(HttpServerExchange exchange, Function, Mono> responseBodyWriter) { @@ -53,7 +55,7 @@ public class UndertowServerHttpResponse implements ServerHttpResponse { Assert.notNull(responseBodyWriter, "'responseBodyWriter' must not be null"); this.exchange = exchange; this.responseBodyWriter = responseBodyWriter; - this.headers = new ExtendedHttpHeaders(new UndertowHeaderChangeListener()); + this.headers = new HttpHeaders(); } @@ -69,7 +71,7 @@ public class UndertowServerHttpResponse implements ServerHttpResponse { @Override public HttpHeaders getHeaders() { - return this.headers; + return (this.headersWritten ? HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers); } @Override @@ -78,27 +80,18 @@ public class UndertowServerHttpResponse implements ServerHttpResponse { } protected Mono setBodyInternal(Publisher publisher) { + writeHeaders(); return this.responseBodyWriter.apply(publisher); } - - private class UndertowHeaderChangeListener implements ExtendedHttpHeaders.HeaderChangeListener { - - @Override - public void headerAdded(String name, String value) { - HttpString headerName = HttpString.tryFromString(name); - getUndertowExchange().getResponseHeaders().add(headerName, value); - } - - @Override - public void headerPut(String key, List values) { - HttpString headerName = HttpString.tryFromString(key); - getUndertowExchange().getResponseHeaders().putAll(headerName, values); - } - - @Override - public void headerRemoved(String key) { - getUndertowExchange().getResponseHeaders().remove(key); + @Override + public void writeHeaders() { + if (!this.headersWritten) { + for (Map.Entry> entry : this.headers.entrySet()) { + HttpString headerName = HttpString.tryFromString(entry.getKey()); + this.exchange.getResponseHeaders().addAll(headerName, entry.getValue()); + } + this.headersWritten = true; } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/WebToHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/server/WebToHttpHandlerAdapter.java index b2d45e7b6f..aace5d7885 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/WebToHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/WebToHttpHandlerAdapter.java @@ -37,7 +37,7 @@ public class WebToHttpHandlerAdapter extends WebHandlerDecorator implements Http @Override public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { WebServerExchange exchange = createWebServerExchange(request, response); - return getDelegate().handle(exchange); + return getDelegate().handle(exchange).doOnTerminate((aVoid, ex) -> response.writeHeaders()); } protected WebServerExchange createWebServerExchange(ServerHttpRequest request, ServerHttpResponse response) { diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java index 0a282a95b3..0aab9caaf3 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java @@ -60,4 +60,8 @@ public class MockServerHttpResponse implements ServerHttpResponse { return this.body; } + @Override + public void writeHeaders() { + } + } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java index 8bd43ba830..dfc0fb1a56 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java @@ -32,8 +32,6 @@ import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; import org.springframework.http.server.reactive.AbstractHttpHandlerIntegrationTests; import org.springframework.http.server.reactive.HttpHandler; -import org.springframework.http.server.reactive.ServerHttpRequest; -import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestTemplate; import org.springframework.web.reactive.DispatcherHandler; From f8ef2e0220414cfb0ecf5dec9dea4e5a68fbce74 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Sun, 10 Jan 2016 07:26:26 -0500 Subject: [PATCH 152/498] Add base classes for ServerHttpRequest/Response impls --- .../reactive/AbstractServerHttpRequest.java | 69 ++++++++++++++ .../reactive/AbstractServerHttpResponse.java | 81 ++++++++++++++++ .../reactive/ReactorServerHttpRequest.java | 30 ++---- .../reactive/ReactorServerHttpResponse.java | 31 ++---- .../reactive/RxNettyServerHttpRequest.java | 30 ++---- .../reactive/RxNettyServerHttpResponse.java | 26 +---- .../reactive/ServletServerHttpRequest.java | 94 ++++++++----------- .../reactive/ServletServerHttpResponse.java | 48 +++------- .../reactive/UndertowServerHttpRequest.java | 36 ++----- .../reactive/UndertowServerHttpResponse.java | 30 +----- 10 files changed, 243 insertions(+), 232 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java new file mode 100644 index 0000000000..e7a35a8e6b --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2015 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.server.reactive; + +import java.net.URI; +import java.net.URISyntaxException; + +import org.springframework.http.HttpHeaders; + +/** + * Common base class for {@link ServerHttpRequest} implementations. + * + * @author Rossen Stoyanchev + */ +public abstract class AbstractServerHttpRequest implements ServerHttpRequest { + + private URI uri; + + private HttpHeaders headers; + + + @Override + public URI getURI() { + if (this.uri == null) { + try { + this.uri = initUri(); + } + catch (URISyntaxException ex) { + throw new IllegalStateException("Could not get URI: " + ex.getMessage(), ex); + } + } + return this.uri; + } + + /** + * Initialize a URI that represents the request. + * Invoked lazily on the first call to {@link #getURI()} and then cached. + * @throws URISyntaxException + */ + protected abstract URI initUri() throws URISyntaxException; + + @Override + public HttpHeaders getHeaders() { + if (this.headers == null) { + this.headers = HttpHeaders.readOnlyHttpHeaders(initHeaders()); + } + return this.headers; + } + + /** + * Initialize the headers from the underlying request. + * Invoked lazily on the first call to {@link #getHeaders()} and then cached. + */ + protected abstract HttpHeaders initHeaders(); + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java new file mode 100644 index 0000000000..2c87f89c46 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java @@ -0,0 +1,81 @@ +/* + * Copyright 2002-2015 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.server.reactive; + +import java.nio.ByteBuffer; + +import org.reactivestreams.Publisher; +import reactor.Flux; +import reactor.Mono; + +import org.springframework.http.HttpHeaders; + +/** + * Base class for {@link ServerHttpResponse} implementations. + * + * @author Rossen Stoyanchev + */ +public abstract class AbstractServerHttpResponse implements ServerHttpResponse { + + private final HttpHeaders headers; + + private boolean headersWritten = false; + + + protected AbstractServerHttpResponse() { + this.headers = new HttpHeaders(); + } + + + @Override + public HttpHeaders getHeaders() { + return (this.headersWritten ? HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers); + } + + @Override + public Mono setBody(Publisher publisher) { + return Flux.from(publisher).lift(new WriteWithOperator<>(writeWithPublisher -> { + writeHeaders(); + return setBodyInternal(writeWithPublisher); + })).after(); + } + + /** + * Implement this method to write to the underlying the response. + * @param publisher the publisher to write with + */ + protected abstract Mono setBodyInternal(Publisher publisher); + + @Override + public void writeHeaders() { + if (!this.headersWritten) { + try { + writeHeadersInternal(); + } + finally { + this.headersWritten = true; + } + } + } + + /** + * Implement this method to apply header changes from {@link #getHeaders()} + * to the underlying response. This method is protected from being called + * more than once. + */ + protected abstract void writeHeadersInternal(); + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java index 6d569c9352..e1f62514c7 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java @@ -32,14 +32,10 @@ import org.springframework.util.Assert; * * @author Stephane Maldini */ -public class ReactorServerHttpRequest implements ServerHttpRequest { +public class ReactorServerHttpRequest extends AbstractServerHttpRequest { private final HttpChannel channel; - private URI uri; - - private HttpHeaders headers; - public ReactorServerHttpRequest(HttpChannel request) { Assert.notNull("'request' must not be null."); @@ -57,27 +53,17 @@ public class ReactorServerHttpRequest implements ServerHttpRequest { } @Override - public URI getURI() { - if (this.uri == null) { - try { - this.uri = new URI(this.channel.uri()); - } - catch (URISyntaxException ex) { - throw new IllegalStateException("Could not get URI: " + ex.getMessage(), ex); - } - } - return this.uri; + protected URI initUri() throws URISyntaxException { + return new URI(this.channel.uri()); } @Override - public HttpHeaders getHeaders() { - if (this.headers == null) { - this.headers = new HttpHeaders(); - for (String name : this.channel.headers().names()) { - this.headers.put(name, this.channel.headers().getAll(name)); - } + protected HttpHeaders initHeaders() { + HttpHeaders headers = new HttpHeaders(); + for (String name : this.channel.headers().names()) { + headers.put(name, this.channel.headers().getAll(name)); } - return this.headers; + return headers; } @Override diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java index f1c7af00ef..64a44b94f6 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java @@ -24,7 +24,6 @@ import reactor.io.buffer.Buffer; import reactor.io.net.http.HttpChannel; import reactor.io.net.http.model.Status; -import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.util.Assert; @@ -34,19 +33,14 @@ import org.springframework.util.Assert; * @author Stephane Maldini * @author Rossen Stoyanchev */ -public class ReactorServerHttpResponse implements ServerHttpResponse { +public class ReactorServerHttpResponse extends AbstractServerHttpResponse { private final HttpChannel channel; - private final HttpHeaders headers; - - private boolean headersWritten = false; - public ReactorServerHttpResponse(HttpChannel response) { Assert.notNull("'response' must not be null."); this.channel = response; - this.headers = new HttpHeaders(); } @@ -60,29 +54,16 @@ public class ReactorServerHttpResponse implements ServerHttpResponse { } @Override - public HttpHeaders getHeaders() { - return (this.headersWritten ? HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers); - } - - @Override - public Mono setBody(Publisher publisher) { - return Flux.from(publisher).lift(new WriteWithOperator<>(this::setBodyInternal)).after(); - } - protected Mono setBodyInternal(Publisher publisher) { - writeHeaders(); - return Mono.from(getReactorChannel().writeWith(Flux.from(publisher).map(Buffer::new))); + return Mono.from(this.channel.writeWith(Flux.from(publisher).map(Buffer::new))); } @Override - public void writeHeaders() { - if (!this.headersWritten) { - for (String name : this.headers.keySet()) { - for (String value : this.headers.get(name)) { - this.channel.responseHeaders().add(name, value); - } + protected void writeHeadersInternal() { + for (String name : getHeaders().keySet()) { + for (String value : getHeaders().get(name)) { + this.channel.responseHeaders().add(name, value); } - this.headersWritten = true; } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java index 7e4683eed5..afdabae668 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java @@ -36,14 +36,10 @@ import org.springframework.util.Assert; * @author Rossen Stoyanchev * @author Stephane Maldini */ -public class RxNettyServerHttpRequest implements ServerHttpRequest { +public class RxNettyServerHttpRequest extends AbstractServerHttpRequest { private final HttpServerRequest request; - private URI uri; - - private HttpHeaders headers; - public RxNettyServerHttpRequest(HttpServerRequest request) { Assert.notNull("'request', request must not be null."); @@ -61,27 +57,17 @@ public class RxNettyServerHttpRequest implements ServerHttpRequest { } @Override - public URI getURI() { - if (this.uri == null) { - try { - this.uri = new URI(this.getRxNettyRequest().getUri()); - } - catch (URISyntaxException ex) { - throw new IllegalStateException("Could not get URI: " + ex.getMessage(), ex); - } - } - return this.uri; + protected URI initUri() throws URISyntaxException { + return new URI(this.getRxNettyRequest().getUri()); } @Override - public HttpHeaders getHeaders() { - if (this.headers == null) { - this.headers = new HttpHeaders(); - for (String name : this.getRxNettyRequest().getHeaderNames()) { - this.headers.put(name, this.getRxNettyRequest().getAllHeaderValues(name)); - } + protected HttpHeaders initHeaders() { + HttpHeaders headers = new HttpHeaders(); + for (String name : this.getRxNettyRequest().getHeaderNames()) { + headers.put(name, this.getRxNettyRequest().getAllHeaderValues(name)); } - return this.headers; + return headers; } @Override diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java index c60eaaded8..5f47e78ced 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java @@ -36,19 +36,14 @@ import org.springframework.util.Assert; * @author Rossen Stoyanchev * @author Stephane Maldini */ -public class RxNettyServerHttpResponse implements ServerHttpResponse { +public class RxNettyServerHttpResponse extends AbstractServerHttpResponse { private final HttpServerResponse response; - private final HttpHeaders headers; - - private boolean headersWritten = false; - public RxNettyServerHttpResponse(HttpServerResponse response) { Assert.notNull("'response', response must not be null."); this.response = response; - this.headers = new HttpHeaders(); } @@ -62,17 +57,7 @@ public class RxNettyServerHttpResponse implements ServerHttpResponse { } @Override - public HttpHeaders getHeaders() { - return (this.headersWritten ? HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers); - } - - @Override - public Mono setBody(Publisher publisher) { - return Flux.from(publisher).lift(new WriteWithOperator<>(this::setBodyInternal)).after(); - } - protected Mono setBodyInternal(Publisher publisher) { - writeHeaders(); Observable content = RxJava1Converter.from(publisher).map(this::toBytes); Observable completion = getRxNettyResponse().writeBytes(content); return RxJava1Converter.from(completion).after(); @@ -85,13 +70,10 @@ public class RxNettyServerHttpResponse implements ServerHttpResponse { } @Override - public void writeHeaders() { - if (!this.headersWritten) { - for (String name : this.headers.keySet()) { - for (String value : this.headers.get(name)) + protected void writeHeadersInternal() { + for (String name : getHeaders().keySet()) { + for (String value : getHeaders().get(name)) this.response.addHeader(name, value); - } - this.headersWritten = true; } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java index d5c1461860..a076768c7f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java @@ -39,14 +39,10 @@ import org.springframework.util.StringUtils; * * @author Rossen Stoyanchev */ -public class ServletServerHttpRequest implements ServerHttpRequest { +public class ServletServerHttpRequest extends AbstractServerHttpRequest { private final HttpServletRequest request; - private URI uri; - - private HttpHeaders headers; - private final Flux requestBodyPublisher; @@ -68,62 +64,48 @@ public class ServletServerHttpRequest implements ServerHttpRequest { } @Override - public URI getURI() { - if (this.uri == null) { - try { - this.uri = new URI(getServletRequest().getScheme(), null, - getServletRequest().getServerName(), - getServletRequest().getServerPort(), - getServletRequest().getRequestURI(), - getServletRequest().getQueryString(), null); - } - catch (URISyntaxException ex) { - throw new IllegalStateException("Could not get HttpServletRequest URI: " + ex.getMessage(), ex); - } - } - return this.uri; + protected URI initUri() throws URISyntaxException { + return new URI(getServletRequest().getScheme(), null, + getServletRequest().getServerName(), + getServletRequest().getServerPort(), + getServletRequest().getRequestURI(), + getServletRequest().getQueryString(), null); } @Override - public HttpHeaders getHeaders() { - if (this.headers == null) { - this.headers = new HttpHeaders(); - for (Enumeration names = getServletRequest().getHeaderNames(); names.hasMoreElements(); ) { - String headerName = (String) names.nextElement(); - for (Enumeration headerValues = getServletRequest().getHeaders(headerName); - headerValues.hasMoreElements(); ) { - String headerValue = (String) headerValues.nextElement(); - this.headers.add(headerName, headerValue); - } - } - // HttpServletRequest exposes some headers as properties: we should include those if not already present - MediaType contentType = this.headers.getContentType(); - if (contentType == null) { - String requestContentType = getServletRequest().getContentType(); - if (StringUtils.hasLength(requestContentType)) { - contentType = MediaType.parseMediaType(requestContentType); - this.headers.setContentType(contentType); - } - } - if (contentType != null && contentType.getCharSet() == null) { - String requestEncoding = getServletRequest().getCharacterEncoding(); - if (StringUtils.hasLength(requestEncoding)) { - Charset charSet = Charset.forName(requestEncoding); - Map params = new LinkedCaseInsensitiveMap<>(); - params.putAll(contentType.getParameters()); - params.put("charset", charSet.toString()); - MediaType newContentType = new MediaType(contentType.getType(), contentType.getSubtype(), params); - this.headers.setContentType(newContentType); - } - } - if (this.headers.getContentLength() == -1) { - int requestContentLength = getServletRequest().getContentLength(); - if (requestContentLength != -1) { - this.headers.setContentLength(requestContentLength); - } + protected HttpHeaders initHeaders() { + HttpHeaders headers = new HttpHeaders(); + for (Enumeration names = getServletRequest().getHeaderNames(); names.hasMoreElements(); ) { + String name = (String) names.nextElement(); + for (Enumeration values = getServletRequest().getHeaders(name); values.hasMoreElements(); ) { + headers.add(name, (String) values.nextElement()); } } - return this.headers; + MediaType contentType = headers.getContentType(); + if (contentType == null) { + String requestContentType = getServletRequest().getContentType(); + if (StringUtils.hasLength(requestContentType)) { + contentType = MediaType.parseMediaType(requestContentType); + headers.setContentType(contentType); + } + } + if (contentType != null && contentType.getCharSet() == null) { + String encoding = getServletRequest().getCharacterEncoding(); + if (StringUtils.hasLength(encoding)) { + Charset charset = Charset.forName(encoding); + Map params = new LinkedCaseInsensitiveMap<>(); + params.putAll(contentType.getParameters()); + params.put("charset", charset.toString()); + headers.setContentType(new MediaType(contentType.getType(), contentType.getSubtype(), params)); + } + } + if (headers.getContentLength() == -1) { + int contentLength = getServletRequest().getContentLength(); + if (contentLength != -1) { + headers.setContentLength(contentLength); + } + } + return headers; } @Override diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java index 27b6798955..a292b44f9b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java @@ -24,10 +24,8 @@ import java.util.function.Function; import javax.servlet.http.HttpServletResponse; import org.reactivestreams.Publisher; -import reactor.Flux; import reactor.Mono; -import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.util.Assert; @@ -37,16 +35,12 @@ import org.springframework.util.Assert; * * @author Rossen Stoyanchev */ -public class ServletServerHttpResponse implements ServerHttpResponse { +public class ServletServerHttpResponse extends AbstractServerHttpResponse { private final HttpServletResponse response; private final Function, Mono> responseBodyWriter; - private final HttpHeaders headers; - - private boolean headersWritten = false; - public ServletServerHttpResponse(HttpServletResponse response, Function, Mono> responseBodyWriter) { @@ -55,7 +49,6 @@ public class ServletServerHttpResponse implements ServerHttpResponse { Assert.notNull(responseBodyWriter, "'responseBodyWriter' must not be null"); this.response = response; this.responseBodyWriter = responseBodyWriter; - this.headers = new HttpHeaders(); } @@ -69,38 +62,25 @@ public class ServletServerHttpResponse implements ServerHttpResponse { } @Override - public HttpHeaders getHeaders() { - return (this.headersWritten ? HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers); - } - - @Override - public Mono setBody(final Publisher publisher) { - return Flux.from(publisher).lift(new WriteWithOperator<>(this::setBodyInternal)).after(); - } - protected Mono setBodyInternal(Publisher publisher) { - writeHeaders(); return this.responseBodyWriter.apply(publisher); } @Override - public void writeHeaders() { - if (!this.headersWritten) { - for (Map.Entry> entry : this.headers.entrySet()) { - String headerName = entry.getKey(); - for (String headerValue : entry.getValue()) { - this.response.addHeader(headerName, headerValue); - } + protected void writeHeadersInternal() { + for (Map.Entry> entry : getHeaders().entrySet()) { + String headerName = entry.getKey(); + for (String headerValue : entry.getValue()) { + this.response.addHeader(headerName, headerValue); } - MediaType contentType = this.headers.getContentType(); - if (this.response.getContentType() == null && contentType != null) { - this.response.setContentType(contentType.toString()); - } - Charset charset = (contentType != null ? contentType.getCharSet() : null); - if (this.response.getCharacterEncoding() == null && charset != null) { - this.response.setCharacterEncoding(charset.name()); - } - this.headersWritten = true; + } + MediaType contentType = getHeaders().getContentType(); + if (this.response.getContentType() == null && contentType != null) { + this.response.setContentType(contentType.toString()); + } + Charset charset = (contentType != null ? contentType.getCharSet() : null); + if (this.response.getCharacterEncoding() == null && charset != null) { + this.response.setCharacterEncoding(charset.name()); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java index 98c766e589..6b9b3c32f4 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java @@ -35,14 +35,10 @@ import org.springframework.util.Assert; * @author Marek Hawrylczak * @author Rossen Stoyanchev */ -public class UndertowServerHttpRequest implements ServerHttpRequest { +public class UndertowServerHttpRequest extends AbstractServerHttpRequest { private final HttpServerExchange exchange; - private URI uri; - - private HttpHeaders headers; - private final Flux body; @@ -64,31 +60,19 @@ public class UndertowServerHttpRequest implements ServerHttpRequest { } @Override - public URI getURI() { - if (this.uri == null) { - try { - return new URI(this.getUndertowExchange().getRequestScheme(), null, - this.getUndertowExchange().getHostName(), - this.getUndertowExchange().getHostPort(), - this.getUndertowExchange().getRequestURI(), - this.getUndertowExchange().getQueryString(), null); - } - catch (URISyntaxException ex) { - throw new IllegalStateException("Could not get URI: " + ex.getMessage(), ex); - } - } - return this.uri; + protected URI initUri() throws URISyntaxException { + return new URI(this.exchange.getRequestScheme(), null, + this.exchange.getHostName(), this.exchange.getHostPort(), + this.exchange.getRequestURI(), this.exchange.getQueryString(), null); } @Override - public HttpHeaders getHeaders() { - if (this.headers == null) { - this.headers = new HttpHeaders(); - for (HeaderValues values : this.getUndertowExchange().getRequestHeaders()) { - this.headers.put(values.getHeaderName().toString(), values); - } + protected HttpHeaders initHeaders() { + HttpHeaders headers = new HttpHeaders(); + for (HeaderValues values : this.getUndertowExchange().getRequestHeaders()) { + headers.put(values.getHeaderName().toString(), values); } - return this.headers; + return headers; } @Override diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java index 08032cf1e2..b32255f461 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java @@ -24,10 +24,8 @@ import java.util.function.Function; import io.undertow.server.HttpServerExchange; import io.undertow.util.HttpString; import org.reactivestreams.Publisher; -import reactor.Flux; import reactor.Mono; -import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.util.Assert; @@ -37,16 +35,12 @@ import org.springframework.util.Assert; * @author Marek Hawrylczak * @author Rossen Stoyanchev */ -public class UndertowServerHttpResponse implements ServerHttpResponse { +public class UndertowServerHttpResponse extends AbstractServerHttpResponse { private final HttpServerExchange exchange; private final Function, Mono> responseBodyWriter; - private final HttpHeaders headers; - - private boolean headersWritten = false; - public UndertowServerHttpResponse(HttpServerExchange exchange, Function, Mono> responseBodyWriter) { @@ -55,7 +49,6 @@ public class UndertowServerHttpResponse implements ServerHttpResponse { Assert.notNull(responseBodyWriter, "'responseBodyWriter' must not be null"); this.exchange = exchange; this.responseBodyWriter = responseBodyWriter; - this.headers = new HttpHeaders(); } @@ -70,28 +63,15 @@ public class UndertowServerHttpResponse implements ServerHttpResponse { } @Override - public HttpHeaders getHeaders() { - return (this.headersWritten ? HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers); - } - - @Override - public Mono setBody(Publisher publisher) { - return Flux.from(publisher).lift(new WriteWithOperator<>(this::setBodyInternal)).after(); - } - protected Mono setBodyInternal(Publisher publisher) { - writeHeaders(); return this.responseBodyWriter.apply(publisher); } @Override - public void writeHeaders() { - if (!this.headersWritten) { - for (Map.Entry> entry : this.headers.entrySet()) { - HttpString headerName = HttpString.tryFromString(entry.getKey()); - this.exchange.getResponseHeaders().addAll(headerName, entry.getValue()); - } - this.headersWritten = true; + protected void writeHeadersInternal() { + for (Map.Entry> entry : getHeaders().entrySet()) { + HttpString headerName = HttpString.tryFromString(entry.getKey()); + this.exchange.getResponseHeaders().addAll(headerName, entry.getValue()); } } From 1faeb0ec8721c568b2eb78cbb77ae7bf92260a7c Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Sun, 10 Jan 2016 22:36:45 -0500 Subject: [PATCH 153/498] Add HttpCookie + server support through HttpHeaders --- .../org/springframework/http/HttpCookie.java | 122 ++ .../org/springframework/http/HttpHeaders.java | 1159 +++++++++++++++++ .../reactive/AbstractServerHttpRequest.java | 104 +- .../reactive/AbstractServerHttpResponse.java | 12 +- .../reactive/ReactorServerHttpRequest.java | 12 +- .../reactive/ReactorServerHttpResponse.java | 5 + .../reactive/RxNettyServerHttpRequest.java | 38 +- .../reactive/RxNettyServerHttpResponse.java | 24 +- .../reactive/ServletServerHttpRequest.java | 26 +- .../reactive/ServletServerHttpResponse.java | 21 + .../reactive/UndertowServerHttpRequest.java | 27 +- .../reactive/UndertowServerHttpResponse.java | 18 + .../reactive/CookieIntegrationTests.java | 160 +++ 13 files changed, 1698 insertions(+), 30 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/http/HttpCookie.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/http/HttpHeaders.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/http/server/reactive/CookieIntegrationTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/http/HttpCookie.java b/spring-web-reactive/src/main/java/org/springframework/http/HttpCookie.java new file mode 100644 index 0000000000..18a140d236 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/http/HttpCookie.java @@ -0,0 +1,122 @@ +/* + * Copyright 2002-2015 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; + +/** + * Representation for an HTTP Cookie. + * + * @author Rossen Stoyanchev + * @see RFC 6265 + */ +public class HttpCookie { + + private final String name; + + private final String value; + + private String domain; + + private String path; + + private long maxAge = Long.MIN_VALUE; + + private boolean secure; + + private boolean httpOnly; + + + public HttpCookie(String name, String value) { + this.name = name; + this.value = value; + } + + /** + * Return the cookie name. + */ + public String getName() { + return this.name; + } + + /** + * Return the cookie value. + */ + public String getValue() { + return this.value; + } + + public HttpCookie setPath(String path) { + this.path = path; + return this; + } + + /** + * Return the domain attribute of the cookie. + */ + public String getDomain() { + return this.domain; + } + + public HttpCookie setDomain(String domain) { + this.domain = domain; + return this; + } + + /** + * Return the path attribute of the cookie. + */ + public String getPath() { + return this.path; + } + + public HttpCookie setMaxAge(long maxAge) { + this.maxAge = maxAge; + return this; + } + + /** + * Return the maximum age attribute of the cookie in seconds or + * {@link Long#MIN_VALUE} if not set. + */ + public long getMaxAge() { + return this.maxAge; + } + + public HttpCookie setSecure(boolean secure) { + this.secure = secure; + return this; + } + + /** + * Return true if the "Secure" attribute of the cookie is present. + */ + public boolean isSecure() { + return this.secure; + } + + public HttpCookie setHttpOnly(boolean httpOnly) { + this.httpOnly = httpOnly; + return this; + } + + /** + * Return true if the "HttpOnly" attribute of the cookie is present. + * @see http://www.owasp.org/index.php/HTTPOnly + */ + public boolean isHttpOnly() { + return this.httpOnly; + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/http/HttpHeaders.java b/spring-web-reactive/src/main/java/org/springframework/http/HttpHeaders.java new file mode 100644 index 0000000000..26380c84f8 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/http/HttpHeaders.java @@ -0,0 +1,1159 @@ +/* + * Copyright 2002-2015 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; + +import java.io.Serializable; +import java.net.URI; +import java.nio.charset.Charset; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.EnumSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.TimeZone; + +import org.springframework.util.Assert; +import org.springframework.util.LinkedCaseInsensitiveMap; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; + +// A copy of HttpHeaders with additional support for: +// - HTTP cookies + +// To be merged into HttpHeaders from spring-web + +public class HttpHeaders implements MultiValueMap, Serializable { + + private static final long serialVersionUID = -8578554704772377436L; + + /** + * The HTTP {@code Accept} header field name. + * @see Section 5.3.2 of RFC 7231 + */ + public static final String ACCEPT = "Accept"; + /** + * The HTTP {@code Accept-Charset} header field name. + * @see Section 5.3.3 of RFC 7231 + */ + public static final String ACCEPT_CHARSET = "Accept-Charset"; + /** + * The HTTP {@code Accept-Encoding} header field name. + * @see Section 5.3.4 of RFC 7231 + */ + public static final String ACCEPT_ENCODING = "Accept-Encoding"; + /** + * The HTTP {@code Accept-Language} header field name. + * @see Section 5.3.5 of RFC 7231 + */ + public static final String ACCEPT_LANGUAGE = "Accept-Language"; + /** + * The HTTP {@code Accept-Ranges} header field name. + * @see Section 5.3.5 of RFC 7233 + */ + public static final String ACCEPT_RANGES = "Accept-Ranges"; + /** + * The CORS {@code Access-Control-Allow-Credentials} response header field name. + * @see CORS W3C recommandation + */ + public static final String ACCESS_CONTROL_ALLOW_CREDENTIALS = "Access-Control-Allow-Credentials"; + /** + * The CORS {@code Access-Control-Allow-Headers} response header field name. + * @see CORS W3C recommandation + */ + public static final String ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers"; + /** + * The CORS {@code Access-Control-Allow-Methods} response header field name. + * @see CORS W3C recommandation + */ + public static final String ACCESS_CONTROL_ALLOW_METHODS = "Access-Control-Allow-Methods"; + /** + * The CORS {@code Access-Control-Allow-Origin} response header field name. + * @see CORS W3C recommandation + */ + public static final String ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin"; + /** + * The CORS {@code Access-Control-Expose-Headers} response header field name. + * @see CORS W3C recommandation + */ + public static final String ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-Headers"; + /** + * The CORS {@code Access-Control-Max-Age} response header field name. + * @see CORS W3C recommandation + */ + public static final String ACCESS_CONTROL_MAX_AGE = "Access-Control-Max-Age"; + /** + * The CORS {@code Access-Control-Request-Headers} request header field name. + * @see CORS W3C recommandation + */ + public static final String ACCESS_CONTROL_REQUEST_HEADERS = "Access-Control-Request-Headers"; + /** + * The CORS {@code Access-Control-Request-Method} request header field name. + * @see CORS W3C recommandation + */ + public static final String ACCESS_CONTROL_REQUEST_METHOD = "Access-Control-Request-Method"; + /** + * The HTTP {@code Age} header field name. + * @see Section 5.1 of RFC 7234 + */ + public static final String AGE = "Age"; + /** + * The HTTP {@code Allow} header field name. + * @see Section 7.4.1 of RFC 7231 + */ + public static final String ALLOW = "Allow"; + /** + * The HTTP {@code Authorization} header field name. + * @see Section 4.2 of RFC 7235 + */ + public static final String AUTHORIZATION = "Authorization"; + /** + * The HTTP {@code Cache-Control} header field name. + * @see Section 5.2 of RFC 7234 + */ + public static final String CACHE_CONTROL = "Cache-Control"; + /** + * The HTTP {@code Connection} header field name. + * @see Section 6.1 of RFC 7230 + */ + public static final String CONNECTION = "Connection"; + /** + * The HTTP {@code Content-Encoding} header field name. + * @see Section 3.1.2.2 of RFC 7231 + */ + public static final String CONTENT_ENCODING = "Content-Encoding"; + /** + * The HTTP {@code Content-Disposition} header field name + * @see RFC 6266 + */ + public static final String CONTENT_DISPOSITION = "Content-Disposition"; + /** + * The HTTP {@code Content-Language} header field name. + * @see Section 3.1.3.2 of RFC 7231 + */ + public static final String CONTENT_LANGUAGE = "Content-Language"; + /** + * The HTTP {@code Content-Length} header field name. + * @see Section 3.3.2 of RFC 7230 + */ + public static final String CONTENT_LENGTH = "Content-Length"; + /** + * The HTTP {@code Content-Location} header field name. + * @see Section 3.1.4.2 of RFC 7231 + */ + public static final String CONTENT_LOCATION = "Content-Location"; + /** + * The HTTP {@code Content-Range} header field name. + * @see Section 4.2 of RFC 7233 + */ + public static final String CONTENT_RANGE = "Content-Range"; + /** + * The HTTP {@code Content-Type} header field name. + * @see Section 3.1.1.5 of RFC 7231 + */ + public static final String CONTENT_TYPE = "Content-Type"; + /** + * The HTTP {@code Cookie} header field name. + * @see Section 4.3.4 of RFC 2109 + */ + public static final String COOKIE = "Cookie"; + /** + * The HTTP {@code Date} header field name. + * @see Section 7.1.1.2 of RFC 7231 + */ + public static final String DATE = "Date"; + /** + * The HTTP {@code ETag} header field name. + * @see Section 2.3 of RFC 7232 + */ + public static final String ETAG = "ETag"; + /** + * The HTTP {@code Expect} header field name. + * @see Section 5.1.1 of RFC 7231 + */ + public static final String EXPECT = "Expect"; + /** + * The HTTP {@code Expires} header field name. + * @see Section 5.3 of RFC 7234 + */ + public static final String EXPIRES = "Expires"; + /** + * The HTTP {@code From} header field name. + * @see Section 5.5.1 of RFC 7231 + */ + public static final String FROM = "From"; + /** + * The HTTP {@code Host} header field name. + * @see Section 5.4 of RFC 7230 + */ + public static final String HOST = "Host"; + /** + * The HTTP {@code If-Match} header field name. + * @see Section 3.1 of RFC 7232 + */ + public static final String IF_MATCH = "If-Match"; + /** + * The HTTP {@code If-Modified-Since} header field name. + * @see Section 3.3 of RFC 7232 + */ + public static final String IF_MODIFIED_SINCE = "If-Modified-Since"; + /** + * The HTTP {@code If-None-Match} header field name. + * @see Section 3.2 of RFC 7232 + */ + public static final String IF_NONE_MATCH = "If-None-Match"; + /** + * The HTTP {@code If-Range} header field name. + * @see Section 3.2 of RFC 7233 + */ + public static final String IF_RANGE = "If-Range"; + /** + * The HTTP {@code If-Unmodified-Since} header field name. + * @see Section 3.4 of RFC 7232 + */ + public static final String IF_UNMODIFIED_SINCE = "If-Unmodified-Since"; + /** + * The HTTP {@code Last-Modified} header field name. + * @see Section 2.2 of RFC 7232 + */ + public static final String LAST_MODIFIED = "Last-Modified"; + /** + * The HTTP {@code Link} header field name. + * @see RFC 5988 + */ + public static final String LINK = "Link"; + /** + * The HTTP {@code Location} header field name. + * @see Section 7.1.2 of RFC 7231 + */ + public static final String LOCATION = "Location"; + /** + * The HTTP {@code Max-Forwards} header field name. + * @see Section 5.1.2 of RFC 7231 + */ + public static final String MAX_FORWARDS = "Max-Forwards"; + /** + * The HTTP {@code Origin} header field name. + * @see RFC 6454 + */ + public static final String ORIGIN = "Origin"; + /** + * The HTTP {@code Pragma} header field name. + * @see Section 5.4 of RFC 7234 + */ + public static final String PRAGMA = "Pragma"; + /** + * The HTTP {@code Proxy-Authenticate} header field name. + * @see Section 4.3 of RFC 7235 + */ + public static final String PROXY_AUTHENTICATE = "Proxy-Authenticate"; + /** + * The HTTP {@code Proxy-Authorization} header field name. + * @see Section 4.4 of RFC 7235 + */ + public static final String PROXY_AUTHORIZATION = "Proxy-Authorization"; + /** + * The HTTP {@code Range} header field name. + * @see Section 3.1 of RFC 7233 + */ + public static final String RANGE = "Range"; + /** + * The HTTP {@code Referer} header field name. + * @see Section 5.5.2 of RFC 7231 + */ + public static final String REFERER = "Referer"; + /** + * The HTTP {@code Retry-After} header field name. + * @see Section 7.1.3 of RFC 7231 + */ + public static final String RETRY_AFTER = "Retry-After"; + /** + * The HTTP {@code Server} header field name. + * @see Section 7.4.2 of RFC 7231 + */ + public static final String SERVER = "Server"; + /** + * The HTTP {@code Set-Cookie} header field name. + * @see Section 4.2.2 of RFC 2109 + */ + public static final String SET_COOKIE = "Set-Cookie"; + /** + * The HTTP {@code Set-Cookie2} header field name. + * @see RFC 2965 + */ + public static final String SET_COOKIE2 = "Set-Cookie2"; + /** + * The HTTP {@code TE} header field name. + * @see Section 4.3 of RFC 7230 + */ + public static final String TE = "TE"; + /** + * The HTTP {@code Trailer} header field name. + * @see Section 4.4 of RFC 7230 + */ + public static final String TRAILER = "Trailer"; + /** + * The HTTP {@code Transfer-Encoding} header field name. + * @see Section 3.3.1 of RFC 7230 + */ + public static final String TRANSFER_ENCODING = "Transfer-Encoding"; + /** + * The HTTP {@code Upgrade} header field name. + * @see Section 6.7 of RFC 7230 + */ + public static final String UPGRADE = "Upgrade"; + /** + * The HTTP {@code User-Agent} header field name. + * @see Section 5.5.3 of RFC 7231 + */ + public static final String USER_AGENT = "User-Agent"; + /** + * The HTTP {@code Vary} header field name. + * @see Section 7.1.4 of RFC 7231 + */ + public static final String VARY = "Vary"; + /** + * The HTTP {@code Via} header field name. + * @see Section 5.7.1 of RFC 7230 + */ + public static final String VIA = "Via"; + /** + * The HTTP {@code Warning} header field name. + * @see Section 5.5 of RFC 7234 + */ + public static final String WARNING = "Warning"; + /** + * The HTTP {@code WWW-Authenticate} header field name. + * @see Section 4.1 of RFC 7235 + */ + public static final String WWW_AUTHENTICATE = "WWW-Authenticate"; + + /** + * Date formats as specified in the HTTP RFC + * @see Section 7.1.1.1 of RFC 7231 + */ + private static final String[] DATE_FORMATS = new String[] { + "EEE, dd MMM yyyy HH:mm:ss zzz", + "EEE, dd-MMM-yy HH:mm:ss zzz", + "EEE MMM dd HH:mm:ss yyyy" + }; + + private static TimeZone GMT = TimeZone.getTimeZone("GMT"); + + + private final Map> headers; + + private final Map> cookies; + + + /** + * Constructs a new, empty instance of the {@code HttpHeaders} object. + */ + public HttpHeaders() { + this(new LinkedCaseInsensitiveMap>(8, Locale.ENGLISH), null, false); + } + + /** + * Constructor with a map of HTTP cookies that enables lazy initialization + * of input cookies on first access of the map. + * @param inputCookies a Map with input cookies + */ + public HttpHeaders(Map> inputCookies) { + this(new LinkedCaseInsensitiveMap>(8, Locale.ENGLISH), inputCookies, false); + Assert.notNull(cookies, "'inputCookies' is required."); + } + + /** + * Private constructor that can create read-only {@code HttpHeader} instances. + */ + private HttpHeaders(Map> headers, Map> cookies, + boolean readOnly) { + + Assert.notNull(headers, "'headers' must not be null"); + if (readOnly) { + Map> map = + new LinkedCaseInsensitiveMap>(headers.size(), Locale.ENGLISH); + for (Entry> entry : headers.entrySet()) { + List values = Collections.unmodifiableList(entry.getValue()); + map.put(entry.getKey(), values); + } + this.headers = Collections.unmodifiableMap(map); + this.cookies = (cookies != null ? Collections.unmodifiableMap(cookies) : Collections.emptyMap()); + } + else { + this.headers = headers; + this.cookies = (cookies != null ? cookies : new LinkedHashMap<>()); + } + } + + /** + * Set the list of acceptable {@linkplain MediaType media types}, + * as specified by the {@code Accept} header. + */ + public void setAccept(List acceptableMediaTypes) { + set(ACCEPT, MediaType.toString(acceptableMediaTypes)); + } + + /** + * Return the list of acceptable {@linkplain MediaType media types}, + * as specified by the {@code Accept} header. + *

    Returns an empty list when the acceptable media types are unspecified. + */ + public List getAccept() { + String value = getFirst(ACCEPT); + List result = (value != null ? MediaType.parseMediaTypes(value) : Collections.emptyList()); + + // Some containers parse 'Accept' into multiple values + if (result.size() == 1) { + List acceptHeader = get(ACCEPT); + if (acceptHeader.size() > 1) { + value = StringUtils.collectionToCommaDelimitedString(acceptHeader); + result = MediaType.parseMediaTypes(value); + } + } + + return result; + } + + /** + * Set the (new) value of the {@code Access-Control-Allow-Credentials} response header. + */ + public void setAccessControlAllowCredentials(boolean allowCredentials) { + set(ACCESS_CONTROL_ALLOW_CREDENTIALS, Boolean.toString(allowCredentials)); + } + + /** + * Returns the value of the {@code Access-Control-Allow-Credentials} response header. + */ + public boolean getAccessControlAllowCredentials() { + return new Boolean(getFirst(ACCESS_CONTROL_ALLOW_CREDENTIALS)); + } + + /** + * Set the (new) value of the {@code Access-Control-Allow-Headers} response header. + */ + public void setAccessControlAllowHeaders(List allowedHeaders) { + set(ACCESS_CONTROL_ALLOW_HEADERS, toCommaDelimitedString(allowedHeaders)); + } + + /** + * Returns the value of the {@code Access-Control-Allow-Headers} response header. + */ + public List getAccessControlAllowHeaders() { + return getFirstValueAsList(ACCESS_CONTROL_ALLOW_HEADERS); + } + + /** + * Set the (new) value of the {@code Access-Control-Allow-Methods} response header. + */ + public void setAccessControlAllowMethods(List allowedMethods) { + set(ACCESS_CONTROL_ALLOW_METHODS, StringUtils.collectionToCommaDelimitedString(allowedMethods)); + } + + /** + * Returns the value of the {@code Access-Control-Allow-Methods} response header. + */ + public List getAccessControlAllowMethods() { + List result = new ArrayList(); + String value = getFirst(ACCESS_CONTROL_ALLOW_METHODS); + if (value != null) { + String[] tokens = value.split(",\\s*"); + for (String token : tokens) { + result.add(HttpMethod.valueOf(token)); + } + } + return result; + } + + /** + * Set the (new) value of the {@code Access-Control-Allow-Origin} response header. + */ + public void setAccessControlAllowOrigin(String allowedOrigin) { + set(ACCESS_CONTROL_ALLOW_ORIGIN, allowedOrigin); + } + + /** + * Returns the value of the {@code Access-Control-Allow-Origin} response header. + */ + public String getAccessControlAllowOrigin() { + return getFirst(ACCESS_CONTROL_ALLOW_ORIGIN); + } + + /** + * Set the (new) value of the {@code Access-Control-Expose-Headers} response header. + */ + public void setAccessControlExposeHeaders(List exposedHeaders) { + set(ACCESS_CONTROL_EXPOSE_HEADERS, toCommaDelimitedString(exposedHeaders)); + } + + /** + * Returns the value of the {@code Access-Control-Expose-Headers} response header. + */ + public List getAccessControlExposeHeaders() { + return getFirstValueAsList(ACCESS_CONTROL_EXPOSE_HEADERS); + } + + /** + * Set the (new) value of the {@code Access-Control-Max-Age} response header. + */ + public void setAccessControlMaxAge(long maxAge) { + set(ACCESS_CONTROL_MAX_AGE, Long.toString(maxAge)); + } + + /** + * Returns the value of the {@code Access-Control-Max-Age} response header. + *

    Returns -1 when the max age is unknown. + */ + public long getAccessControlMaxAge() { + String value = getFirst(ACCESS_CONTROL_MAX_AGE); + return (value != null ? Long.parseLong(value) : -1); + } + + /** + * Set the (new) value of the {@code Access-Control-Request-Headers} request header. + */ + public void setAccessControlRequestHeaders(List requestHeaders) { + set(ACCESS_CONTROL_REQUEST_HEADERS, toCommaDelimitedString(requestHeaders)); + } + + /** + * Returns the value of the {@code Access-Control-Request-Headers} request header. + */ + public List getAccessControlRequestHeaders() { + return getFirstValueAsList(ACCESS_CONTROL_REQUEST_HEADERS); + } + + /** + * Set the (new) value of the {@code Access-Control-Request-Method} request header. + */ + public void setAccessControlRequestMethod(HttpMethod requestedMethod) { + set(ACCESS_CONTROL_REQUEST_METHOD, requestedMethod.name()); + } + + /** + * Returns the value of the {@code Access-Control-Request-Method} request header. + */ + public HttpMethod getAccessControlRequestMethod() { + String value = getFirst(ACCESS_CONTROL_REQUEST_METHOD); + return (value != null ? HttpMethod.valueOf(value) : null); + } + + /** + * Set the list of acceptable {@linkplain Charset charsets}, + * as specified by the {@code Accept-Charset} header. + */ + public void setAcceptCharset(List acceptableCharsets) { + StringBuilder builder = new StringBuilder(); + for (Iterator iterator = acceptableCharsets.iterator(); iterator.hasNext();) { + Charset charset = iterator.next(); + builder.append(charset.name().toLowerCase(Locale.ENGLISH)); + if (iterator.hasNext()) { + builder.append(", "); + } + } + set(ACCEPT_CHARSET, builder.toString()); + } + + /** + * Return the list of acceptable {@linkplain Charset charsets}, + * as specified by the {@code Accept-Charset} header. + */ + public List getAcceptCharset() { + List result = new ArrayList(); + String value = getFirst(ACCEPT_CHARSET); + if (value != null) { + String[] tokens = value.split(",\\s*"); + for (String token : tokens) { + int paramIdx = token.indexOf(';'); + String charsetName; + if (paramIdx == -1) { + charsetName = token; + } + else { + charsetName = token.substring(0, paramIdx); + } + if (!charsetName.equals("*")) { + result.add(Charset.forName(charsetName)); + } + } + } + return result; + } + + /** + * Set the set of allowed {@link HttpMethod HTTP methods}, + * as specified by the {@code Allow} header. + */ + public void setAllow(Set allowedMethods) { + set(ALLOW, StringUtils.collectionToCommaDelimitedString(allowedMethods)); + } + + /** + * Return the set of allowed {@link HttpMethod HTTP methods}, + * as specified by the {@code Allow} header. + *

    Returns an empty set when the allowed methods are unspecified. + */ + public Set getAllow() { + String value = getFirst(ALLOW); + if (!StringUtils.isEmpty(value)) { + List allowedMethod = new ArrayList(5); + String[] tokens = value.split(",\\s*"); + for (String token : tokens) { + allowedMethod.add(HttpMethod.valueOf(token)); + } + return EnumSet.copyOf(allowedMethod); + } + else { + return EnumSet.noneOf(HttpMethod.class); + } + } + + /** + * Set the (new) value of the {@code Cache-Control} header. + */ + public void setCacheControl(String cacheControl) { + set(CACHE_CONTROL, cacheControl); + } + + /** + * Returns the value of the {@code Cache-Control} header. + */ + public String getCacheControl() { + return getFirst(CACHE_CONTROL); + } + + /** + * Set the (new) value of the {@code Connection} header. + */ + public void setConnection(String connection) { + set(CONNECTION, connection); + } + + /** + * Set the (new) value of the {@code Connection} header. + */ + public void setConnection(List connection) { + set(CONNECTION, toCommaDelimitedString(connection)); + } + + /** + * Returns the value of the {@code Connection} header. + */ + public List getConnection() { + return getFirstValueAsList(CONNECTION); + } + + /** + * Set the (new) value of the {@code Content-Disposition} header + * for {@code form-data}. + * @param name the control name + * @param filename the filename (may be {@code null}) + */ + public void setContentDispositionFormData(String name, String filename) { + Assert.notNull(name, "'name' must not be null"); + StringBuilder builder = new StringBuilder("form-data; name=\""); + builder.append(name).append('\"'); + if (filename != null) { + builder.append("; filename=\""); + builder.append(filename).append('\"'); + } + set(CONTENT_DISPOSITION, builder.toString()); + } + + /** + * Set the length of the body in bytes, as specified by the + * {@code Content-Length} header. + */ + public void setContentLength(long contentLength) { + set(CONTENT_LENGTH, Long.toString(contentLength)); + } + + /** + * Return the length of the body in bytes, as specified by the + * {@code Content-Length} header. + *

    Returns -1 when the content-length is unknown. + */ + public long getContentLength() { + String value = getFirst(CONTENT_LENGTH); + return (value != null ? Long.parseLong(value) : -1); + } + + /** + * Set the {@linkplain MediaType media type} of the body, + * as specified by the {@code Content-Type} header. + */ + public void setContentType(MediaType mediaType) { + Assert.isTrue(!mediaType.isWildcardType(), "'Content-Type' cannot contain wildcard type '*'"); + Assert.isTrue(!mediaType.isWildcardSubtype(), "'Content-Type' cannot contain wildcard subtype '*'"); + set(CONTENT_TYPE, mediaType.toString()); + } + + /** + * Return the {@linkplain MediaType media type} of the body, as specified + * by the {@code Content-Type} header. + *

    Returns {@code null} when the content-type is unknown. + */ + public MediaType getContentType() { + String value = getFirst(CONTENT_TYPE); + return (StringUtils.hasLength(value) ? MediaType.parseMediaType(value) : null); + } + + /** + * Add an HTTP cookie. Supported only when writing output cookies. + */ + public void addCookie(HttpCookie cookie) { + String name = cookie.getName(); + Set set = this.cookies.get(name); + if (set == null) { + set = new LinkedHashSet<>(); + this.cookies.put(name, set); + } + set.add(cookie); + } + + /** + * Return a map with {@link HttpCookie}s. When reading input cookies this map + * cannot be modified. When writing output cookies, this map is mutable. + */ + public Map> getCookies() { + return this.cookies; + } + + /** + * Set the date and time at which the message was created, as specified + * by the {@code Date} header. + *

    The date should be specified as the number of milliseconds since + * January 1, 1970 GMT. + */ + public void setDate(long date) { + setDate(DATE, date); + } + + /** + * Return the date and time at which the message was created, as specified + * by the {@code Date} header. + *

    The date is returned as the number of milliseconds since + * January 1, 1970 GMT. Returns -1 when the date is unknown. + * @throws IllegalArgumentException if the value can't be converted to a date + */ + public long getDate() { + return getFirstDate(DATE); + } + + /** + * Set the (new) entity tag of the body, as specified by the {@code ETag} header. + */ + public void setETag(String eTag) { + if (eTag != null) { + Assert.isTrue(eTag.startsWith("\"") || eTag.startsWith("W/"), + "Invalid eTag, does not start with W/ or \""); + Assert.isTrue(eTag.endsWith("\""), "Invalid eTag, does not end with \""); + } + set(ETAG, eTag); + } + + /** + * Return the entity tag of the body, as specified by the {@code ETag} header. + */ + public String getETag() { + return getFirst(ETAG); + } + + /** + * Set the date and time at which the message is no longer valid, + * as specified by the {@code Expires} header. + *

    The date should be specified as the number of milliseconds since + * January 1, 1970 GMT. + */ + public void setExpires(long expires) { + setDate(EXPIRES, expires); + } + + /** + * Return the date and time at which the message is no longer valid, + * as specified by the {@code Expires} header. + *

    The date is returned as the number of milliseconds since + * January 1, 1970 GMT. Returns -1 when the date is unknown. + */ + public long getExpires() { + try { + return getFirstDate(EXPIRES); + } + catch (IllegalArgumentException ex) { + return -1; + } + } + + /** + * Set the (new) value of the {@code If-Modified-Since} header. + *

    The date should be specified as the number of milliseconds since + * January 1, 1970 GMT. + */ + public void setIfModifiedSince(long ifModifiedSince) { + setDate(IF_MODIFIED_SINCE, ifModifiedSince); + } + + /** + * Return the value of the {@code If-Modified-Since} header. + *

    The date is returned as the number of milliseconds since + * January 1, 1970 GMT. Returns -1 when the date is unknown. + */ + public long getIfModifiedSince() { + return getFirstDate(IF_MODIFIED_SINCE); + } + + /** + * Set the (new) value of the {@code If-None-Match} header. + */ + public void setIfNoneMatch(String ifNoneMatch) { + set(IF_NONE_MATCH, ifNoneMatch); + } + + /** + * Set the (new) values of the {@code If-None-Match} header. + */ + public void setIfNoneMatch(List ifNoneMatchList) { + set(IF_NONE_MATCH, toCommaDelimitedString(ifNoneMatchList)); + } + + protected String toCommaDelimitedString(List list) { + StringBuilder builder = new StringBuilder(); + for (Iterator iterator = list.iterator(); iterator.hasNext();) { + String ifNoneMatch = iterator.next(); + builder.append(ifNoneMatch); + if (iterator.hasNext()) { + builder.append(", "); + } + } + return builder.toString(); + } + + /** + * Return the value of the {@code If-None-Match} header. + */ + public List getIfNoneMatch() { + return getFirstValueAsList(IF_NONE_MATCH); + } + + protected List getFirstValueAsList(String header) { + List result = new ArrayList(); + String value = getFirst(header); + if (value != null) { + String[] tokens = value.split(",\\s*"); + for (String token : tokens) { + result.add(token); + } + } + return result; + } + + /** + * Set the time the resource was last changed, as specified by the + * {@code Last-Modified} header. + *

    The date should be specified as the number of milliseconds since + * January 1, 1970 GMT. + */ + public void setLastModified(long lastModified) { + setDate(LAST_MODIFIED, lastModified); + } + + /** + * Return the time the resource was last changed, as specified by the + * {@code Last-Modified} header. + *

    The date is returned as the number of milliseconds since + * January 1, 1970 GMT. Returns -1 when the date is unknown. + */ + public long getLastModified() { + return getFirstDate(LAST_MODIFIED); + } + + /** + * Set the (new) location of a resource, + * as specified by the {@code Location} header. + */ + public void setLocation(URI location) { + set(LOCATION, location.toASCIIString()); + } + + /** + * Return the (new) location of a resource + * as specified by the {@code Location} header. + *

    Returns {@code null} when the location is unknown. + */ + public URI getLocation() { + String value = getFirst(LOCATION); + return (value != null ? URI.create(value) : null); + } + + /** + * Set the (new) value of the {@code Origin} header. + */ + public void setOrigin(String origin) { + set(ORIGIN, origin); + } + + /** + * Return the value of the {@code Origin} header. + */ + public String getOrigin() { + return getFirst(ORIGIN); + } + + /** + * Set the (new) value of the {@code Pragma} header. + */ + public void setPragma(String pragma) { + set(PRAGMA, pragma); + } + + /** + * Return the value of the {@code Pragma} header. + */ + public String getPragma() { + return getFirst(PRAGMA); + } + + /** + * Sets the (new) value of the {@code Range} header. + */ + public void setRange(List ranges) { + String value = HttpRange.toString(ranges); + set(RANGE, value); + } + + /** + * Returns the value of the {@code Range} header. + *

    Returns an empty list when the range is unknown. + */ + public List getRange() { + String value = getFirst(RANGE); + return HttpRange.parseRanges(value); + } + + /** + * Set the (new) value of the {@code Upgrade} header. + */ + public void setUpgrade(String upgrade) { + set(UPGRADE, upgrade); + } + + /** + * Returns the value of the {@code Upgrade} header. + */ + public String getUpgrade() { + return getFirst(UPGRADE); + } + + /** + * Parse the first header value for the given header name as a date, + * return -1 if there is no value, or raise {@link IllegalArgumentException} + * if the value cannot be parsed as a date. + */ + public long getFirstDate(String headerName) { + String headerValue = getFirst(headerName); + if (headerValue == null) { + return -1; + } + for (String dateFormat : DATE_FORMATS) { + SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateFormat, Locale.US); + simpleDateFormat.setTimeZone(GMT); + try { + return simpleDateFormat.parse(headerValue).getTime(); + } + catch (ParseException ex) { + // ignore + } + } + throw new IllegalArgumentException("Cannot parse date value \"" + headerValue + + "\" for \"" + headerName + "\" header"); + } + + /** + * Set the given date under the given header name after formatting it as a string + * using the pattern {@code "EEE, dd MMM yyyy HH:mm:ss zzz"}. The equivalent of + * {@link #set(String, String)} but for date headers. + */ + public void setDate(String headerName, long date) { + SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMATS[0], Locale.US); + dateFormat.setTimeZone(GMT); + set(headerName, dateFormat.format(new Date(date))); + } + + /** + * Return the first header value for the given header name, if any. + * @param headerName the header name + * @return the first header value, or {@code null} if none + */ + @Override + public String getFirst(String headerName) { + List headerValues = this.headers.get(headerName); + return (headerValues != null ? headerValues.get(0) : null); + } + + /** + * Add the given, single header value under the given name. + * @param headerName the header name + * @param headerValue the header value + * @throws UnsupportedOperationException if adding headers is not supported + * @see #put(String, List) + * @see #set(String, String) + */ + @Override + public void add(String headerName, String headerValue) { + List headerValues = this.headers.get(headerName); + if (headerValues == null) { + headerValues = new LinkedList(); + this.headers.put(headerName, headerValues); + } + headerValues.add(headerValue); + } + + /** + * Set the given, single header value under the given name. + * @param headerName the header name + * @param headerValue the header value + * @throws UnsupportedOperationException if adding headers is not supported + * @see #put(String, List) + * @see #add(String, String) + */ + @Override + public void set(String headerName, String headerValue) { + List headerValues = new LinkedList(); + headerValues.add(headerValue); + this.headers.put(headerName, headerValues); + } + + @Override + public void setAll(Map values) { + for (Entry entry : values.entrySet()) { + set(entry.getKey(), entry.getValue()); + } + } + + @Override + public Map toSingleValueMap() { + LinkedHashMap singleValueMap = new LinkedHashMap(this.headers.size()); + for (Entry> entry : this.headers.entrySet()) { + singleValueMap.put(entry.getKey(), entry.getValue().get(0)); + } + return singleValueMap; + } + + + // Map implementation + + @Override + public int size() { + return this.headers.size(); + } + + @Override + public boolean isEmpty() { + return this.headers.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return this.headers.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return this.headers.containsValue(value); + } + + @Override + public List get(Object key) { + return this.headers.get(key); + } + + @Override + public List put(String key, List value) { + return this.headers.put(key, value); + } + + @Override + public List remove(Object key) { + return this.headers.remove(key); + } + + @Override + public void putAll(Map> map) { + this.headers.putAll(map); + } + + @Override + public void clear() { + this.headers.clear(); + } + + @Override + public Set keySet() { + return this.headers.keySet(); + } + + @Override + public Collection> values() { + return this.headers.values(); + } + + @Override + public Set>> entrySet() { + return this.headers.entrySet(); + } + + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof HttpHeaders)) { + return false; + } + HttpHeaders otherHeaders = (HttpHeaders) other; + return this.headers.equals(otherHeaders.headers); + } + + @Override + public int hashCode() { + return this.headers.hashCode(); + } + + @Override + public String toString() { + return this.headers.toString(); + } + + + /** + * Return a {@code HttpHeaders} object that can only be read, not written to. + */ + public static HttpHeaders readOnlyHttpHeaders(HttpHeaders headers) { + return new HttpHeaders(headers, headers.getCookies(), true); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java index e7a35a8e6b..24eeeb61c1 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java @@ -17,8 +17,13 @@ package org.springframework.http.server.reactive; import java.net.URI; import java.net.URISyntaxException; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpCookie; /** * Common base class for {@link ServerHttpRequest} implementations. @@ -46,8 +51,8 @@ public abstract class AbstractServerHttpRequest implements ServerHttpRequest { } /** - * Initialize a URI that represents the request. - * Invoked lazily on the first call to {@link #getURI()} and then cached. + * Initialize a URI that represents the request. Invoked lazily on the first + * call to {@link #getURI()} and then cached. * @throws URISyntaxException */ protected abstract URI initUri() throws URISyntaxException; @@ -55,15 +60,102 @@ public abstract class AbstractServerHttpRequest implements ServerHttpRequest { @Override public HttpHeaders getHeaders() { if (this.headers == null) { - this.headers = HttpHeaders.readOnlyHttpHeaders(initHeaders()); + this.headers = new HttpHeaders(new HttpCookieInputMap()); + initHeaders(this.headers); } return this.headers; } /** - * Initialize the headers from the underlying request. - * Invoked lazily on the first call to {@link #getHeaders()} and then cached. + * Initialize the headers from the underlying request. Invoked lazily on the + * first call to {@link #getHeaders()} and then cached. + * @param headers the map to add headers to */ - protected abstract HttpHeaders initHeaders(); + protected abstract void initHeaders(HttpHeaders headers); + + /** + * Initialize the cookies from the underlying request. Invoked lazily on the + * first access to cookies via {@link #getHeaders()} and then cached. + * @param cookies the map to add cookies to + */ + protected abstract void initCookies(Map> cookies); + + + /** + * Read-only map of input cookies with lazy initialization. + */ + private class HttpCookieInputMap implements Map> { + + private Map> cookies; + + + private Map> getCookies() { + if (this.cookies == null) { + this.cookies = new LinkedHashMap<>(); + initCookies(this.cookies); + } + return this.cookies; + } + + @Override + public int size() { + return getCookies().size(); + } + + @Override + public boolean isEmpty() { + return getCookies().isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return getCookies().containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return getCookies().containsValue(value); + } + + @Override + public Set get(Object key) { + return getCookies().get(key); + } + + @Override + public Set keySet() { + return getCookies().keySet(); + } + + @Override + public Collection> values() { + return getCookies().values(); + } + + @Override + public Set>> entrySet() { + return getCookies().entrySet(); + } + + @Override + public Set put(String key, Set value) { + throw new UnsupportedOperationException("Read-only map of cookies."); + } + + @Override + public Set remove(Object key) { + throw new UnsupportedOperationException("Read-only map of cookies."); + } + + @Override + public void putAll(Map> m) { + throw new UnsupportedOperationException("Read-only map of cookies."); + } + + @Override + public void clear() { + throw new UnsupportedOperationException("Read-only map of cookies."); + } + } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java index 2c87f89c46..f75f4be2ad 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java @@ -42,7 +42,7 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse { @Override public HttpHeaders getHeaders() { - return (this.headersWritten ? HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers); + return (this.headersWritten ? org.springframework.http.HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers); } @Override @@ -64,6 +64,7 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse { if (!this.headersWritten) { try { writeHeadersInternal(); + writeCookies(); } finally { this.headersWritten = true; @@ -73,9 +74,14 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse { /** * Implement this method to apply header changes from {@link #getHeaders()} - * to the underlying response. This method is protected from being called - * more than once. + * to the underlying response. This method is called once only. */ protected abstract void writeHeadersInternal(); + /** + * Implement this method to add cookies from {@link #getHeaders()} to the + * underlying response. This method is called once only. + */ + protected abstract void writeCookies(); + } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java index e1f62514c7..4ae9fc9f0a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java @@ -18,11 +18,14 @@ package org.springframework.http.server.reactive; import java.net.URI; import java.net.URISyntaxException; import java.nio.ByteBuffer; +import java.util.Map; +import java.util.Set; import reactor.Flux; import reactor.io.buffer.Buffer; import reactor.io.net.http.HttpChannel; +import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.util.Assert; @@ -58,12 +61,15 @@ public class ReactorServerHttpRequest extends AbstractServerHttpRequest { } @Override - protected HttpHeaders initHeaders() { - HttpHeaders headers = new HttpHeaders(); + protected void initHeaders(HttpHeaders headers) { for (String name : this.channel.headers().names()) { headers.put(name, this.channel.headers().getAll(name)); } - return headers; + } + + @Override + protected void initCookies(Map> cookies) { + // https://github.com/reactor/reactor/issues/614 } @Override diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java index 64a44b94f6..b7cbed3b8c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java @@ -67,4 +67,9 @@ public class ReactorServerHttpResponse extends AbstractServerHttpResponse { } } + @Override + protected void writeCookies() { + // https://github.com/reactor/reactor/issues/614 + } + } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java index afdabae668..3e6c541f66 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java @@ -19,13 +19,18 @@ package org.springframework.http.server.reactive; import java.net.URI; import java.net.URISyntaxException; import java.nio.ByteBuffer; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; import io.netty.buffer.ByteBuf; +import io.netty.handler.codec.http.cookie.Cookie; import io.reactivex.netty.protocol.http.server.HttpServerRequest; import reactor.Flux; import reactor.core.publisher.convert.RxJava1Converter; import rx.Observable; +import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.util.Assert; @@ -53,26 +58,43 @@ public class RxNettyServerHttpRequest extends AbstractServerHttpRequest { @Override public HttpMethod getMethod() { - return HttpMethod.valueOf(this.getRxNettyRequest().getHttpMethod().name()); + return HttpMethod.valueOf(this.request.getHttpMethod().name()); } @Override protected URI initUri() throws URISyntaxException { - return new URI(this.getRxNettyRequest().getUri()); + return new URI(this.request.getUri()); } @Override - protected HttpHeaders initHeaders() { - HttpHeaders headers = new HttpHeaders(); - for (String name : this.getRxNettyRequest().getHeaderNames()) { - headers.put(name, this.getRxNettyRequest().getAllHeaderValues(name)); + protected void initHeaders(HttpHeaders headers) { + for (String name : this.request.getHeaderNames()) { + headers.put(name, this.request.getAllHeaderValues(name)); + } + } + + @Override + protected void initCookies(Map> map) { + for (String name : this.request.getCookies().keySet()) { + Set set = map.get(name); + if (set == null) { + set = new LinkedHashSet<>(); + map.put(name, set); + } + for (Cookie cookie : this.request.getCookies().get(name)) { + set.add(new HttpCookie(name, cookie.value()) + .setDomain(cookie.domain()) + .setPath(cookie.path()) + .setMaxAge(cookie.maxAge()) + .setSecure(cookie.isSecure()) + .setHttpOnly(cookie.isHttpOnly())); + } } - return headers; } @Override public Flux getBody() { - Observable content = this.getRxNettyRequest().getContent().map(ByteBuf::nioBuffer); + Observable content = this.request.getContent().map(ByteBuf::nioBuffer); content = content.concatWith(Observable.empty()); // See GH issue #58 return RxJava1Converter.from(content); } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java index 5f47e78ced..467c1d6dfd 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java @@ -19,14 +19,15 @@ package org.springframework.http.server.reactive; import java.nio.ByteBuffer; import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.cookie.Cookie; +import io.netty.handler.codec.http.cookie.DefaultCookie; import io.reactivex.netty.protocol.http.server.HttpServerResponse; import org.reactivestreams.Publisher; -import reactor.Flux; import reactor.Mono; import reactor.core.publisher.convert.RxJava1Converter; import rx.Observable; -import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpCookie; import org.springframework.http.HttpStatus; import org.springframework.util.Assert; @@ -53,13 +54,13 @@ public class RxNettyServerHttpResponse extends AbstractServerHttpResponse { @Override public void setStatusCode(HttpStatus status) { - getRxNettyResponse().setStatus(HttpResponseStatus.valueOf(status.value())); + this.response.setStatus(HttpResponseStatus.valueOf(status.value())); } @Override protected Mono setBodyInternal(Publisher publisher) { Observable content = RxJava1Converter.from(publisher).map(this::toBytes); - Observable completion = getRxNettyResponse().writeBytes(content); + Observable completion = this.response.writeBytes(content); return RxJava1Converter.from(completion).after(); } @@ -77,4 +78,19 @@ public class RxNettyServerHttpResponse extends AbstractServerHttpResponse { } } + @Override + protected void writeCookies() { + for (String name : getHeaders().getCookies().keySet()) { + for (HttpCookie httpCookie : getHeaders().getCookies().get(name)) { + Cookie cookie = new DefaultCookie(name, httpCookie.getValue()); + cookie.setDomain(httpCookie.getDomain()); + cookie.setPath(httpCookie.getPath()); + cookie.setMaxAge(httpCookie.getMaxAge()); + cookie.setSecure(httpCookie.isSecure()); + cookie.setHttpOnly(httpCookie.isHttpOnly()); + this.response.addCookie(cookie); + } + } + } + } \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java index a076768c7f..867c9356a2 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java @@ -21,12 +21,16 @@ import java.net.URISyntaxException; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.util.Enumeration; +import java.util.LinkedHashSet; import java.util.Map; +import java.util.Set; +import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import org.reactivestreams.Publisher; import reactor.Flux; +import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; @@ -73,8 +77,7 @@ public class ServletServerHttpRequest extends AbstractServerHttpRequest { } @Override - protected HttpHeaders initHeaders() { - HttpHeaders headers = new HttpHeaders(); + protected void initHeaders(HttpHeaders headers) { for (Enumeration names = getServletRequest().getHeaderNames(); names.hasMoreElements(); ) { String name = (String) names.nextElement(); for (Enumeration values = getServletRequest().getHeaders(name); values.hasMoreElements(); ) { @@ -105,7 +108,24 @@ public class ServletServerHttpRequest extends AbstractServerHttpRequest { headers.setContentLength(contentLength); } } - return headers; + } + + @Override + protected void initCookies(Map> map) { + for (Cookie cookie : this.request.getCookies()) { + String name = cookie.getName(); + Set set = map.get(name); + if (set == null) { + set = new LinkedHashSet<>(); + map.put(name, set); + } + set.add(new HttpCookie(name, cookie.getValue()) + .setDomain(cookie.getDomain()) + .setPath(cookie.getPath()) + .setMaxAge(cookie.getMaxAge() == -1 ? Long.MIN_VALUE : cookie.getMaxAge()) + .setHttpOnly(cookie.isHttpOnly()) + .setSecure(cookie.getSecure())); + } } @Override diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java index a292b44f9b..a4e7e91446 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java @@ -21,11 +21,13 @@ import java.nio.charset.Charset; import java.util.List; import java.util.Map; import java.util.function.Function; +import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletResponse; import org.reactivestreams.Publisher; import reactor.Mono; +import org.springframework.http.HttpCookie; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.util.Assert; @@ -84,4 +86,23 @@ public class ServletServerHttpResponse extends AbstractServerHttpResponse { } } + @Override + protected void writeCookies() { + for (String name : getHeaders().getCookies().keySet()) { + for (HttpCookie httpCookie : getHeaders().getCookies().get(name)) { + Cookie cookie = new Cookie(name, httpCookie.getValue()); + if (httpCookie.getDomain() != null) { + cookie.setDomain(httpCookie.getDomain()); + } + if (httpCookie.getPath() != null) { + cookie.setPath(httpCookie.getPath()); + } + cookie.setMaxAge(httpCookie.getMaxAge() == Long.MIN_VALUE ? -1 : (int) httpCookie.getMaxAge()); + cookie.setSecure(httpCookie.isSecure()); + cookie.setHttpOnly(httpCookie.isHttpOnly()); + this.response.addCookie(cookie); + } + } + } + } \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java index 6b9b3c32f4..53013fbe54 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java @@ -19,12 +19,17 @@ package org.springframework.http.server.reactive; import java.net.URI; import java.net.URISyntaxException; import java.nio.ByteBuffer; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.Cookie; import io.undertow.util.HeaderValues; import org.reactivestreams.Publisher; import reactor.Flux; +import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.util.Assert; @@ -67,12 +72,28 @@ public class UndertowServerHttpRequest extends AbstractServerHttpRequest { } @Override - protected HttpHeaders initHeaders() { - HttpHeaders headers = new HttpHeaders(); + protected void initHeaders(HttpHeaders headers) { for (HeaderValues values : this.getUndertowExchange().getRequestHeaders()) { headers.put(values.getHeaderName().toString(), values); } - return headers; + } + + @Override + protected void initCookies(Map> map) { + for (String name : this.exchange.getRequestCookies().keySet()) { + Set set = map.get(name); + if (set == null) { + set = new LinkedHashSet<>(); + map.put(name, set); + } + Cookie cookie = this.exchange.getRequestCookies().get(name); + set.add(new HttpCookie(name, cookie.getValue()) + .setDomain(cookie.getDomain()) + .setPath(cookie.getPath()) + .setMaxAge(cookie.getMaxAge() != null ? cookie.getMaxAge() : Long.MIN_VALUE) + .setSecure(cookie.isSecure()) + .setHttpOnly(cookie.isHttpOnly())); + } } @Override diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java index b32255f461..27c38342f8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java @@ -22,10 +22,13 @@ import java.util.Map; import java.util.function.Function; import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.Cookie; +import io.undertow.server.handlers.CookieImpl; import io.undertow.util.HttpString; import org.reactivestreams.Publisher; import reactor.Mono; +import org.springframework.http.HttpCookie; import org.springframework.http.HttpStatus; import org.springframework.util.Assert; @@ -75,4 +78,19 @@ public class UndertowServerHttpResponse extends AbstractServerHttpResponse { } } + @Override + protected void writeCookies() { + for (String name : getHeaders().getCookies().keySet()) { + for (HttpCookie httpCookie : getHeaders().getCookies().get(name)) { + Cookie cookie = new CookieImpl(name, httpCookie.getValue()); + cookie.setDomain(httpCookie.getDomain()); + cookie.setPath(httpCookie.getPath()); + cookie.setMaxAge(httpCookie.getMaxAge() == Long.MIN_VALUE ? null : (int) httpCookie.getMaxAge()); + cookie.setSecure(httpCookie.isSecure()); + cookie.setHttpOnly(httpCookie.isHttpOnly()); + this.exchange.getResponseCookies().putIfAbsent(name, cookie); + } + } + } + } diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/CookieIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/CookieIntegrationTests.java new file mode 100644 index 0000000000..6a1dccb4ae --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/CookieIntegrationTests.java @@ -0,0 +1,160 @@ +/* + * Copyright 2002-2015 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.server.reactive; + +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import reactor.Mono; + +import org.springframework.http.HttpCookie; +import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.http.server.reactive.boot.HttpServer; +import org.springframework.http.server.reactive.boot.JettyHttpServer; +import org.springframework.http.server.reactive.boot.RxNettyHttpServer; +import org.springframework.http.server.reactive.boot.TomcatHttpServer; +import org.springframework.http.server.reactive.boot.UndertowHttpServer; +import org.springframework.util.SocketUtils; +import org.springframework.web.client.RestTemplate; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.equalToIgnoringCase; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; + +/** + * Temporarily does not extend AbstractHttpHandlerIntegrationTests in order to + * exclude Reactor Net due to https://github.com/reactor/reactor/issues/614. + * + * @author Rossen Stoyanchev + */ +@RunWith(Parameterized.class) +public class CookieIntegrationTests { + + protected int port; + + @Parameterized.Parameter(0) + public HttpServer server; + + private CookieHandler cookieHandler; + + + @Parameterized.Parameters(name = "server [{0}]") + public static Object[][] arguments() { + return new Object[][] { + {new JettyHttpServer()}, + {new RxNettyHttpServer()}, +// {new ReactorHttpServer()}, + {new TomcatHttpServer()}, + {new UndertowHttpServer()} + }; + } + + + @Before + public void setup() throws Exception { + this.port = SocketUtils.findAvailableTcpPort(); + this.server.setPort(this.port); + this.server.setHandler(createHttpHandler()); + this.server.afterPropertiesSet(); + this.server.start(); + } + + protected HttpHandler createHttpHandler() { + this.cookieHandler = new CookieHandler(); + return this.cookieHandler; + } + + @After + public void tearDown() throws Exception { + this.server.stop(); + } + + + @SuppressWarnings("unchecked") + @Test + public void basicTest() throws Exception { + URI url = new URI("http://localhost:" + port); + String header = "SID=31d4d96e407aad42; lang=en-US"; + ResponseEntity response = new RestTemplate().exchange( + RequestEntity.get(url).header("Cookie", header).build(), Void.class); + + Map> requestCookies = this.cookieHandler.requestCookies; + assertEquals(2, requestCookies.size()); + + Set set = requestCookies.get("SID"); + assertEquals(1, set.size()); + assertEquals("31d4d96e407aad42", set.iterator().next().getValue()); + + set = requestCookies.get("lang"); + assertEquals(1, set.size()); + assertEquals("en-US", set.iterator().next().getValue()); + + List headerValues = response.getHeaders().get("Set-Cookie"); + assertEquals(2, headerValues.size()); + + List parts = splitCookieHeader(headerValues.get(0)); + assertThat(parts, containsInAnyOrder(equalTo("SID=31d4d96e407aad42"), + equalToIgnoringCase("Path=/"), equalToIgnoringCase("Secure"), + equalToIgnoringCase("HttpOnly"))); + + parts = splitCookieHeader(headerValues.get(1)); + assertThat(parts, containsInAnyOrder(equalTo("lang=en-US"), + equalToIgnoringCase("Path=/"), equalToIgnoringCase("Domain=example.com"))); + } + + // No client side HttpCookie support yet + private List splitCookieHeader(String value) { + List list = new ArrayList<>(); + for (String s : value.split(";")){ + list.add(s.trim()); + } + return list; + } + + + private class CookieHandler implements HttpHandler { + + private Map> requestCookies; + + + @Override + public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { + + this.requestCookies = request.getHeaders().getCookies(); + this.requestCookies.size(); // Cause lazy loading + + response.getHeaders().addCookie(new HttpCookie("SID", "31d4d96e407aad42") + .setPath("/").setHttpOnly(true).setSecure(true)); + response.getHeaders().addCookie(new HttpCookie("lang", "en-US") + .setDomain("example.com").setPath("/")); + response.writeHeaders(); + + return Mono.empty(); + } + } + +} From ae4b35ced75e8e2882640d5645d482a77a38d19d Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Mon, 11 Jan 2016 13:02:36 +0100 Subject: [PATCH 154/498] Add Mono and Flux support in @RequestMapping handler methods --- .../RequestBodyArgumentResolver.java | 7 ++ .../RequestMappingIntegrationTests.java | 95 ++++++++++++++++++- 2 files changed, 97 insertions(+), 5 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestBodyArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestBodyArgumentResolver.java index 3e5c01306b..2ad7e92b1e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestBodyArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestBodyArgumentResolver.java @@ -76,7 +76,14 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve if (this.conversionService.canConvert(Publisher.class, type.getRawClass())) { return Mono.just(this.conversionService.convert(elementFlux, type.getRawClass())); } + else if (type.getRawClass() == Flux.class) { + return Mono.just(elementFlux); + } + else if (type.getRawClass() == Mono.class) { + return Mono.just(Mono.from(elementFlux)); + } + // TODO Currently manage only "Foo" parameter, not "List" parameters, Stéphane is going to add toIterable/toIterator to Flux to support that use case return elementFlux.next().map(o -> o); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java index 1175641212..1de455fe7c 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java @@ -25,6 +25,7 @@ import java.util.concurrent.CompletableFuture; import org.junit.Test; import org.reactivestreams.Publisher; +import reactor.Flux; import reactor.Mono; import reactor.io.buffer.Buffer; import reactor.rx.Promise; @@ -61,7 +62,6 @@ import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.client.RestTemplate; import org.springframework.web.reactive.DispatcherHandler; import org.springframework.web.reactive.handler.SimpleHandlerResultHandler; -import org.springframework.web.server.WebToHttpHandlerAdapter; import org.springframework.web.server.WebToHttpHandlerBuilder; import static org.junit.Assert.assertEquals; @@ -113,7 +113,19 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati } @Test - public void rawHelloResponse() throws Exception { + public void rawFluxResponse() throws Exception { + + RestTemplate restTemplate = new RestTemplate(); + + URI url = new URI("http://localhost:" + port + "/raw-flux"); + RequestEntity request = RequestEntity.get(url).build(); + ResponseEntity response = restTemplate.exchange(request, String.class); + + assertEquals("Hello!", response.getBody()); + } + + @Test + public void rawObservableResponse() throws Exception { RestTemplate restTemplate = new RestTemplate(); @@ -158,6 +170,11 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati serializeAsPojo("http://localhost:" + port + "/completable-future"); } + @Test + public void serializeAsMono() throws Exception { + serializeAsPojo("http://localhost:" + port + "/mono"); + } + @Test public void serializeAsSingle() throws Exception { serializeAsPojo("http://localhost:" + port + "/single"); @@ -178,6 +195,11 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati serializeAsCollection("http://localhost:" + port + "/publisher"); } + @Test + public void serializeAsFlux() throws Exception { + serializeAsCollection("http://localhost:" + port + "/flux"); + } + @Test public void serializeAsObservable() throws Exception { serializeAsCollection("http://localhost:" + port + "/observable"); @@ -193,6 +215,11 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati capitalizeCollection("http://localhost:" + port + "/publisher-capitalize"); } + @Test + public void fluxCapitalize() throws Exception { + capitalizeCollection("http://localhost:" + port + "/flux-capitalize"); + } + @Test public void observableCapitalize() throws Exception { capitalizeCollection("http://localhost:" + port + "/observable-capitalize"); @@ -213,6 +240,11 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati capitalizePojo("http://localhost:" + port + "/completable-future-capitalize"); } + @Test + public void monoCapitalize() throws Exception { + capitalizePojo("http://localhost:" + port + "/mono-capitalize"); + } + @Test public void singleCapitalize() throws Exception { capitalizePojo("http://localhost:" + port + "/single-capitalize"); @@ -228,6 +260,11 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati create("http://localhost:" + this.port + "/publisher-create"); } + @Test + public void fluxCreate() throws Exception { + create("http://localhost:" + this.port + "/flux-create"); + } + @Test public void streamCreate() throws Exception { create("http://localhost:" + this.port + "/stream-create"); @@ -384,12 +421,24 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati ResolvableType.forClass(Person.class), MediaType.APPLICATION_JSON); } + @RequestMapping("/raw-flux") + @ResponseBody + public Flux rawFluxResponseBody() { + return Flux.just(Buffer.wrap("Hello!").byteBuffer()); + } + @RequestMapping("/raw-observable") @ResponseBody public Observable rawObservableResponseBody() { return Observable.just(Buffer.wrap("Hello!").byteBuffer()); } + @RequestMapping("/mono") + @ResponseBody + public Mono monoResponseBody() { + return Mono.just(new Person("Robert")); + } + @RequestMapping("/single") @ResponseBody public Single singleResponseBody() { @@ -414,6 +463,12 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati return Stream.just(new Person("Robert"), new Person("Marie")); } + @RequestMapping("/flux") + @ResponseBody + public Flux fluxResponseBody() { + return Flux.just(new Person("Robert"), new Person("Marie")); + } + @RequestMapping("/observable") @ResponseBody public Observable observableResponseBody() { @@ -435,6 +490,15 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati }); } + @RequestMapping("/flux-capitalize") + @ResponseBody + public Flux fluxCapitalize(@RequestBody Flux persons) { + return persons.map(person -> { + person.setName(person.getName().toUpperCase()); + return person; + }); + } + @RequestMapping("/observable-capitalize") @ResponseBody public Observable observableCapitalize(@RequestBody Observable persons) { @@ -471,6 +535,15 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati }); } + @RequestMapping("/mono-capitalize") + @ResponseBody + public Mono monoCapitalize(@RequestBody Mono personFuture) { + return personFuture.map(person -> { + person.setName(person.getName().toUpperCase()); + return person; + }); + } + @RequestMapping("/single-capitalize") @ResponseBody public Single singleCapitalize(@RequestBody Single personFuture) { @@ -491,12 +564,17 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @RequestMapping("/publisher-create") public Publisher publisherCreate(@RequestBody Publisher personStream) { - return Stream.from(personStream).toList().doOnSuccess(persons::addAll).after(); + return Flux.from(personStream).doOnNext(persons::add).after(); + } + + @RequestMapping("/flux-create") + public Mono fluxCreate(@RequestBody Flux personStream) { + return personStream.doOnNext(persons::add).after(); } @RequestMapping("/stream-create") public Publisher streamCreate(@RequestBody Stream personStream) { - return Stream.from(personStream.toList().doOnSuccess(persons::addAll).after()).promise(); + return personStream.toList().doOnSuccess(persons::addAll).after(); } @RequestMapping("/observable-create") @@ -519,7 +597,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @ExceptionHandler @ResponseBody public Publisher handleException(IllegalStateException ex) { - return Stream.just("Recovered from error: " + ex.getMessage()); + return Mono.just("Recovered from error: " + ex.getMessage()); } //TODO add mixed and T request mappings tests @@ -562,6 +640,13 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati public int hashCode() { return this.name != null ? this.name.hashCode() : 0; } + + @Override + public String toString() { + return "Person{" + + "name='" + name + '\'' + + '}'; + } } } From c3cde84e6b47b909955e92d2f15abcd8745a8fa2 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 11 Jan 2016 17:15:55 -0500 Subject: [PATCH 155/498] Polish cookie support HttpCookie is now immutable with factory methods to create a client cookie (name-value) vs a server cookie (name-value + attributes) including a builder for the latter. --- .../org/springframework/http/HttpCookie.java | 218 ++++++++++++++---- .../org/springframework/http/HttpHeaders.java | 23 +- .../reactive/AbstractServerHttpRequest.java | 35 +-- .../reactive/ReactorServerHttpRequest.java | 4 +- .../reactive/RxNettyServerHttpRequest.java | 21 +- .../reactive/RxNettyServerHttpResponse.java | 4 +- .../reactive/ServletServerHttpRequest.java | 21 +- .../reactive/ServletServerHttpResponse.java | 4 +- .../reactive/UndertowServerHttpRequest.java | 21 +- .../reactive/UndertowServerHttpResponse.java | 4 +- .../reactive/CookieIntegrationTests.java | 36 ++- 11 files changed, 256 insertions(+), 135 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/HttpCookie.java b/spring-web-reactive/src/main/java/org/springframework/http/HttpCookie.java index 18a140d236..5021b62614 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/HttpCookie.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/HttpCookie.java @@ -15,32 +15,52 @@ */ package org.springframework.http; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + /** * Representation for an HTTP Cookie. * + *

    Use the {@link #clientCookie} factory method to create a client-to-server, + * name-value pair cookie and the {@link #serverCookie} factory method to build + * a server-to-client cookie with additional attributes. + * * @author Rossen Stoyanchev * @see RFC 6265 */ -public class HttpCookie { +public final class HttpCookie { private final String name; private final String value; - private String domain; + private final int maxAge; - private String path; + private final String domain; - private long maxAge = Long.MIN_VALUE; + private final String path; - private boolean secure; + private final boolean secure; - private boolean httpOnly; + private final boolean httpOnly; - public HttpCookie(String name, String value) { + private HttpCookie(String name, String value) { + this(name, value, -1, null, null, false, false); + } + + private HttpCookie(String name, String value, int maxAge, String domain, String path, + boolean secure, boolean httpOnly) { + + Assert.hasLength(name, "'name' is required and must not be empty."); + Assert.hasLength(value, "'value' is required and must not be empty."); this.name = name; this.value = value; + this.maxAge = (maxAge > -1 ? maxAge : -1); + this.domain = domain; + this.path = path; + this.secure = secure; + this.httpOnly = httpOnly; } /** @@ -57,66 +77,180 @@ public class HttpCookie { return this.value; } - public HttpCookie setPath(String path) { - this.path = path; - return this; + /** + * Return the cookie "Max-Age" attribute in seconds. + * + *

    A positive value indicates when the cookie expires relative to the + * current time. A value of 0 means the cookie should expire immediately. + * A negative value means no "Max-Age" attribute in which case the cookie + * is removed when the browser is closed. + */ + public int getMaxAge() { + return this.maxAge; } /** - * Return the domain attribute of the cookie. + * Return the cookie "Domain" attribute. */ public String getDomain() { return this.domain; } - public HttpCookie setDomain(String domain) { - this.domain = domain; - return this; - } - /** - * Return the path attribute of the cookie. + * Return the cookie "Path" attribute. */ public String getPath() { return this.path; } - public HttpCookie setMaxAge(long maxAge) { - this.maxAge = maxAge; - return this; - } - /** - * Return the maximum age attribute of the cookie in seconds or - * {@link Long#MIN_VALUE} if not set. - */ - public long getMaxAge() { - return this.maxAge; - } - - public HttpCookie setSecure(boolean secure) { - this.secure = secure; - return this; - } - - /** - * Return true if the "Secure" attribute of the cookie is present. + * Return {@code true} if the cookie has the "Secure" attribute. */ public boolean isSecure() { return this.secure; } - public HttpCookie setHttpOnly(boolean httpOnly) { - this.httpOnly = httpOnly; - return this; - } - /** - * Return true if the "HttpOnly" attribute of the cookie is present. + * Return {@code true} if the cookie has the "HttpOnly" attribute. * @see http://www.owasp.org/index.php/HTTPOnly */ public boolean isHttpOnly() { return this.httpOnly; } + @Override + public int hashCode() { + int result = this.name.hashCode(); + result = 31 * result + ObjectUtils.nullSafeHashCode(this.domain); + result = 31 * result + ObjectUtils.nullSafeHashCode(this.path); + return result; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof HttpCookie)) { + return false; + } + HttpCookie otherCookie = (HttpCookie) other; + return (this.name.equalsIgnoreCase(otherCookie.getName()) && + ObjectUtils.nullSafeEquals(this.path, otherCookie.getPath()) && + ObjectUtils.nullSafeEquals(this.domain, otherCookie.getDomain())); + } + + /** + * Factory method to create a cookie sent from a client to a server. + * Client cookies are name-value pairs only without attributes. + * @param name the cookie name + * @param value the cookie value + * @return the created cookie instance + */ + public static HttpCookie clientCookie(String name, String value) { + return new HttpCookie(name, value); + } + + /** + * Factory method to obtain a builder for a server-defined cookie that starts + * with a name-value pair and may also include attributes. + * @param name the cookie name + * @param value the cookie value + * @return the created cookie instance + */ + public static HttpCookieBuilder serverCookie(final String name, final String value) { + + return new HttpCookieBuilder() { + + private int maxAge = -1; + + private String domain; + + private String path; + + private boolean secure; + + private boolean httpOnly; + + + @Override + public HttpCookieBuilder maxAge(int maxAge) { + this.maxAge = maxAge; + return this; + } + + @Override + public HttpCookieBuilder domain(String domain) { + this.domain = domain; + return this; + } + + @Override + public HttpCookieBuilder path(String path) { + this.path = path; + return this; + } + + @Override + public HttpCookieBuilder secure() { + this.secure = true; + return this; + } + + @Override + public HttpCookieBuilder httpOnly() { + this.httpOnly = true; + return this; + } + + @Override + public HttpCookie build() { + return new HttpCookie(name, value, this.maxAge, this.domain, this.path, + this.secure, this.httpOnly); + } + }; + } + + /** + * A builder for a server-defined HttpCookie with attributes. + */ + public interface HttpCookieBuilder { + + /** + * Set the cookie "Max-Age" attribute in seconds. + * + *

    A positive value indicates when the cookie should expire relative + * to the current time. A value of 0 means the cookie should expire + * immediately. A negative value results in no "Max-Age" attribute in + * which case the cookie is removed when the browser is closed. + */ + HttpCookieBuilder maxAge(int maxAge); + + /** + * Set the cookie "Path" attribute. + */ + HttpCookieBuilder path(String path); + + /** + * Set the cookie "Domain" attribute. + */ + HttpCookieBuilder domain(String domain); + + /** + * Add the "Secure" attribute to the cookie. + */ + HttpCookieBuilder secure(); + + /** + * Add the "HttpOnly" attribute to the cookie. + * @see http://www.owasp.org/index.php/HTTPOnly + */ + HttpCookieBuilder httpOnly(); + + /** + * Create the HttpCookie. + */ + HttpCookie build(); + } + } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/HttpHeaders.java b/spring-web-reactive/src/main/java/org/springframework/http/HttpHeaders.java index 26380c84f8..419e94b947 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/HttpHeaders.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/HttpHeaders.java @@ -28,7 +28,6 @@ import java.util.Date; import java.util.EnumSet; import java.util.Iterator; import java.util.LinkedHashMap; -import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Locale; @@ -366,7 +365,7 @@ public class HttpHeaders implements MultiValueMap, Serializable private final Map> headers; - private final Map> cookies; + private final Map> cookies; /** @@ -377,11 +376,11 @@ public class HttpHeaders implements MultiValueMap, Serializable } /** - * Constructor with a map of HTTP cookies that enables lazy initialization - * of input cookies on first access of the map. - * @param inputCookies a Map with input cookies + * Constructor with a map of HTTP input cookies (e.g. cookies sent by client) + * that enables lazy initialization on first access of the map. + * @param inputCookies input cookies */ - public HttpHeaders(Map> inputCookies) { + public HttpHeaders(Map> inputCookies) { this(new LinkedCaseInsensitiveMap>(8, Locale.ENGLISH), inputCookies, false); Assert.notNull(cookies, "'inputCookies' is required."); } @@ -389,7 +388,7 @@ public class HttpHeaders implements MultiValueMap, Serializable /** * Private constructor that can create read-only {@code HttpHeader} instances. */ - private HttpHeaders(Map> headers, Map> cookies, + private HttpHeaders(Map> headers, Map> cookies, boolean readOnly) { Assert.notNull(headers, "'headers' must not be null"); @@ -405,7 +404,7 @@ public class HttpHeaders implements MultiValueMap, Serializable } else { this.headers = headers; - this.cookies = (cookies != null ? cookies : new LinkedHashMap<>()); + this.cookies = (cookies != null ? cookies : new LinkedCaseInsensitiveMap<>()); } } @@ -722,13 +721,13 @@ public class HttpHeaders implements MultiValueMap, Serializable } /** - * Add an HTTP cookie. Supported only when writing output cookies. + * Add an HTTP cookie. */ public void addCookie(HttpCookie cookie) { String name = cookie.getName(); - Set set = this.cookies.get(name); + List set = this.cookies.get(name); if (set == null) { - set = new LinkedHashSet<>(); + set = new ArrayList<>(); this.cookies.put(name, set); } set.add(cookie); @@ -738,7 +737,7 @@ public class HttpHeaders implements MultiValueMap, Serializable * Return a map with {@link HttpCookie}s. When reading input cookies this map * cannot be modified. When writing output cookies, this map is mutable. */ - public Map> getCookies() { + public Map> getCookies() { return this.cookies; } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java index 24eeeb61c1..ebc5641cb1 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java @@ -18,12 +18,13 @@ package org.springframework.http.server.reactive; import java.net.URI; import java.net.URISyntaxException; import java.util.Collection; -import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Set; -import org.springframework.http.HttpHeaders; import org.springframework.http.HttpCookie; +import org.springframework.http.HttpHeaders; +import org.springframework.util.LinkedCaseInsensitiveMap; /** * Common base class for {@link ServerHttpRequest} implementations. @@ -78,20 +79,20 @@ public abstract class AbstractServerHttpRequest implements ServerHttpRequest { * first access to cookies via {@link #getHeaders()} and then cached. * @param cookies the map to add cookies to */ - protected abstract void initCookies(Map> cookies); + protected abstract void initCookies(Map> cookies); /** * Read-only map of input cookies with lazy initialization. */ - private class HttpCookieInputMap implements Map> { + private class HttpCookieInputMap implements Map> { - private Map> cookies; + private Map> cookies; - private Map> getCookies() { + private Map> getCookies() { if (this.cookies == null) { - this.cookies = new LinkedHashMap<>(); + this.cookies = new LinkedCaseInsensitiveMap<>(); initCookies(this.cookies); } return this.cookies; @@ -118,7 +119,7 @@ public abstract class AbstractServerHttpRequest implements ServerHttpRequest { } @Override - public Set get(Object key) { + public List get(Object key) { return getCookies().get(key); } @@ -128,33 +129,33 @@ public abstract class AbstractServerHttpRequest implements ServerHttpRequest { } @Override - public Collection> values() { + public Collection> values() { return getCookies().values(); } @Override - public Set>> entrySet() { + public Set>> entrySet() { return getCookies().entrySet(); } @Override - public Set put(String key, Set value) { - throw new UnsupportedOperationException("Read-only map of cookies."); + public List put(String key, List value) { + throw new UnsupportedOperationException("Can't modify client sent cookies."); } @Override - public Set remove(Object key) { - throw new UnsupportedOperationException("Read-only map of cookies."); + public List remove(Object key) { + throw new UnsupportedOperationException("Can't modify client sent cookies."); } @Override - public void putAll(Map> m) { - throw new UnsupportedOperationException("Read-only map of cookies."); + public void putAll(Map> map) { + throw new UnsupportedOperationException("Can't modify client sent cookies."); } @Override public void clear() { - throw new UnsupportedOperationException("Read-only map of cookies."); + throw new UnsupportedOperationException("Can't modify client sent cookies."); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java index 4ae9fc9f0a..9cf3260bb8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java @@ -18,8 +18,8 @@ package org.springframework.http.server.reactive; import java.net.URI; import java.net.URISyntaxException; import java.nio.ByteBuffer; +import java.util.List; import java.util.Map; -import java.util.Set; import reactor.Flux; import reactor.io.buffer.Buffer; @@ -68,7 +68,7 @@ public class ReactorServerHttpRequest extends AbstractServerHttpRequest { } @Override - protected void initCookies(Map> cookies) { + protected void initCookies(Map> cookies) { // https://github.com/reactor/reactor/issues/614 } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java index 3e6c541f66..aa72c3f728 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java @@ -19,9 +19,9 @@ package org.springframework.http.server.reactive; import java.net.URI; import java.net.URISyntaxException; import java.nio.ByteBuffer; -import java.util.LinkedHashSet; +import java.util.ArrayList; +import java.util.List; import java.util.Map; -import java.util.Set; import io.netty.buffer.ByteBuf; import io.netty.handler.codec.http.cookie.Cookie; @@ -74,20 +74,15 @@ public class RxNettyServerHttpRequest extends AbstractServerHttpRequest { } @Override - protected void initCookies(Map> map) { + protected void initCookies(Map> map) { for (String name : this.request.getCookies().keySet()) { - Set set = map.get(name); - if (set == null) { - set = new LinkedHashSet<>(); - map.put(name, set); + List list = map.get(name); + if (list == null) { + list = new ArrayList<>(); + map.put(name, list); } for (Cookie cookie : this.request.getCookies().get(name)) { - set.add(new HttpCookie(name, cookie.value()) - .setDomain(cookie.domain()) - .setPath(cookie.path()) - .setMaxAge(cookie.maxAge()) - .setSecure(cookie.isSecure()) - .setHttpOnly(cookie.isHttpOnly())); + list.add(HttpCookie.clientCookie(name, cookie.value())); } } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java index 467c1d6dfd..863d71f5ca 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java @@ -83,9 +83,11 @@ public class RxNettyServerHttpResponse extends AbstractServerHttpResponse { for (String name : getHeaders().getCookies().keySet()) { for (HttpCookie httpCookie : getHeaders().getCookies().get(name)) { Cookie cookie = new DefaultCookie(name, httpCookie.getValue()); + if (httpCookie.getMaxAge() > -1) { + cookie.setMaxAge(httpCookie.getMaxAge()); + } cookie.setDomain(httpCookie.getDomain()); cookie.setPath(httpCookie.getPath()); - cookie.setMaxAge(httpCookie.getMaxAge()); cookie.setSecure(httpCookie.isSecure()); cookie.setHttpOnly(httpCookie.isHttpOnly()); this.response.addCookie(cookie); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java index 867c9356a2..6dd59e9379 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java @@ -20,10 +20,10 @@ import java.net.URI; import java.net.URISyntaxException; import java.nio.ByteBuffer; import java.nio.charset.Charset; +import java.util.ArrayList; import java.util.Enumeration; -import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; -import java.util.Set; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; @@ -111,20 +111,15 @@ public class ServletServerHttpRequest extends AbstractServerHttpRequest { } @Override - protected void initCookies(Map> map) { + protected void initCookies(Map> map) { for (Cookie cookie : this.request.getCookies()) { String name = cookie.getName(); - Set set = map.get(name); - if (set == null) { - set = new LinkedHashSet<>(); - map.put(name, set); + List list = map.get(name); + if (list == null) { + list = new ArrayList<>(); + map.put(name, list); } - set.add(new HttpCookie(name, cookie.getValue()) - .setDomain(cookie.getDomain()) - .setPath(cookie.getPath()) - .setMaxAge(cookie.getMaxAge() == -1 ? Long.MIN_VALUE : cookie.getMaxAge()) - .setHttpOnly(cookie.isHttpOnly()) - .setSecure(cookie.getSecure())); + list.add(HttpCookie.clientCookie(name, cookie.getValue())); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java index a4e7e91446..53f5dcc349 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java @@ -91,13 +91,15 @@ public class ServletServerHttpResponse extends AbstractServerHttpResponse { for (String name : getHeaders().getCookies().keySet()) { for (HttpCookie httpCookie : getHeaders().getCookies().get(name)) { Cookie cookie = new Cookie(name, httpCookie.getValue()); + if (httpCookie.getMaxAge() > -1) { + cookie.setMaxAge(httpCookie.getMaxAge()); + } if (httpCookie.getDomain() != null) { cookie.setDomain(httpCookie.getDomain()); } if (httpCookie.getPath() != null) { cookie.setPath(httpCookie.getPath()); } - cookie.setMaxAge(httpCookie.getMaxAge() == Long.MIN_VALUE ? -1 : (int) httpCookie.getMaxAge()); cookie.setSecure(httpCookie.isSecure()); cookie.setHttpOnly(httpCookie.isHttpOnly()); this.response.addCookie(cookie); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java index 53013fbe54..6f8c35b7ff 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java @@ -19,9 +19,9 @@ package org.springframework.http.server.reactive; import java.net.URI; import java.net.URISyntaxException; import java.nio.ByteBuffer; -import java.util.LinkedHashSet; +import java.util.ArrayList; +import java.util.List; import java.util.Map; -import java.util.Set; import io.undertow.server.HttpServerExchange; import io.undertow.server.handlers.Cookie; @@ -79,20 +79,15 @@ public class UndertowServerHttpRequest extends AbstractServerHttpRequest { } @Override - protected void initCookies(Map> map) { + protected void initCookies(Map> map) { for (String name : this.exchange.getRequestCookies().keySet()) { - Set set = map.get(name); - if (set == null) { - set = new LinkedHashSet<>(); - map.put(name, set); + List list = map.get(name); + if (list == null) { + list = new ArrayList<>(); + map.put(name, list); } Cookie cookie = this.exchange.getRequestCookies().get(name); - set.add(new HttpCookie(name, cookie.getValue()) - .setDomain(cookie.getDomain()) - .setPath(cookie.getPath()) - .setMaxAge(cookie.getMaxAge() != null ? cookie.getMaxAge() : Long.MIN_VALUE) - .setSecure(cookie.isSecure()) - .setHttpOnly(cookie.isHttpOnly())); + list.add(HttpCookie.clientCookie(name, cookie.getValue())); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java index 27c38342f8..3057396587 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java @@ -83,9 +83,11 @@ public class UndertowServerHttpResponse extends AbstractServerHttpResponse { for (String name : getHeaders().getCookies().keySet()) { for (HttpCookie httpCookie : getHeaders().getCookies().get(name)) { Cookie cookie = new CookieImpl(name, httpCookie.getValue()); + if (httpCookie.getMaxAge() > -1) { + cookie.setMaxAge(httpCookie.getMaxAge()); + } cookie.setDomain(httpCookie.getDomain()); cookie.setPath(httpCookie.getPath()); - cookie.setMaxAge(httpCookie.getMaxAge() == Long.MIN_VALUE ? null : (int) httpCookie.getMaxAge()); cookie.setSecure(httpCookie.isSecure()); cookie.setHttpOnly(httpCookie.isHttpOnly()); this.exchange.getResponseCookies().putIfAbsent(name, cookie); diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/CookieIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/CookieIntegrationTests.java index 6a1dccb4ae..dbb866219e 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/CookieIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/CookieIntegrationTests.java @@ -19,7 +19,6 @@ import java.net.URI; import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.Set; import org.junit.After; import org.junit.Before; @@ -102,32 +101,29 @@ public class CookieIntegrationTests { ResponseEntity response = new RestTemplate().exchange( RequestEntity.get(url).header("Cookie", header).build(), Void.class); - Map> requestCookies = this.cookieHandler.requestCookies; + Map> requestCookies = this.cookieHandler.requestCookies; assertEquals(2, requestCookies.size()); - Set set = requestCookies.get("SID"); - assertEquals(1, set.size()); - assertEquals("31d4d96e407aad42", set.iterator().next().getValue()); + List list = requestCookies.get("SID"); + assertEquals(1, list.size()); + assertEquals("31d4d96e407aad42", list.iterator().next().getValue()); - set = requestCookies.get("lang"); - assertEquals(1, set.size()); - assertEquals("en-US", set.iterator().next().getValue()); + list = requestCookies.get("lang"); + assertEquals(1, list.size()); + assertEquals("en-US", list.iterator().next().getValue()); List headerValues = response.getHeaders().get("Set-Cookie"); assertEquals(2, headerValues.size()); - List parts = splitCookieHeader(headerValues.get(0)); - assertThat(parts, containsInAnyOrder(equalTo("SID=31d4d96e407aad42"), - equalToIgnoringCase("Path=/"), equalToIgnoringCase("Secure"), - equalToIgnoringCase("HttpOnly"))); + assertThat(splitCookie(headerValues.get(0)), containsInAnyOrder(equalTo("SID=31d4d96e407aad42"), + equalToIgnoringCase("Path=/"), equalToIgnoringCase("Secure"), equalToIgnoringCase("HttpOnly"))); - parts = splitCookieHeader(headerValues.get(1)); - assertThat(parts, containsInAnyOrder(equalTo("lang=en-US"), + assertThat(splitCookie(headerValues.get(1)), containsInAnyOrder(equalTo("lang=en-US"), equalToIgnoringCase("Path=/"), equalToIgnoringCase("Domain=example.com"))); } // No client side HttpCookie support yet - private List splitCookieHeader(String value) { + private List splitCookie(String value) { List list = new ArrayList<>(); for (String s : value.split(";")){ list.add(s.trim()); @@ -138,7 +134,7 @@ public class CookieIntegrationTests { private class CookieHandler implements HttpHandler { - private Map> requestCookies; + private Map> requestCookies; @Override @@ -147,10 +143,10 @@ public class CookieIntegrationTests { this.requestCookies = request.getHeaders().getCookies(); this.requestCookies.size(); // Cause lazy loading - response.getHeaders().addCookie(new HttpCookie("SID", "31d4d96e407aad42") - .setPath("/").setHttpOnly(true).setSecure(true)); - response.getHeaders().addCookie(new HttpCookie("lang", "en-US") - .setDomain("example.com").setPath("/")); + response.getHeaders().addCookie(HttpCookie.serverCookie("SID", "31d4d96e407aad42") + .path("/").secure().httpOnly().build()); + response.getHeaders().addCookie(HttpCookie.serverCookie("lang", "en-US") + .domain("example.com").path("/").build()); response.writeHeaders(); return Mono.empty(); From d9b67f5e726f6c0bcfc686a15af735466e5c4bb8 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Tue, 12 Jan 2016 11:31:18 +0100 Subject: [PATCH 156/498] Use Mono semantics for JSON object/array serialization Before this commit, a handler method returning a stream with a JSON content-type was producing a JSON object for single element streams or a JSON array for multiple elements streams. This kind of dynamic change of the output based on the number of elements was difficult to handle on client side and not consistent with Spring MVC behavior. With this commit, we achieve a more consistent behavior by using the Mono semantics to control this behavior. Mono (and Promise/Single) are serialized to JSON object and Flux (and Observable/Stream) are serialized to JSON array. --- .../codec/support/JacksonJsonEncoder.java | 34 +++++++------- .../core/codec/support/JsonObjectEncoder.java | 45 ++++++++++--------- .../codec/encoder/JsonObjectEncoderTests.java | 42 ++++++++++------- 3 files changed, 68 insertions(+), 53 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonEncoder.java index d1f78ee5d5..bd4426ce39 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonEncoder.java @@ -23,6 +23,7 @@ import java.nio.charset.StandardCharsets; import com.fasterxml.jackson.databind.ObjectMapper; import org.reactivestreams.Publisher; import reactor.Flux; +import reactor.Mono; import reactor.io.buffer.Buffer; import org.springframework.core.ResolvableType; @@ -64,22 +65,23 @@ public class JacksonJsonEncoder extends AbstractEncoder { public Flux encode(Publisher inputStream, ResolvableType type, MimeType mimeType, Object... hints) { - Flux stream = Flux.from(inputStream).map(value -> { - Buffer buffer = new Buffer(); - BufferOutputStream outputStream = new BufferOutputStream(buffer); - try { - this.mapper.writeValue(outputStream, value); - } - catch (IOException e) { - throw new CodecException("Error while writing the data", e); - } - buffer.flip(); - return buffer.byteBuffer(); - }); - if (this.postProcessor != null) { - stream = this.postProcessor.encode(stream, type, mimeType, hints); - }; - return stream; + Publisher stream = (inputStream instanceof Mono ? + ((Mono)inputStream).map(this::serialize) : + Flux.from(inputStream).map(this::serialize)); + return (this.postProcessor == null ? Flux.from(stream) : this.postProcessor.encode(stream, type, mimeType, hints)); + } + + private ByteBuffer serialize(Object value) { + Buffer buffer = new Buffer(); + BufferOutputStream outputStream = new BufferOutputStream(buffer); + try { + this.mapper.writeValue(outputStream, value); + } + catch (IOException e) { + throw new CodecException("Error while writing the data", e); + } + buffer.flip(); + return buffer.byteBuffer(); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectEncoder.java index d00ef799a9..9b8f09e910 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectEncoder.java @@ -24,6 +24,7 @@ import java.util.concurrent.atomic.AtomicLongFieldUpdater; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import reactor.Flux; +import reactor.Mono; import reactor.core.subscriber.SubscriberBarrier; import reactor.core.support.BackpressureUtils; import reactor.io.buffer.Buffer; @@ -32,8 +33,9 @@ import org.springframework.core.ResolvableType; import org.springframework.util.MimeType; /** - * Encode a byte stream of individual JSON element to a byte stream representing - * a single JSON array when if it contains more than one element. + * Encode a byte stream of individual JSON element to a byte stream representing: + * - the same JSON object than the input stream if it is a {@link Mono} + * - a JSON array for other kinds of {@link Publisher} * * @author Sebastien Deleuze * @author Stephane Maldini @@ -48,22 +50,24 @@ public class JsonObjectEncoder extends AbstractEncoder { } @Override - public Flux encode(Publisher messageStream, + public Flux encode(Publisher inputStream, ResolvableType type, MimeType mimeType, Object... hints) { - //noinspection Convert2MethodRef - return Flux.from(messageStream).lift(bbs -> new JsonEncoderBarrier(bbs)); + if (inputStream instanceof Mono) { + return Flux.from(inputStream); + } + return Flux.from(inputStream).lift(s -> new JsonArrayEncoderBarrier(s)); } - private static class JsonEncoderBarrier extends SubscriberBarrier { + private static class JsonArrayEncoderBarrier extends SubscriberBarrier { @SuppressWarnings("rawtypes") - static final AtomicLongFieldUpdater REQUESTED = - AtomicLongFieldUpdater.newUpdater(JsonEncoderBarrier.class, "requested"); + static final AtomicLongFieldUpdater REQUESTED = + AtomicLongFieldUpdater.newUpdater(JsonArrayEncoderBarrier.class, "requested"); - static final AtomicIntegerFieldUpdater TERMINATED = - AtomicIntegerFieldUpdater.newUpdater(JsonEncoderBarrier.class, "terminated"); + static final AtomicIntegerFieldUpdater TERMINATED = + AtomicIntegerFieldUpdater.newUpdater(JsonArrayEncoderBarrier.class, "terminated"); private ByteBuffer prev = null; @@ -75,7 +79,7 @@ public class JsonObjectEncoder extends AbstractEncoder { private volatile int terminated; - public JsonEncoderBarrier(Subscriber subscriber) { + public JsonArrayEncoderBarrier(Subscriber subscriber) { super(subscriber); } @@ -94,20 +98,19 @@ public class JsonObjectEncoder extends AbstractEncoder { @Override protected void doNext(ByteBuffer next) { this.count++; - if (this.count == 1) { - this.prev = next; - super.doRequest(1); - return; - } ByteBuffer tmp = this.prev; this.prev = next; Buffer buffer = new Buffer(); - if (this.count == 2) { + if (this.count == 1) { buffer.append("["); } - buffer.append(tmp); - buffer.append(","); + if (tmp != null) { + buffer.append(tmp); + } + if (this.count > 1) { + buffer.append(","); + } buffer.flip(); BackpressureUtils.getAndSub(REQUESTED, this, 1L); @@ -118,9 +121,7 @@ public class JsonObjectEncoder extends AbstractEncoder { if(BackpressureUtils.getAndSub(REQUESTED, this, 1L) > 0) { Buffer buffer = new Buffer(); buffer.append(this.prev); - if (this.count > 1) { - buffer.append("]"); - } + buffer.append("]"); buffer.flip(); subscriber.onNext(buffer.byteBuffer()); super.doComplete(); diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/JsonObjectEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/JsonObjectEncoderTests.java index a41f62ad98..0d31bfda8a 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/JsonObjectEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/JsonObjectEncoderTests.java @@ -18,13 +18,12 @@ package org.springframework.reactive.codec.encoder; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; -import java.util.List; import static org.junit.Assert.assertEquals; import org.junit.Test; +import reactor.Flux; +import reactor.Mono; import reactor.io.buffer.Buffer; -import reactor.rx.Stream; -import reactor.rx.Streams; import org.springframework.core.codec.support.JsonObjectEncoder; @@ -34,46 +33,59 @@ import org.springframework.core.codec.support.JsonObjectEncoder; public class JsonObjectEncoderTests { @Test - public void encodeSingleElement() throws InterruptedException { + public void encodeSingleElementFlux() throws InterruptedException { JsonObjectEncoder encoder = new JsonObjectEncoder(); - Stream source = Streams.just(Buffer.wrap("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}").byteBuffer()); - List results = Streams.from(encoder.encode(source, null, null)).map(chunk -> { + Flux source = Flux.just(Buffer.wrap("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}").byteBuffer()); + Iterable results = Flux.from(encoder.encode(source, null, null)).map(chunk -> { byte[] b = new byte[chunk.remaining()]; chunk.get(b); return new String(b, StandardCharsets.UTF_8); - }).toList().get(); + }).toIterable(); + String result = String.join("", results); + assertEquals("[{\"foo\": \"foofoo\", \"bar\": \"barbar\"}]", result); + } + + @Test + public void encodeSingleElementMono() throws InterruptedException { + JsonObjectEncoder encoder = new JsonObjectEncoder(); + Mono source = Mono.just(Buffer.wrap("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}").byteBuffer()); + Iterable results = Flux.from(encoder.encode(source, null, null)).map(chunk -> { + byte[] b = new byte[chunk.remaining()]; + chunk.get(b); + return new String(b, StandardCharsets.UTF_8); + }).toIterable(); String result = String.join("", results); assertEquals("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}", result); } @Test - public void encodeTwoElements() throws InterruptedException { + public void encodeTwoElementsFlux() throws InterruptedException { JsonObjectEncoder encoder = new JsonObjectEncoder(); - Stream source = Streams.just( + Flux source = Flux.just( Buffer.wrap("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}").byteBuffer(), Buffer.wrap("{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}").byteBuffer()); - List results = Streams.from(encoder.encode(source, null, null)).map(chunk -> { + Iterable results = Flux.from(encoder.encode(source, null, null)).map(chunk -> { byte[] b = new byte[chunk.remaining()]; chunk.get(b); return new String(b, StandardCharsets.UTF_8); - }).toList().get(); + }).toIterable(); String result = String.join("", results); assertEquals("[{\"foo\": \"foofoo\", \"bar\": \"barbar\"},{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}]", result); } @Test - public void encodeThreeElements() throws InterruptedException { + public void encodeThreeElementsFlux() throws InterruptedException { JsonObjectEncoder encoder = new JsonObjectEncoder(); - Stream source = Streams.just( + Flux source = Flux.just( Buffer.wrap("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}").byteBuffer(), Buffer.wrap("{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}").byteBuffer(), Buffer.wrap("{\"foo\": \"foofoofoofoo\", \"bar\": \"barbarbarbar\"}").byteBuffer() ); - List results = Streams.from(encoder.encode(source, null, null)).map(chunk -> { + Iterable results = Flux.from(encoder.encode(source, null, null)).map(chunk -> { byte[] b = new byte[chunk.remaining()]; chunk.get(b); return new String(b, StandardCharsets.UTF_8); - }).toList().get(); + }).toIterable(); String result = String.join("", results); assertEquals("[{\"foo\": \"foofoo\", \"bar\": \"barbar\"},{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"},{\"foo\": \"foofoofoofoo\", \"bar\": \"barbarbarbar\"}]", result); } From 99e51a9101a83b6f625331e02348252f1c12c818 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Tue, 12 Jan 2016 15:27:02 +0100 Subject: [PATCH 157/498] Replace Reactor Stream by Flux in tests when possible --- .../util/ByteBufferPublisherInputStream.java | 1 + .../codec/decoder/ByteBufferDecoderTests.java | 11 ++-- .../decoder/JacksonJsonDecoderTests.java | 11 ++-- .../codec/decoder/Jaxb2DecoderTests.java | 11 ++-- .../codec/decoder/JsonObjectDecoderTests.java | 57 ++++++++++--------- .../codec/decoder/StringDecoderTests.java | 11 ++-- .../codec/encoder/ByteBufferEncoderTests.java | 11 ++-- .../encoder/JacksonJsonEncoderTests.java | 13 +++-- .../codec/encoder/Jaxb2EncoderTests.java | 18 +++--- .../codec/encoder/StringEncoderTests.java | 17 +++--- ...mpleUrlHandlerMappingIntegrationTests.java | 6 +- .../RequestMappingHandlerMappingTests.java | 6 +- 12 files changed, 97 insertions(+), 76 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/util/ByteBufferPublisherInputStream.java b/spring-web-reactive/src/main/java/org/springframework/util/ByteBufferPublisherInputStream.java index 92165fac16..abc1df3c44 100644 --- a/spring-web-reactive/src/main/java/org/springframework/util/ByteBufferPublisherInputStream.java +++ b/spring-web-reactive/src/main/java/org/springframework/util/ByteBufferPublisherInputStream.java @@ -60,6 +60,7 @@ public class ByteBufferPublisherInputStream extends InputStream { public ByteBufferPublisherInputStream(Publisher publisher, int requestSize) { Assert.notNull(publisher, "'publisher' must not be null"); + // TODO Avoid using Reactor Stream, it should not be a mandatory dependency of Spring Reactive this.queue = Stream.from(publisher).toBlockingQueue(requestSize); } diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/ByteBufferDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/ByteBufferDecoderTests.java index a934afd749..1259a0019c 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/ByteBufferDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/ByteBufferDecoderTests.java @@ -18,13 +18,14 @@ package org.springframework.reactive.codec.decoder; import java.nio.ByteBuffer; import java.util.List; +import java.util.stream.StreamSupport; +import static java.util.stream.Collectors.toList; import static org.junit.Assert.*; import org.junit.Test; import org.reactivestreams.Publisher; +import reactor.Flux; import reactor.io.buffer.Buffer; -import reactor.rx.Stream; -import reactor.rx.Streams; import org.springframework.core.ResolvableType; import org.springframework.core.codec.support.ByteBufferDecoder; @@ -48,9 +49,9 @@ public class ByteBufferDecoderTests { public void decode() throws InterruptedException { ByteBuffer fooBuffer = Buffer.wrap("foo").byteBuffer(); ByteBuffer barBuffer = Buffer.wrap("bar").byteBuffer(); - Stream source = Streams.just(fooBuffer, barBuffer); - List results = Streams.from(decoder.decode(source, - ResolvableType.forClassWithGenerics(Publisher.class, ByteBuffer.class), null)).toList().get(); + Flux source = Flux.just(fooBuffer, barBuffer); + Flux output = decoder.decode(source, ResolvableType.forClassWithGenerics(Publisher.class, ByteBuffer.class), null); + List results = StreamSupport.stream(output.toIterable().spliterator(), false).collect(toList()); assertEquals(2, results.size()); assertEquals(fooBuffer, results.get(0)); assertEquals(barBuffer, results.get(1)); diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoderTests.java index 8059a681ae..e80bd0298d 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoderTests.java @@ -18,14 +18,15 @@ package org.springframework.reactive.codec.decoder; import java.nio.ByteBuffer; import java.util.List; +import java.util.stream.StreamSupport; +import static java.util.stream.Collectors.toList; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import org.junit.Test; +import reactor.Flux; import reactor.io.buffer.Buffer; -import reactor.rx.Stream; -import reactor.rx.Streams; import org.springframework.core.ResolvableType; import org.springframework.core.codec.support.JacksonJsonDecoder; @@ -47,9 +48,9 @@ public class JacksonJsonDecoderTests { @Test public void decode() throws InterruptedException { - Stream source = Streams.just(Buffer.wrap("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}").byteBuffer()); - List results = Streams.from(decoder.decode(source, ResolvableType.forClass(Pojo.class), null)) - .toList().get(); + Flux source = Flux.just(Buffer.wrap("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}").byteBuffer()); + Flux output = decoder.decode(source, ResolvableType.forClass(Pojo.class), null); + List results = StreamSupport.stream(output.toIterable().spliterator(), false).collect(toList()); assertEquals(1, results.size()); assertEquals("foofoo", ((Pojo) results.get(0)).getFoo()); } diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/Jaxb2DecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/Jaxb2DecoderTests.java index 65b23e4ac3..69aaffdf8a 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/Jaxb2DecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/Jaxb2DecoderTests.java @@ -18,14 +18,15 @@ package org.springframework.reactive.codec.decoder; import java.nio.ByteBuffer; import java.util.List; +import java.util.stream.StreamSupport; +import static java.util.stream.Collectors.toList; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import org.junit.Test; +import reactor.Flux; import reactor.io.buffer.Buffer; -import reactor.rx.Stream; -import reactor.rx.Streams; import org.springframework.core.ResolvableType; import org.springframework.core.codec.support.Jaxb2Decoder; @@ -48,9 +49,9 @@ public class Jaxb2DecoderTests { @Test public void decode() throws InterruptedException { - Stream source = Streams.just(Buffer.wrap("barbarfoofoo").byteBuffer()); - List results = Streams.from(decoder.decode(source, ResolvableType.forClass(Pojo.class), null)) - .toList().get(); + Flux source = Flux.just(Buffer.wrap("barbarfoofoo").byteBuffer()); + Flux output = decoder.decode(source, ResolvableType.forClass(Pojo.class), null); + List results = StreamSupport.stream(output.toIterable().spliterator(), false).collect(toList()); assertEquals(1, results.size()); assertEquals("foofoo", ((Pojo) results.get(0)).getFoo()); } diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/JsonObjectDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/JsonObjectDecoderTests.java index ac3eea8399..6f3a5e1e1c 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/JsonObjectDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/JsonObjectDecoderTests.java @@ -19,12 +19,13 @@ package org.springframework.reactive.codec.decoder; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.List; +import java.util.stream.StreamSupport; +import static java.util.stream.Collectors.toList; import static org.junit.Assert.assertEquals; import org.junit.Test; +import reactor.Flux; import reactor.io.buffer.Buffer; -import reactor.rx.Stream; -import reactor.rx.Streams; import org.springframework.core.codec.support.JsonObjectDecoder; @@ -36,12 +37,13 @@ public class JsonObjectDecoderTests { @Test public void decodeSingleChunkToJsonObject() throws InterruptedException { JsonObjectDecoder decoder = new JsonObjectDecoder(); - Stream source = Streams.just(Buffer.wrap("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}").byteBuffer()); - List results = Streams.from(decoder.decode(source, null, null)).map(chunk -> { - byte[] b = new byte[chunk.remaining()]; - chunk.get(b); - return new String(b, StandardCharsets.UTF_8); - }).toList().get(); + Flux source = Flux.just(Buffer.wrap("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}").byteBuffer()); + Flux output = decoder.decode(source, null, null).map(chunk -> { + byte[] b = new byte[chunk.remaining()]; + chunk.get(b); + return new String(b, StandardCharsets.UTF_8); + }); + List results = StreamSupport.stream(output.toIterable().spliterator(), false).collect(toList()); assertEquals(1, results.size()); assertEquals("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}", results.get(0)); } @@ -49,12 +51,13 @@ public class JsonObjectDecoderTests { @Test public void decodeMultipleChunksToJsonObject() throws InterruptedException { JsonObjectDecoder decoder = new JsonObjectDecoder(); - Stream source = Streams.just(Buffer.wrap("{\"foo\": \"foofoo\"").byteBuffer(), Buffer.wrap(", \"bar\": \"barbar\"}").byteBuffer()); - List results = Streams.from(decoder.decode(source, null, null)).map(chunk -> { - byte[] b = new byte[chunk.remaining()]; - chunk.get(b); - return new String(b, StandardCharsets.UTF_8); - }).toList().get(); + Flux source = Flux.just(Buffer.wrap("{\"foo\": \"foofoo\"").byteBuffer(), Buffer.wrap(", \"bar\": \"barbar\"}").byteBuffer()); + Flux output = decoder.decode(source, null, null).map(chunk -> { + byte[] b = new byte[chunk.remaining()]; + chunk.get(b); + return new String(b, StandardCharsets.UTF_8); + }); + List results = StreamSupport.stream(output.toIterable().spliterator(), false).collect(toList()); assertEquals(1, results.size()); assertEquals("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}", results.get(0)); } @@ -62,12 +65,13 @@ public class JsonObjectDecoderTests { @Test public void decodeSingleChunkToArray() throws InterruptedException { JsonObjectDecoder decoder = new JsonObjectDecoder(); - Stream source = Streams.just(Buffer.wrap("[{\"foo\": \"foofoo\", \"bar\": \"barbar\"},{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}]").byteBuffer()); - List results = Streams.from(decoder.decode(source, null, null)).map(chunk -> { - byte[] b = new byte[chunk.remaining()]; - chunk.get(b); - return new String(b, StandardCharsets.UTF_8); - }).toList().get(); + Flux source = Flux.just(Buffer.wrap("[{\"foo\": \"foofoo\", \"bar\": \"barbar\"},{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}]").byteBuffer()); + Flux output = decoder.decode(source, null, null).map(chunk -> { + byte[] b = new byte[chunk.remaining()]; + chunk.get(b); + return new String(b, StandardCharsets.UTF_8); + }); + List results = StreamSupport.stream(output.toIterable().spliterator(), false).collect(toList()); assertEquals(2, results.size()); assertEquals("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}", results.get(0)); assertEquals("{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}", results.get(1)); @@ -76,12 +80,13 @@ public class JsonObjectDecoderTests { @Test public void decodeMultipleChunksToArray() throws InterruptedException { JsonObjectDecoder decoder = new JsonObjectDecoder(); - Stream source = Streams.just(Buffer.wrap("[{\"foo\": \"foofoo\", \"bar\"").byteBuffer(), Buffer.wrap(": \"barbar\"},{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}]").byteBuffer()); - List results = Streams.from(decoder.decode(source, null, null)).map(chunk -> { - byte[] b = new byte[chunk.remaining()]; - chunk.get(b); - return new String(b, StandardCharsets.UTF_8); - }).toList().get(); + Flux source = Flux.just(Buffer.wrap("[{\"foo\": \"foofoo\", \"bar\"").byteBuffer(), Buffer.wrap(": \"barbar\"},{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}]").byteBuffer()); + Flux output = decoder.decode(source, null, null).map(chunk -> { + byte[] b = new byte[chunk.remaining()]; + chunk.get(b); + return new String(b, StandardCharsets.UTF_8); + }); + List results = StreamSupport.stream(output.toIterable().spliterator(), false).collect(toList()); assertEquals(2, results.size()); assertEquals("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}", results.get(0)); assertEquals("{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}", results.get(1)); diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/StringDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/StringDecoderTests.java index 4e22989b9b..038d26e2b1 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/StringDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/StringDecoderTests.java @@ -18,15 +18,16 @@ package org.springframework.reactive.codec.decoder; import java.nio.ByteBuffer; import java.util.List; +import java.util.stream.StreamSupport; +import static java.util.stream.Collectors.toList; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import org.junit.Test; import org.reactivestreams.Publisher; +import reactor.Flux; import reactor.io.buffer.Buffer; -import reactor.rx.Stream; -import reactor.rx.Streams; import org.springframework.core.ResolvableType; import org.springframework.core.codec.support.StringDecoder; @@ -48,9 +49,9 @@ public class StringDecoderTests { @Test public void decode() throws InterruptedException { - Stream source = Streams.just(Buffer.wrap("foo").byteBuffer(), Buffer.wrap("bar").byteBuffer()); - List results = Streams.from(decoder.decode(source, - ResolvableType.forClassWithGenerics(Publisher.class, String.class), null)).toList().get(); + Flux source = Flux.just(Buffer.wrap("foo").byteBuffer(), Buffer.wrap("bar").byteBuffer()); + Flux output = decoder.decode(source, ResolvableType.forClassWithGenerics(Publisher.class, String.class), null); + List results = StreamSupport.stream(output.toIterable().spliterator(), false).collect(toList()); assertEquals(2, results.size()); assertEquals("foo", results.get(0)); assertEquals("bar", results.get(1)); diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/ByteBufferEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/ByteBufferEncoderTests.java index 168d150de9..c7612bb0aa 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/ByteBufferEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/ByteBufferEncoderTests.java @@ -18,13 +18,14 @@ package org.springframework.reactive.codec.encoder; import java.nio.ByteBuffer; import java.util.List; +import java.util.stream.StreamSupport; +import static java.util.stream.Collectors.toList; import static org.junit.Assert.*; import org.junit.Test; import org.reactivestreams.Publisher; +import reactor.Flux; import reactor.io.buffer.Buffer; -import reactor.rx.Stream; -import reactor.rx.Streams; import org.springframework.core.ResolvableType; import org.springframework.core.codec.support.ByteBufferEncoder; @@ -48,9 +49,9 @@ public class ByteBufferEncoderTests { public void decode() throws InterruptedException { ByteBuffer fooBuffer = Buffer.wrap("foo").byteBuffer(); ByteBuffer barBuffer = Buffer.wrap("bar").byteBuffer(); - Stream source = Streams.just(fooBuffer, barBuffer); - List results = Streams.from(encoder.encode(source, - ResolvableType.forClassWithGenerics(Publisher.class, ByteBuffer.class), null)).toList().get(); + Flux source = Flux.just(fooBuffer, barBuffer); + Flux output = encoder.encode(source, ResolvableType.forClassWithGenerics(Publisher.class, ByteBuffer.class), null); + List results = StreamSupport.stream(output.toIterable().spliterator(), false).collect(toList()); assertEquals(2, results.size()); assertEquals(fooBuffer, results.get(0)); assertEquals(barBuffer, results.get(1)); diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoderTests.java index 5350148d76..685c3d8e9f 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoderTests.java @@ -16,15 +16,17 @@ package org.springframework.reactive.codec.encoder; +import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.List; +import java.util.stream.StreamSupport; +import static java.util.stream.Collectors.toList; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import org.junit.Test; -import reactor.rx.Stream; -import reactor.rx.Streams; +import reactor.Flux; import org.springframework.core.codec.support.JacksonJsonEncoder; import org.springframework.http.MediaType; @@ -45,12 +47,13 @@ public class JacksonJsonEncoderTests { @Test public void write() throws InterruptedException { - Stream source = Streams.just(new Pojo("foofoo", "barbar"), new Pojo("foofoofoo", "barbarbar")); - List results = Streams.from(encoder.encode(source, null, null)).map(chunk -> { + Flux source = Flux.just(new Pojo("foofoo", "barbar"), new Pojo("foofoofoo", "barbarbar")); + Flux output = encoder.encode(source, null, null).map(chunk -> { byte[] b = new byte[chunk.remaining()]; chunk.get(b); return new String(b, StandardCharsets.UTF_8); - }).toList().get(); + }); + List results = StreamSupport.stream(output.toIterable().spliterator(), false).collect(toList()); assertEquals(2, results.size()); assertEquals("{\"foo\":\"foofoo\",\"bar\":\"barbar\"}", results.get(0)); assertEquals("{\"foo\":\"foofoofoo\",\"bar\":\"barbarbar\"}", results.get(1)); diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/Jaxb2EncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/Jaxb2EncoderTests.java index 2536b2c027..d2c4d62ca4 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/Jaxb2EncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/Jaxb2EncoderTests.java @@ -18,13 +18,14 @@ package org.springframework.reactive.codec.encoder; import java.nio.charset.StandardCharsets; import java.util.List; +import java.util.stream.StreamSupport; +import static java.util.stream.Collectors.toList; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import org.junit.Test; -import reactor.rx.Stream; -import reactor.rx.Streams; +import reactor.Flux; import org.springframework.core.codec.support.Jaxb2Encoder; import org.springframework.http.MediaType; @@ -46,12 +47,13 @@ public class Jaxb2EncoderTests { @Test public void encode() throws InterruptedException { - Stream source = Streams.just(new Pojo("foofoo", "barbar"), new Pojo("foofoofoo", "barbarbar")); - List results = Streams.from(encoder.encode(source, null, null)).map(chunk -> { - byte[] b = new byte[chunk.remaining()]; - chunk.get(b); - return new String(b, StandardCharsets.UTF_8); - }).toList().get(); + Flux source = Flux.just(new Pojo("foofoo", "barbar"), new Pojo("foofoofoo", "barbarbar")); + Flux output = encoder.encode(source, null, null).map(chunk -> { + byte[] b = new byte[chunk.remaining()]; + chunk.get(b); + return new String(b, StandardCharsets.UTF_8); + }); + List results = StreamSupport.stream(output.toIterable().spliterator(), false).collect(toList()); assertEquals(2, results.size()); assertEquals("barbarfoofoo", results.get(0)); assertEquals("barbarbarfoofoofoo", results.get(1)); diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/StringEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/StringEncoderTests.java index 3eef55cfe5..215c3d17ea 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/StringEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/StringEncoderTests.java @@ -16,14 +16,17 @@ package org.springframework.reactive.codec.encoder; +import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.List; +import java.util.stream.StreamSupport; +import static java.util.stream.Collectors.toList; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import org.junit.Test; -import reactor.rx.Streams; +import reactor.Flux; import org.springframework.core.ResolvableType; import org.springframework.core.codec.support.StringEncoder; @@ -45,12 +48,12 @@ public class StringEncoderTests { @Test public void write() throws InterruptedException { - List results = Streams.from(encoder.encode(Streams.just("foo"), null, null)) - .map(chunk -> { - byte[] b = new byte[chunk.remaining()]; - chunk.get(b); - return new String(b, StandardCharsets.UTF_8); - }).toList().get(); + Flux output = Flux.from(encoder.encode(Flux.just("foo"), null, null)).map(chunk -> { + byte[] b = new byte[chunk.remaining()]; + chunk.get(b); + return new String(b, StandardCharsets.UTF_8); + }); + List results = StreamSupport.stream(output.toIterable().spliterator(), false).collect(toList()); assertEquals(1, results.size()); assertEquals("foo", results.get(0)); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java index dfc0fb1a56..c25ea1bb7e 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java @@ -22,9 +22,9 @@ import java.util.HashMap; import java.util.Map; import org.junit.Test; +import reactor.Flux; import reactor.Mono; import reactor.io.buffer.Buffer; -import reactor.rx.Stream; import org.springframework.context.support.StaticApplicationContext; import org.springframework.http.HttpStatus; @@ -139,7 +139,7 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler @Override public Mono handle(WebServerExchange exchange) { - return exchange.getResponse().setBody(Stream.just(Buffer.wrap("foo").byteBuffer())); + return exchange.getResponse().setBody(Flux.just(Buffer.wrap("foo").byteBuffer())); } } @@ -147,7 +147,7 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler @Override public Mono handle(WebServerExchange exchange) { - return exchange.getResponse().setBody(Stream.just(Buffer.wrap("bar").byteBuffer())); + return exchange.getResponse().setBody(Flux.just(Buffer.wrap("bar").byteBuffer())); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java index 945c2b6ff2..1bc53dcdce 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java @@ -18,11 +18,12 @@ package org.springframework.web.reactive.method.annotation; import java.net.URI; import java.util.List; +import java.util.stream.StreamSupport; +import static java.util.stream.Collectors.toList; import org.junit.Before; import org.junit.Test; import org.reactivestreams.Publisher; -import reactor.rx.Stream; import org.springframework.context.support.StaticApplicationContext; import org.springframework.http.HttpMethod; @@ -38,6 +39,7 @@ import org.springframework.web.server.WebServerExchange; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import reactor.Flux; /** * @author Sebastien Deleuze @@ -83,7 +85,7 @@ public class RequestMappingHandlerMappingTests { private HandlerMethod toHandlerMethod(Publisher handlerPublisher) throws InterruptedException { assertNotNull(handlerPublisher); - List handlerList = Stream.from(handlerPublisher).toList().get(); + List handlerList = StreamSupport.stream(Flux.from(handlerPublisher).toIterable().spliterator(), false).collect(toList()); assertEquals(1, handlerList.size()); return (HandlerMethod) handlerList.get(0); } From cf6ceeed621531288579051495312f069ddc133d Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Tue, 15 Dec 2015 14:14:00 +0100 Subject: [PATCH 158/498] Allow Splitting/aggregation operations in Decoders When decoding buffers as plain strings, the StringDecoder returns a Publisher that may produce one or more `onNext` events. This is perfectly valid, but leads to errors when trying to convert the resulting Publisher into a `reactor.Mono` or `rx.Single`. If the original Publisher emits 2 or more `onNext` signals, converting to: * `rx.Single` will throw an error saying that the underlying Observable "emitted too many elements" * `reactor.Mono` may contain only the first emitted element This commit adds a `AbstractRawByteStreamDecoder` that takes a `SubscriberBarrier` to apply splitting/aggregation operations on the received elements. The `StringDecoder` class now inherits from this abstract class and uses one of the provided `SubscriberBarrier` implementations to buffer all received elements in a single buffer. --- .../support/AbstractRawByteStreamDecoder.java | 226 ++++++++++++++++++ .../core/codec/support/StringDecoder.java | 50 +++- .../codec/decoder/StringDecoderTests.java | 46 +++- 3 files changed, 308 insertions(+), 14 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractRawByteStreamDecoder.java diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractRawByteStreamDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractRawByteStreamDecoder.java new file mode 100644 index 0000000000..fa20ce0c19 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractRawByteStreamDecoder.java @@ -0,0 +1,226 @@ +/* + * 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.support; + +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import java.util.concurrent.atomic.AtomicLongFieldUpdater; + +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import reactor.Flux; +import reactor.core.subscriber.SubscriberBarrier; +import reactor.core.support.BackpressureUtils; + +import org.springframework.core.ResolvableType; +import org.springframework.core.codec.Decoder; +import org.springframework.util.MimeType; + +/** + * Abstract {@link Decoder} that plugs a {@link SubscriberBarrier} into the {@code Flux} + * pipeline in order to apply splitting/aggregation operations on the stream of data. + * + * @author Brian Clozel + */ +public abstract class AbstractRawByteStreamDecoder extends AbstractDecoder { + + public AbstractRawByteStreamDecoder(MimeType... supportedMimeTypes) { + super(supportedMimeTypes); + } + + @Override + public Flux decode(Publisher inputStream, ResolvableType type, MimeType mimeType, Object... hints) { + + return decodeInternal(Flux.from(inputStream).lift(bbs -> subscriberBarrier(bbs)), + type, mimeType, hints); + } + + /** + * Create a {@link SubscriberBarrier} instance that will be plugged into the Publisher pipeline + * + *

    Implementations should provide their own {@link SubscriberBarrier} or use one of the + * provided implementations by this class + */ + public abstract SubscriberBarrier subscriberBarrier(Subscriber subscriber); + + public abstract Flux decodeInternal(Publisher inputStream, ResolvableType type + , MimeType mimeType, Object... hints); + + + /** + * {@code SubscriberBarrier} implementation that buffers all received elements and emits a single + * {@code ByteBuffer} once the incoming stream has been completed + */ + public static class ReduceSingleByteStreamBarrier extends SubscriberBarrier { + + @SuppressWarnings("rawtypes") + static final AtomicLongFieldUpdater REQUESTED = + AtomicLongFieldUpdater.newUpdater(ReduceSingleByteStreamBarrier.class, "requested"); + + static final AtomicIntegerFieldUpdater TERMINATED = + AtomicIntegerFieldUpdater.newUpdater(ReduceSingleByteStreamBarrier.class, "terminated"); + + + private volatile long requested; + + private volatile int terminated; + + private ByteBuffer buffer; + + public ReduceSingleByteStreamBarrier(Subscriber subscriber) { + super(subscriber); + this.buffer = ByteBuffer.allocate(0); + } + + @Override + protected void doRequest(long n) { + BackpressureUtils.getAndAdd(REQUESTED, this, n); + if (TERMINATED.compareAndSet(this, 1, 2)) { + drainLast(); + } + else { + super.doRequest(Long.MAX_VALUE); + } + } + + @Override + protected void doComplete() { + if (TERMINATED.compareAndSet(this, 0, 1)) { + drainLast(); + } + } + + /* + * TODO: when available, wrap buffers with a single buffer and avoid copying data for every method call. + */ + @Override + protected void doNext(ByteBuffer byteBuffer) { + this.buffer = ByteBuffer.allocate(this.buffer.capacity() + byteBuffer.capacity()) + .put(this.buffer).put(byteBuffer); + this.buffer.flip(); + } + + protected void drainLast() { + if (BackpressureUtils.getAndSub(REQUESTED, this, 1L) > 0) { + this.buffer.flip(); + subscriber.onNext(this.buffer); + super.doComplete(); + } + } + } + + /** + * {@code SubscriberBarrier} implementation that splits incoming elements + * using line return delimiters: {@code "\n"} and {@code "\r\n"} + */ + public static class SplitLinesByteStreamBarrier extends SubscriberBarrier { + + @SuppressWarnings("rawtypes") + static final AtomicLongFieldUpdater REQUESTED = + AtomicLongFieldUpdater.newUpdater(SplitLinesByteStreamBarrier.class, "requested"); + + static final AtomicIntegerFieldUpdater TERMINATED = + AtomicIntegerFieldUpdater.newUpdater(SplitLinesByteStreamBarrier.class, "terminated"); + + + private volatile long requested; + + private volatile int terminated; + + private ByteBuffer buffer; + + public SplitLinesByteStreamBarrier(Subscriber subscriber) { + super(subscriber); + this.buffer = ByteBuffer.allocate(0); + } + + @Override + protected void doRequest(long n) { + BackpressureUtils.getAndAdd(REQUESTED, this, n); + if (TERMINATED.compareAndSet(this, 1, 2)) { + drainLast(); + } + else { + super.doRequest(n); + } + } + + @Override + protected void doComplete() { + if (TERMINATED.compareAndSet(this, 0, 1)) { + drainLast(); + } + } + + /* + * TODO: when available, wrap buffers with a single buffer and avoid copying data for every method call. + */ + @Override + protected void doNext(ByteBuffer byteBuffer) { + this.buffer = ByteBuffer.allocate(this.buffer.capacity() + byteBuffer.capacity()) + .put(this.buffer).put(byteBuffer); + + while (REQUESTED.get(this) > 0) { + int separatorIndex = findEndOfLine(this.buffer); + if (separatorIndex != -1) { + if (BackpressureUtils.getAndSub(REQUESTED, this, 1L) > 0) { + byte[] message = new byte[separatorIndex]; + this.buffer.get(message); + consumeSeparator(this.buffer); + this.buffer = this.buffer.slice(); + super.doNext(ByteBuffer.wrap(message)); + } + } + else { + super.doRequest(1); + } + } + } + + protected int findEndOfLine(ByteBuffer buffer) { + + final int n = buffer.limit(); + for (int i = 0; i < n; i++) { + final byte b = buffer.get(i); + if (b == '\n') { + return i; + } + else if (b == '\r' && i < n - 1 && buffer.get(i + 1) == '\n') { + return i; + } + } + + return -1; + } + + protected void consumeSeparator(ByteBuffer buffer) { + byte sep = buffer.get(); + if (sep == '\r') { + buffer.get(); + } + } + + protected void drainLast() { + if (BackpressureUtils.getAndSub(REQUESTED, this, 1L) > 0) { + this.buffer.flip(); + subscriber.onNext(this.buffer); + super.doComplete(); + } + } + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java index 5205a1e0d0..332af997ea 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -21,8 +21,9 @@ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; import reactor.Flux; -import reactor.io.buffer.Buffer; +import reactor.core.subscriber.SubscriberBarrier; import org.springframework.core.ResolvableType; import org.springframework.util.MimeType; @@ -30,15 +31,41 @@ import org.springframework.util.MimeType; /** * Decode from a bytes stream to a String stream. * + *

    By default, this decoder will buffer the received elements into a single + * {@code ByteBuffer} and will emit a single {@code String} once the stream of + * elements is complete. This behavior can be turned off using an constructor + * argument but the {@code Subcriber} should pay attention to split characters + * issues. + * * @author Sebastien Deleuze + * @author Brian Clozel * @see StringEncoder */ -public class StringDecoder extends AbstractDecoder { +public class StringDecoder extends AbstractRawByteStreamDecoder { public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; + public final boolean reduceToSingleBuffer; + + /** + * Create a {@code StringDecoder} that decodes a bytes stream to a String stream + * + *

    By default, this decoder will buffer bytes and + * emit a single String as a result. + */ public StringDecoder() { + this(true); + } + + /** + * Create a {@code StringDecoder} that decodes a bytes stream to a String stream + * + * @param reduceToSingleBuffer whether this decoder should buffer all received items + * and decode a single consolidated String or re-emit items as they are provided + */ + public StringDecoder(boolean reduceToSingleBuffer) { super(new MimeType("text", "plain", DEFAULT_CHARSET)); + this.reduceToSingleBuffer = reduceToSingleBuffer; } @Override @@ -48,17 +75,26 @@ public class StringDecoder extends AbstractDecoder { } @Override - public Flux decode(Publisher inputStream, ResolvableType type, - MimeType mimeType, Object... hints) { + public SubscriberBarrier subscriberBarrier(Subscriber subscriber) { + if (reduceToSingleBuffer) { + return new ReduceSingleByteStreamBarrier(subscriber); + } + else { + return new SubscriberBarrier(subscriber); + } + } + + @Override + public Flux decodeInternal(Publisher inputStream, ResolvableType type, MimeType mimeType, Object... hints) { Charset charset; if (mimeType != null && mimeType.getCharSet() != null) { charset = mimeType.getCharSet(); } else { - charset = DEFAULT_CHARSET; + charset = DEFAULT_CHARSET; } - return Flux.from(inputStream).map(content -> new String(new Buffer(content).asBytes(), charset)); + return Flux.from(inputStream).map(content -> new String(content.duplicate().array(), charset)); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/StringDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/StringDecoderTests.java index 038d26e2b1..0d693ec4af 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/StringDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/StringDecoderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -16,18 +16,19 @@ package org.springframework.reactive.codec.decoder; +import static java.util.stream.Collectors.*; +import static org.junit.Assert.*; + import java.nio.ByteBuffer; import java.util.List; import java.util.stream.StreamSupport; -import static java.util.stream.Collectors.toList; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; import org.junit.Test; -import org.reactivestreams.Publisher; import reactor.Flux; +import reactor.Mono; +import reactor.core.publisher.convert.RxJava1SingleConverter; import reactor.io.buffer.Buffer; +import rx.Single; import org.springframework.core.ResolvableType; import org.springframework.core.codec.support.StringDecoder; @@ -35,6 +36,7 @@ import org.springframework.http.MediaType; /** * @author Sebastien Deleuze + * @author Brian Clozel */ public class StringDecoderTests { @@ -50,11 +52,41 @@ public class StringDecoderTests { @Test public void decode() throws InterruptedException { Flux source = Flux.just(Buffer.wrap("foo").byteBuffer(), Buffer.wrap("bar").byteBuffer()); - Flux output = decoder.decode(source, ResolvableType.forClassWithGenerics(Publisher.class, String.class), null); + Flux output = this.decoder.decode(source, ResolvableType.forClassWithGenerics(Flux.class, String.class), null); + List results = StreamSupport.stream(output.toIterable().spliterator(), false).collect(toList()); + assertEquals(1, results.size()); + assertEquals("foobar", results.get(0)); + } + + @Test + public void decodeDoNotBuffer() throws InterruptedException { + StringDecoder decoder = new StringDecoder(false); + Flux source = Flux.just(Buffer.wrap("foo").byteBuffer(), Buffer.wrap("bar").byteBuffer()); + Flux output = decoder.decode(source, ResolvableType.forClassWithGenerics(Flux.class, String.class), null); List results = StreamSupport.stream(output.toIterable().spliterator(), false).collect(toList()); assertEquals(2, results.size()); assertEquals("foo", results.get(0)); assertEquals("bar", results.get(1)); } + @Test + public void decodeMono() throws InterruptedException { + Flux source = Flux.just(Buffer.wrap("foo").byteBuffer(), Buffer.wrap("bar").byteBuffer()); + Mono mono = Mono.from(this.decoder.decode(source, + ResolvableType.forClassWithGenerics(Mono.class, String.class), + MediaType.TEXT_PLAIN)); + String result = mono.get(); + assertEquals("foobar", result); + } + + @Test + public void decodeSingle() throws InterruptedException { + Flux source = Flux.just(Buffer.wrap("foo").byteBuffer(), Buffer.wrap("bar").byteBuffer()); + Single single = RxJava1SingleConverter.from(this.decoder.decode(source, + ResolvableType.forClassWithGenerics(Single.class, String.class), + MediaType.TEXT_PLAIN)); + String result = single.toBlocking().value(); + assertEquals("foobar", result); + } + } From 437c127b627aa6ab12c3fd91ab70ee2d1d8c9f54 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 13 Jan 2016 18:00:13 -0500 Subject: [PATCH 159/498] Log and handle unresolved exceptions Before this change use of ExceptionHandlingWebHandler did ensure no error signals are allowed to escape (hence relying on runtime behavior). This change ensures the same is done even when ExceptionHandlingWebHandler is not configured for use, at the lowest level which is the WebToHttpHandlerAdapter. --- .../handler/SimpleHandlerResultHandler.java | 4 ++-- .../server/ExceptionHandlingWebHandler.java | 13 +++++++++++-- .../web/server/WebToHttpHandlerAdapter.java | 18 +++++++++++++++++- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java index 6c8494ff1d..7c7c4126cd 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java @@ -62,8 +62,8 @@ public class SimpleHandlerResultHandler implements Ordered, HandlerResultHandler @Override public boolean supports(HandlerResult result) { ResolvableType type = result.getResultType(); - return type != null && Void.TYPE.equals(type.getRawClass()) || - (Void.class.isAssignableFrom(type.getGeneric(0).getRawClass()) && isConvertibleToPublisher(type)); + return (type != null && Void.TYPE.equals(type.getRawClass()) || + (isConvertibleToPublisher(type) && Void.class.isAssignableFrom(type.getGeneric(0).getRawClass()))); } private boolean isConvertibleToPublisher(ResolvableType type) { diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/ExceptionHandlingWebHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/server/ExceptionHandlingWebHandler.java index 2842bc801b..1c6ba76a65 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/ExceptionHandlingWebHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/ExceptionHandlingWebHandler.java @@ -19,6 +19,8 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import reactor.Mono; import org.springframework.http.HttpStatus; @@ -31,6 +33,9 @@ import org.springframework.http.HttpStatus; */ public class ExceptionHandlingWebHandler extends WebHandlerDecorator { + private static Log logger = LogFactory.getLog(ExceptionHandlingWebHandler.class); + + private final List exceptionHandlers; @@ -51,6 +56,7 @@ public class ExceptionHandlingWebHandler extends WebHandlerDecorator { return this.exceptionHandlers; } + @Override public Mono handle(WebServerExchange exchange) { Mono mono; @@ -63,10 +69,13 @@ public class ExceptionHandlingWebHandler extends WebHandlerDecorator { for (WebExceptionHandler exceptionHandler : this.exceptionHandlers) { mono = mono.otherwise(ex -> exceptionHandler.handle(exchange, ex)); } - return mono.otherwise(ex -> handleUnresolvedException(exchange)); + return mono.otherwise(ex -> handleUnresolvedException(exchange, ex)); } - private Mono handleUnresolvedException(WebServerExchange exchange) { + private Mono handleUnresolvedException(WebServerExchange exchange, Throwable ex) { + if (logger.isDebugEnabled()) { + logger.debug("Could not complete request", ex); + } exchange.getResponse().setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR); return Mono.empty(); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/WebToHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/server/WebToHttpHandlerAdapter.java index aace5d7885..ec8b418a40 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/WebToHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/WebToHttpHandlerAdapter.java @@ -15,8 +15,11 @@ */ package org.springframework.web.server; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import reactor.Mono; +import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.HttpHandler; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; @@ -29,15 +32,28 @@ import org.springframework.http.server.reactive.ServerHttpResponse; */ public class WebToHttpHandlerAdapter extends WebHandlerDecorator implements HttpHandler { + private static Log logger = LogFactory.getLog(WebToHttpHandlerAdapter.class); + public WebToHttpHandlerAdapter(WebHandler delegate) { super(delegate); } + @Override public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { WebServerExchange exchange = createWebServerExchange(request, response); - return getDelegate().handle(exchange).doOnTerminate((aVoid, ex) -> response.writeHeaders()); + return getDelegate().handle(exchange).otherwise(ex -> { + if (logger.isDebugEnabled()) { + logger.debug("Could not complete request", ex); + } + response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR); + return Mono.empty(); + }) + .doOnTerminate((aVoid, ex) -> { + response.writeHeaders(); + }); + } protected WebServerExchange createWebServerExchange(ServerHttpRequest request, ServerHttpResponse response) { From da90f03f4c7e192a23aaf64680cccf1c51c2b0f5 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 13 Jan 2016 18:12:36 -0500 Subject: [PATCH 160/498] Support type-level @ResponeBody (and @RestController) --- .../method/annotation/ResponseBodyResultHandler.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java index 0d9422266f..8018783b35 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java @@ -30,9 +30,11 @@ import java.util.stream.Collectors; import org.reactivestreams.Publisher; import reactor.Mono; +import org.springframework.core.MethodParameter; import org.springframework.core.Ordered; import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.codec.Encoder; import org.springframework.core.convert.ConversionService; import org.springframework.http.MediaType; @@ -120,8 +122,10 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered public boolean supports(HandlerResult result) { Object handler = result.getHandler(); if (handler instanceof HandlerMethod) { - Method method = ((HandlerMethod) handler).getMethod(); - return AnnotatedElementUtils.isAnnotated(method, ResponseBody.class.getName()); + MethodParameter returnType = ((HandlerMethod) handler).getReturnType(); + Class containingClass = returnType.getContainingClass(); + return (AnnotationUtils.findAnnotation(containingClass, ResponseBody.class) != null || + returnType.getMethodAnnotation(ResponseBody.class) != null); } return false; } From 0f432f735a72c3f26ba93374e3c9686703c5c4fb Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Thu, 14 Jan 2016 03:34:25 +0000 Subject: [PATCH 161/498] use reactor cookie support --- .../reactive/ReactorServerHttpRequest.java | 13 ++++- .../reactive/ReactorServerHttpResponse.java | 54 ++++++++++++++++++- .../reactive/CookieIntegrationTests.java | 3 +- 3 files changed, 67 insertions(+), 3 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java index 9cf3260bb8..2ea2c9fcf9 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java @@ -18,12 +18,14 @@ package org.springframework.http.server.reactive; import java.net.URI; import java.net.URISyntaxException; import java.nio.ByteBuffer; +import java.util.ArrayList; import java.util.List; import java.util.Map; import reactor.Flux; import reactor.io.buffer.Buffer; import reactor.io.net.http.HttpChannel; +import reactor.io.net.http.model.Cookie; import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; @@ -69,7 +71,16 @@ public class ReactorServerHttpRequest extends AbstractServerHttpRequest { @Override protected void initCookies(Map> cookies) { - // https://github.com/reactor/reactor/issues/614 + for (String name : this.channel.cookies().keySet()) { + List list = cookies.get(name); + if (list == null) { + list = new ArrayList<>(); + cookies.put(name, list); + } + for (Cookie cookie : this.channel.cookies().get(name)) { + list.add(HttpCookie.clientCookie(name, cookie.value())); + } + } } @Override diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java index b7cbed3b8c..4e93607c5f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java @@ -22,8 +22,10 @@ import reactor.Flux; import reactor.Mono; import reactor.io.buffer.Buffer; import reactor.io.net.http.HttpChannel; +import reactor.io.net.http.model.Cookie; import reactor.io.net.http.model.Status; +import org.springframework.http.HttpCookie; import org.springframework.http.HttpStatus; import org.springframework.util.Assert; @@ -69,7 +71,57 @@ public class ReactorServerHttpResponse extends AbstractServerHttpResponse { @Override protected void writeCookies() { - // https://github.com/reactor/reactor/issues/614 + for (String name : getHeaders().getCookies().keySet()) { + for (HttpCookie httpCookie : getHeaders().getCookies().get(name)) { + Cookie cookie = new ReactorCookie(name, httpCookie); + this.channel.addResponseCookie(name, cookie); + } + } } + private final static class ReactorCookie extends Cookie { + + final HttpCookie httpCookie; + final String name; + + public ReactorCookie(String name, HttpCookie httpCookie) { + this.name = name; + this.httpCookie = httpCookie; + } + + @Override + public String name() { + return name; + } + + @Override + public String value() { + return httpCookie.getValue(); + } + + @Override + public boolean httpOnly() { + return httpCookie.isHttpOnly(); + } + + @Override + public long maxAge() { + return httpCookie.getMaxAge() > -1 ? httpCookie.getMaxAge() : -1; + } + + @Override + public String domain() { + return httpCookie.getDomain(); + } + + @Override + public String path() { + return httpCookie.getPath(); + } + + @Override + public boolean secure() { + return httpCookie.isSecure(); + } + } } diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/CookieIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/CookieIntegrationTests.java index dbb866219e..05a3b4c043 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/CookieIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/CookieIntegrationTests.java @@ -32,6 +32,7 @@ import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; import org.springframework.http.server.reactive.boot.HttpServer; import org.springframework.http.server.reactive.boot.JettyHttpServer; +import org.springframework.http.server.reactive.boot.ReactorHttpServer; import org.springframework.http.server.reactive.boot.RxNettyHttpServer; import org.springframework.http.server.reactive.boot.TomcatHttpServer; import org.springframework.http.server.reactive.boot.UndertowHttpServer; @@ -66,7 +67,7 @@ public class CookieIntegrationTests { return new Object[][] { {new JettyHttpServer()}, {new RxNettyHttpServer()}, -// {new ReactorHttpServer()}, + {new ReactorHttpServer()}, {new TomcatHttpServer()}, {new UndertowHttpServer()} }; From 1e28dee6085fe9b086edea989fd70bf445134993 Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Thu, 14 Jan 2016 03:59:15 +0000 Subject: [PATCH 162/498] add streaming test, commented due to issue with a tomcat and jetty --- .../RequestMappingIntegrationTests.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java index 1de455fe7c..28cdf6a313 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java @@ -23,6 +23,7 @@ import java.util.Arrays; import java.util.List; import java.util.concurrent.CompletableFuture; +import org.junit.Ignore; import org.junit.Test; import org.reactivestreams.Publisher; import reactor.Flux; @@ -64,6 +65,7 @@ import org.springframework.web.reactive.DispatcherHandler; import org.springframework.web.reactive.handler.SimpleHandlerResultHandler; import org.springframework.web.server.WebToHttpHandlerBuilder; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; /** @@ -160,6 +162,19 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati assertEquals("Recovered from error: Boo", response.getBody()); } + @Test + @Ignore + //FIXME Fail with Jetty and Tomcat + public void streamResult() throws Exception { + RestTemplate restTemplate = new RestTemplate(); + + URI url = new URI("http://localhost:" + port + "/stream-result"); + RequestEntity request = RequestEntity.get(url).build(); + ResponseEntity response = restTemplate.exchange(request, String[].class); + + assertArrayEquals(new String[]{"0", "1", "2", "3", "4"}, response.getBody()); + } + @Test public void serializeAsPojo() throws Exception { serializeAsPojo("http://localhost:" + port + "/person"); @@ -421,6 +436,12 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati ResolvableType.forClass(Person.class), MediaType.APPLICATION_JSON); } + @RequestMapping("/stream-result") + @ResponseBody + public Publisher stringStreamResponseBody() { + return Flux.interval(1).map(Object::toString).as(Stream::from).take(5); + } + @RequestMapping("/raw-flux") @ResponseBody public Flux rawFluxResponseBody() { From 72e5aa39ec6c2f5d773a1ff1a67cb00fb2b8277a Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Thu, 14 Jan 2016 11:04:27 +0100 Subject: [PATCH 163/498] Polish @RestController support --- .../annotation/ResponseBodyResultHandler.java | 3 ++ .../RequestMappingIntegrationTests.java | 32 ++----------------- 2 files changed, 5 insertions(+), 30 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java index 8018783b35..1ebc9c23a5 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java @@ -145,6 +145,9 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered if (this.conversionService.canConvert(returnType.getRawClass(), Publisher.class)) { publisher = this.conversionService.convert(value, Publisher.class); elementType = returnType.getGeneric(0); + if (Void.class.equals(elementType.getRawClass())) { + return (Mono)Mono.from(publisher); + } } else { publisher = Mono.just(value); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java index 28cdf6a313..27e3ccb099 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java @@ -54,12 +54,11 @@ import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; import org.springframework.http.server.reactive.AbstractHttpHandlerIntegrationTests; import org.springframework.http.server.reactive.HttpHandler; -import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; import org.springframework.web.reactive.DispatcherHandler; import org.springframework.web.reactive.handler.SimpleHandlerResultHandler; @@ -404,32 +403,28 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati } - @Controller + @RestController @SuppressWarnings("unused") private static class TestController { final List persons = new ArrayList<>(); @RequestMapping("/param") - @ResponseBody public Publisher handleWithParam(@RequestParam String name) { return Stream.just("Hello ", name, "!"); } @RequestMapping("/person") - @ResponseBody public Person personResponseBody() { return new Person("Robert"); } @RequestMapping("/completable-future") - @ResponseBody public CompletableFuture completableFutureResponseBody() { return CompletableFuture.completedFuture(new Person("Robert")); } @RequestMapping("/raw") - @ResponseBody public Publisher rawResponseBody() { JacksonJsonEncoder encoder = new JacksonJsonEncoder(); return encoder.encode(Stream.just(new Person("Robert")), @@ -437,73 +432,61 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati } @RequestMapping("/stream-result") - @ResponseBody public Publisher stringStreamResponseBody() { return Flux.interval(1).map(Object::toString).as(Stream::from).take(5); } @RequestMapping("/raw-flux") - @ResponseBody public Flux rawFluxResponseBody() { return Flux.just(Buffer.wrap("Hello!").byteBuffer()); } @RequestMapping("/raw-observable") - @ResponseBody public Observable rawObservableResponseBody() { return Observable.just(Buffer.wrap("Hello!").byteBuffer()); } @RequestMapping("/mono") - @ResponseBody public Mono monoResponseBody() { return Mono.just(new Person("Robert")); } @RequestMapping("/single") - @ResponseBody public Single singleResponseBody() { return Single.just(new Person("Robert")); } @RequestMapping("/promise") - @ResponseBody public Promise promiseResponseBody() { return Promise.success(new Person("Robert")); } @RequestMapping("/list") - @ResponseBody public List listResponseBody() { return Arrays.asList(new Person("Robert"), new Person("Marie")); } @RequestMapping("/publisher") - @ResponseBody public Publisher publisherResponseBody() { return Stream.just(new Person("Robert"), new Person("Marie")); } @RequestMapping("/flux") - @ResponseBody public Flux fluxResponseBody() { return Flux.just(new Person("Robert"), new Person("Marie")); } @RequestMapping("/observable") - @ResponseBody public Observable observableResponseBody() { return Observable.just(new Person("Robert"), new Person("Marie")); } @RequestMapping("/stream") - @ResponseBody public Stream reactorStreamResponseBody() { return Stream.just(new Person("Robert"), new Person("Marie")); } @RequestMapping("/publisher-capitalize") - @ResponseBody public Publisher publisherCapitalize(@RequestBody Publisher persons) { return Stream.from(persons).map(person -> { person.setName(person.getName().toUpperCase()); @@ -512,7 +495,6 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati } @RequestMapping("/flux-capitalize") - @ResponseBody public Flux fluxCapitalize(@RequestBody Flux persons) { return persons.map(person -> { person.setName(person.getName().toUpperCase()); @@ -521,7 +503,6 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati } @RequestMapping("/observable-capitalize") - @ResponseBody public Observable observableCapitalize(@RequestBody Observable persons) { return persons.map(person -> { person.setName(person.getName().toUpperCase()); @@ -530,7 +511,6 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati } @RequestMapping("/stream-capitalize") - @ResponseBody public Stream streamCapitalize(@RequestBody Stream persons) { return persons.map(person -> { person.setName(person.getName().toUpperCase()); @@ -539,14 +519,12 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati } @RequestMapping("/person-capitalize") - @ResponseBody public Person personCapitalize(@RequestBody Person person) { person.setName(person.getName().toUpperCase()); return person; } @RequestMapping("/completable-future-capitalize") - @ResponseBody public CompletableFuture completableFutureCapitalize( @RequestBody CompletableFuture personFuture) { @@ -557,7 +535,6 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati } @RequestMapping("/mono-capitalize") - @ResponseBody public Mono monoCapitalize(@RequestBody Mono personFuture) { return personFuture.map(person -> { person.setName(person.getName().toUpperCase()); @@ -566,7 +543,6 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati } @RequestMapping("/single-capitalize") - @ResponseBody public Single singleCapitalize(@RequestBody Single personFuture) { return personFuture.map(person -> { person.setName(person.getName().toUpperCase()); @@ -575,7 +551,6 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati } @RequestMapping("/promise-capitalize") - @ResponseBody public Promise promiseCapitalize(@RequestBody Promise personFuture) { return Stream.from(personFuture.map(person -> { person.setName(person.getName().toUpperCase()); @@ -604,19 +579,16 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati } @RequestMapping("/thrown-exception") - @ResponseBody public Publisher handleAndThrowException() { throw new IllegalStateException("Boo"); } @RequestMapping("/error-signal") - @ResponseBody public Publisher handleWithError() { return Mono.error(new IllegalStateException("Boo")); } @ExceptionHandler - @ResponseBody public Publisher handleException(IllegalStateException ex) { return Mono.just("Recovered from error: " + ex.getMessage()); } From 89da87a3e5f4a82bb06185d009408bbc27a69a7d Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Thu, 14 Jan 2016 12:35:56 +0000 Subject: [PATCH 164/498] Async test case to explore jetty/tomcat issues --- .../reactive/AsyncIntegrationTests.java | 117 ++++++++++++++++++ .../reactive/CookieIntegrationTests.java | 3 - .../RequestMappingIntegrationTests.java | 4 +- 3 files changed, 119 insertions(+), 5 deletions(-) create mode 100644 spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java new file mode 100644 index 0000000000..66140914de --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2011-2016 Pivotal Software Inc, All Rights Reserved. + * + * 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.server.reactive; + +import java.net.URI; + +import org.hamcrest.Matchers; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import reactor.Mono; +import reactor.Processors; +import reactor.Timers; +import reactor.core.processor.ProcessorGroup; +import reactor.io.buffer.Buffer; +import reactor.rx.Stream; + +import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.http.server.reactive.boot.HttpServer; +import org.springframework.http.server.reactive.boot.ReactorHttpServer; +import org.springframework.http.server.reactive.boot.RxNettyHttpServer; +import org.springframework.http.server.reactive.boot.UndertowHttpServer; +import org.springframework.util.SocketUtils; +import org.springframework.web.client.RestTemplate; + +import static org.junit.Assert.assertThat; + +/** + * Temporarily does not extend AbstractHttpHandlerIntegrationTests. + * + * @author Stephane Maldini + */ +@RunWith(Parameterized.class) +public class AsyncIntegrationTests { + + private final ProcessorGroup asyncGroup = Processors.asyncGroup(); + + protected int port; + + @Parameterized.Parameter(0) + public HttpServer server; + + private AsyncHandler asyncHandler; + + @Parameterized.Parameters(name = "server [{0}]") + public static Object[][] arguments() { + return new Object[][]{ + //{new JettyHttpServer()}, + {new RxNettyHttpServer()}, + {new ReactorHttpServer()}, + //{new TomcatHttpServer()}, + {new UndertowHttpServer()} + }; + } + + @Before + public void setup() throws Exception { + this.port = SocketUtils.findAvailableTcpPort(); + this.server.setPort(this.port); + this.server.setHandler(createHttpHandler()); + this.server.afterPropertiesSet(); + this.server.start(); + } + + protected HttpHandler createHttpHandler() { + this.asyncHandler = new AsyncHandler(); + return this.asyncHandler; + } + + @After + public void tearDown() throws Exception { + this.server.stop(); + } + + @SuppressWarnings("unchecked") + @Test + public void basicTest() throws Exception { + URI url = new URI("http://localhost:" + port); + ResponseEntity response = new RestTemplate().exchange(RequestEntity.get(url) + .build(), String.class); + + assertThat(response.getBody(), Matchers.equalTo("hello")); + } + + private class AsyncHandler implements HttpHandler { + + @Override + public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { + return response.setBody(Stream.just("h", "e", "l", "l", "o") + .timer(Timers.global()) + .throttleRequest(100) + .dispatchOn(asyncGroup) + .collect(Buffer::new, Buffer::append) + .doOnSuccess(Buffer::flip) + .map(Buffer::byteBuffer) + ); + } + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/CookieIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/CookieIntegrationTests.java index 05a3b4c043..78dc08bd13 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/CookieIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/CookieIntegrationTests.java @@ -46,9 +46,6 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; /** - * Temporarily does not extend AbstractHttpHandlerIntegrationTests in order to - * exclude Reactor Net due to https://github.com/reactor/reactor/issues/614. - * * @author Rossen Stoyanchev */ @RunWith(Parameterized.class) diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java index 27e3ccb099..a10c0f23f3 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java @@ -432,8 +432,8 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati } @RequestMapping("/stream-result") - public Publisher stringStreamResponseBody() { - return Flux.interval(1).map(Object::toString).as(Stream::from).take(5); + public Publisher stringStreamResponseBody() { + return Flux.interval(1).as(Stream::from).take(5); } @RequestMapping("/raw-flux") From 22bea1da0129b6cb065b0402cae4e27ab4dd3297 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Thu, 14 Jan 2016 15:37:30 +0100 Subject: [PATCH 165/498] Add sources and Javadoc to the build --- spring-web-reactive/build.gradle | 24 +++++++++++++++++++ .../core/codec/support/JsonObjectDecoder.java | 3 +-- .../web/reactive/DispatcherHandler.java | 6 ++--- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index 117cab0e2a..dab2f0a712 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -37,6 +37,27 @@ ext { jettyVersion = '9.3.5.v20151012' } +javadoc { + options.author = true + options.addStringOption('Xdoclint:none', '-quiet') + options.links 'http://docs.oracle.com/javase/8/docs/api/' +} + +task sourcesJar(type: Jar, dependsOn: classes) { + classifier = 'sources' + from sourceSets.main.allSource +} + +task javadocJar(type: Jar, dependsOn: javadoc) { + classifier = 'javadoc' + from javadoc.destinationDir +} + +artifacts { + archives sourcesJar + archives javadocJar +} + dependencies { compile "org.springframework:spring-core:${springVersion}" compile "org.springframework:spring-web:${springVersion}" @@ -66,6 +87,9 @@ dependencies { exclude group: 'org.hamcrest', module: 'hamcrest-core' } testCompile("org.hamcrest:hamcrest-all:1.3") + + // Needed to run Javadoc without error + optional("org.apache.httpcomponents:httpclient:4.5.1") } diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectDecoder.java index 59598d519e..07ec097cd7 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectDecoder.java @@ -39,8 +39,7 @@ import org.springframework.util.MimeType; * is considered a JSON object/array if it contains a matching number of opening * and closing braces/brackets. * - * Based on - * Netty {@code JsonObjectDecoder} + * Based on Netty JsonObjectDecoder * * @author Sebastien Deleuze * @see JsonObjectEncoder diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java index cb99099a71..3c4484a5ba 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java @@ -39,14 +39,14 @@ import org.springframework.web.server.WebServerExchange; * Central dispatcher for HTTP request handlers/controllers. Dispatches to registered * handlers for processing a web request, providing convenient mapping facilities. * - *

  • It can use any {@link HandlerMapping} implementation to control the routing of + *

    It can use any {@link HandlerMapping} implementation to control the routing of * requests to handler objects. HandlerMapping objects can be defined as beans in * the application context. * - *

  • It can use any {@link HandlerAdapter}; this allows for using any handler interface. + *

    It can use any {@link HandlerAdapter}; this allows for using any handler interface. * HandlerAdapter objects can be added as beans in the application context. * - *

  • It can use any {@link HandlerResultHandler}; this allows to process the result of + *

    It can use any {@link HandlerResultHandler}; this allows to process the result of * the request handling. HandlerResultHandler objects can be added as beans in the * application context. * From 14cb20a9f18dacee0c253ad47e960e9e5e43c4b3 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Thu, 14 Jan 2016 16:12:27 +0100 Subject: [PATCH 166/498] Disable uniqueVersion in order to get a reliable Javadoc link --- spring-web-reactive/README.md | 3 +++ spring-web-reactive/build.gradle | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/spring-web-reactive/README.md b/spring-web-reactive/README.md index 4bddcd39ce..a3aa139d1e 100644 --- a/spring-web-reactive/README.md +++ b/spring-web-reactive/README.md @@ -9,6 +9,9 @@ Spring Reactive JAR dependency is available from Spring snapshot repository: - ArtifactId: `spring-reactive` - Version: `0.1.0.BUILD-SNAPSHOT` +## Documentation +See the current [Javadoc][] and [reference docs][]. + ## Sample application [Spring Reactive Playground] is a sample application based on Spring Reactive and on MongoDB, Couchbase and PostgreSQL Reactive database drivers. diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index dab2f0a712..f46943d550 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -37,6 +37,14 @@ ext { jettyVersion = '9.3.5.v20151012' } +uploadArchives { + repositories { + mavenDeployer { + uniqueVersion = false + } + } +} + javadoc { options.author = true options.addStringOption('Xdoclint:none', '-quiet') From 2cdce62c3d9a1a5aec12676343e03e694a8a6e29 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Thu, 14 Jan 2016 16:15:34 +0100 Subject: [PATCH 167/498] Update README with the Javadoc link --- spring-web-reactive/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spring-web-reactive/README.md b/spring-web-reactive/README.md index a3aa139d1e..178cff2327 100644 --- a/spring-web-reactive/README.md +++ b/spring-web-reactive/README.md @@ -10,7 +10,7 @@ Spring Reactive JAR dependency is available from Spring snapshot repository: - Version: `0.1.0.BUILD-SNAPSHOT` ## Documentation -See the current [Javadoc][] and [reference docs][]. +See the current [Javadoc][]. ## Sample application [Spring Reactive Playground] is a sample application based on Spring Reactive and on MongoDB, @@ -53,3 +53,4 @@ The Spring Reactive is released under version 2.0 of the [Apache License][]. [issue tracker]: https://github.com/spring-projects/spring-reactive/issues [Pull requests]: http://help.github.com/send-pull-requests [Apache License]: http://www.apache.org/licenses/LICENSE-2.0 +[Javadoc]: https://repo.spring.io/snapshot/org/springframework/reactive/spring-reactive/0.1.0.BUILD-SNAPSHOT/spring-reactive-0.1.0.BUILD-SNAPSHOT-javadoc.jar!/index.html From ae6912086aa4177b13615339cb9050558fd8ce47 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Thu, 14 Jan 2016 18:42:51 +0100 Subject: [PATCH 168/498] Add packages documentation --- .../core/codec/package-info.java | 21 +++++++++++++++++++ .../core/codec/support/package-info.java | 20 ++++++++++++++++++ .../core/convert/support/package-info.java | 20 ++++++++++++++++++ .../http/client/package-info.java | 21 +++++++++++++++++++ .../http/server/reactive/package-info.java | 20 ++++++++++++++++++ .../springframework/util/package-info.java | 20 ++++++++++++++++++ .../org/springframework/web/package-info.java | 21 +++++++++++++++++++ .../web/reactive/handler/package-info.java | 20 ++++++++++++++++++ .../method/annotation/package-info.java | 20 ++++++++++++++++++ .../web/reactive/method/package-info.java | 20 ++++++++++++++++++ .../web/reactive/package-info.java | 20 ++++++++++++++++++ .../web/server/package-info.java | 20 ++++++++++++++++++ 12 files changed, 243 insertions(+) create mode 100644 spring-web-reactive/src/main/java/org/springframework/core/codec/package-info.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/core/codec/support/package-info.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/core/convert/support/package-info.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/http/client/package-info.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/http/server/reactive/package-info.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/util/package-info.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/package-info.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/package-info.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/package-info.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/method/package-info.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/package-info.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/server/package-info.java diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/package-info.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/package-info.java new file mode 100644 index 0000000000..c71c068caa --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/package-info.java @@ -0,0 +1,21 @@ +/* + * 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. + */ + +/** + * Root-package for codecs used to transform streams of bytes. + * Used for example for serialization/deserialization. + */ +package org.springframework.core.codec; diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/package-info.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/package-info.java new file mode 100644 index 0000000000..634952baf0 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * This package contains codec implementations. + */ +package org.springframework.core.codec.support; diff --git a/spring-web-reactive/src/main/java/org/springframework/core/convert/support/package-info.java b/spring-web-reactive/src/main/java/org/springframework/core/convert/support/package-info.java new file mode 100644 index 0000000000..f81a625d7c --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/core/convert/support/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * Default implementation of the type conversion system. + */ +package org.springframework.core.convert.support; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/package-info.java b/spring-web-reactive/src/main/java/org/springframework/http/client/package-info.java new file mode 100644 index 0000000000..d913027725 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/package-info.java @@ -0,0 +1,21 @@ +/* + * 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. + */ + +/** + * Core package of the client-side web support. + * Provides a RestTemplate class and various callback interfaces. + */ +package org.springframework.http.client; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/package-info.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/package-info.java new file mode 100644 index 0000000000..d9c4967c6a --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * Core package of the reactive server-side HTTP support. + */ +package org.springframework.http.server.reactive; diff --git a/spring-web-reactive/src/main/java/org/springframework/util/package-info.java b/spring-web-reactive/src/main/java/org/springframework/util/package-info.java new file mode 100644 index 0000000000..02cc0f8c2a --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/util/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * Miscellaneous utility classes, such as InputStream/OutputStream manipulation utilities. + */ +package org.springframework.util; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/package-info.java b/spring-web-reactive/src/main/java/org/springframework/web/package-info.java new file mode 100644 index 0000000000..c3e2e97b72 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/package-info.java @@ -0,0 +1,21 @@ +/* + * 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. + */ + +/** + * Common, generic interfaces that define minimal boundary points + * between Spring's web infrastructure and other framework modules. + */ +package org.springframework.web; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/package-info.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/package-info.java new file mode 100644 index 0000000000..9e6c1eba58 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * Provides reactive HandlerMapping implementations. + */ +package org.springframework.web.reactive.handler; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/package-info.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/package-info.java new file mode 100644 index 0000000000..cd65a7ee26 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * Reactive infrastructure for annotation-based handler method processing. + */ +package org.springframework.web.reactive.method.annotation; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/package-info.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/package-info.java new file mode 100644 index 0000000000..96a7727322 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * Reactive infrastructure for handler method processing. + */ +package org.springframework.web.reactive.method; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/package-info.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/package-info.java new file mode 100644 index 0000000000..9ec7eabc9a --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * Provides the core interfaces and classes for the Spring web reactive framework. + */ +package org.springframework.web.reactive; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/package-info.java b/spring-web-reactive/src/main/java/org/springframework/web/server/package-info.java new file mode 100644 index 0000000000..5d344fadcd --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * Web server infrastructure like filter, exchange (request + response), etc. + */ +package org.springframework.web.server; From 398eb0b4787255210b09f406c880c01cae589093 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Thu, 14 Jan 2016 18:54:17 +0100 Subject: [PATCH 169/498] Polish packages documentation --- .../http/client/package-info.java | 2 +- .../springframework/http/package-info.java | 21 +++++++++++++++++++ .../springframework/util/package-info.java | 2 +- 3 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/http/package-info.java diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/package-info.java b/spring-web-reactive/src/main/java/org/springframework/http/client/package-info.java index d913027725..f3c20a57a7 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/package-info.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/package-info.java @@ -16,6 +16,6 @@ /** * Core package of the client-side web support. - * Provides a RestTemplate class and various callback interfaces. + * Provides a {@code RestTemplate} class and various callback interfaces. */ package org.springframework.http.client; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/package-info.java b/spring-web-reactive/src/main/java/org/springframework/http/package-info.java new file mode 100644 index 0000000000..798fc50e6e --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/http/package-info.java @@ -0,0 +1,21 @@ +/* + * 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. + */ + +/** + * Contains a basic abstraction over client/server-side HTTP. This package contains + * the {@code HttpInputMessage} and {@code HttpOutputMessage} interfaces. + */ +package org.springframework.http; diff --git a/spring-web-reactive/src/main/java/org/springframework/util/package-info.java b/spring-web-reactive/src/main/java/org/springframework/util/package-info.java index 02cc0f8c2a..8c69995741 100644 --- a/spring-web-reactive/src/main/java/org/springframework/util/package-info.java +++ b/spring-web-reactive/src/main/java/org/springframework/util/package-info.java @@ -15,6 +15,6 @@ */ /** - * Miscellaneous utility classes, such as InputStream/OutputStream manipulation utilities. + * Miscellaneous utility classes, such as {@code InputStream}/{@code OutputStream} manipulation utilities. */ package org.springframework.util; From 9436dd74127ff57ed048299af8b23f29f4c24d36 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Thu, 14 Jan 2016 19:18:38 +0100 Subject: [PATCH 170/498] Polish Javadoc --- spring-web-reactive/build.gradle | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index f46943d550..8dfe49f515 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -35,6 +35,13 @@ ext { reactorVersion = '2.5.0.BUILD-SNAPSHOT' tomcatVersion = '8.0.28' jettyVersion = '9.3.5.v20151012' + + javadocLinks = [ + "http://docs.oracle.com/javase/8/docs/api/", + "http://next.projectreactor.io/core/docs/api/", + "http://docs.spring.io/spring/docs/${springVersion}/javadoc-api/", + "http://www.reactive-streams.org/reactive-streams-1.0.0-javadoc/" + ] as String[] } uploadArchives { @@ -46,9 +53,13 @@ uploadArchives { } javadoc { + description = "Generates project-level javadoc for use in -javadoc jar" + + options.memberLevel = org.gradle.external.javadoc.JavadocMemberLevel.PROTECTED options.author = true + options.header = project.name options.addStringOption('Xdoclint:none', '-quiet') - options.links 'http://docs.oracle.com/javase/8/docs/api/' + options.links(project.ext.javadocLinks) } task sourcesJar(type: Jar, dependsOn: classes) { From de300e8c00ac1d6371448acd63e2060c6766253e Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Thu, 14 Jan 2016 20:01:38 +0100 Subject: [PATCH 171/498] Fix the Javadoc external links by using a page without frames --- spring-web-reactive/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-web-reactive/README.md b/spring-web-reactive/README.md index 178cff2327..79d5f2b3be 100644 --- a/spring-web-reactive/README.md +++ b/spring-web-reactive/README.md @@ -53,4 +53,4 @@ The Spring Reactive is released under version 2.0 of the [Apache License][]. [issue tracker]: https://github.com/spring-projects/spring-reactive/issues [Pull requests]: http://help.github.com/send-pull-requests [Apache License]: http://www.apache.org/licenses/LICENSE-2.0 -[Javadoc]: https://repo.spring.io/snapshot/org/springframework/reactive/spring-reactive/0.1.0.BUILD-SNAPSHOT/spring-reactive-0.1.0.BUILD-SNAPSHOT-javadoc.jar!/index.html +[Javadoc]: https://repo.spring.io/snapshot/org/springframework/reactive/spring-reactive/0.1.0.BUILD-SNAPSHOT/spring-reactive-0.1.0.BUILD-SNAPSHOT-javadoc.jar!/overview-summary.html From 386dfb0881f083c792c2e7e8023d27304d9c70bf Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Tue, 19 Jan 2016 19:30:20 +0000 Subject: [PATCH 172/498] update DependencyUtils package --- .../http/server/reactive/UndertowHttpHandlerAdapter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java index 0e4f86f60d..4682592a51 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java @@ -36,9 +36,9 @@ import org.xnio.ChannelListener; import org.xnio.channels.StreamSinkChannel; import org.xnio.channels.StreamSourceChannel; import reactor.Mono; -import reactor.core.error.Exceptions; import reactor.core.subscriber.BaseSubscriber; import reactor.core.support.BackpressureUtils; +import reactor.core.support.Exceptions; import org.springframework.util.Assert; From bdfdd64db74ba706cb32ee864481e67adc68f55b Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Tue, 19 Jan 2016 19:46:46 +0000 Subject: [PATCH 173/498] merge core.processor and core.publisher --- .../http/server/reactive/AsyncIntegrationTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java index 66140914de..6f18736c15 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java @@ -27,7 +27,7 @@ import org.junit.runners.Parameterized; import reactor.Mono; import reactor.Processors; import reactor.Timers; -import reactor.core.processor.ProcessorGroup; +import reactor.core.publisher.ProcessorGroup; import reactor.io.buffer.Buffer; import reactor.rx.Stream; From 53b76e90fc5b3ad954439241ee85acd8209c5fca Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Tue, 19 Jan 2016 19:57:34 +0000 Subject: [PATCH 174/498] move BackpressureUtils to subscription --- .../core/codec/support/AbstractRawByteStreamDecoder.java | 2 +- .../springframework/core/codec/support/JsonObjectEncoder.java | 2 +- .../http/server/reactive/UndertowHttpHandlerAdapter.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractRawByteStreamDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractRawByteStreamDecoder.java index fa20ce0c19..da49c6a983 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractRawByteStreamDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractRawByteStreamDecoder.java @@ -24,7 +24,7 @@ import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import reactor.Flux; import reactor.core.subscriber.SubscriberBarrier; -import reactor.core.support.BackpressureUtils; +import reactor.core.subscription.BackpressureUtils; import org.springframework.core.ResolvableType; import org.springframework.core.codec.Decoder; diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectEncoder.java index 9b8f09e910..c39639f700 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectEncoder.java @@ -26,7 +26,7 @@ import org.reactivestreams.Subscriber; import reactor.Flux; import reactor.Mono; import reactor.core.subscriber.SubscriberBarrier; -import reactor.core.support.BackpressureUtils; +import reactor.core.subscription.BackpressureUtils; import reactor.io.buffer.Buffer; import org.springframework.core.ResolvableType; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java index 4682592a51..090caf179e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java @@ -37,7 +37,7 @@ import org.xnio.channels.StreamSinkChannel; import org.xnio.channels.StreamSourceChannel; import reactor.Mono; import reactor.core.subscriber.BaseSubscriber; -import reactor.core.support.BackpressureUtils; +import reactor.core.subscription.BackpressureUtils; import reactor.core.support.Exceptions; import org.springframework.util.Assert; From 198e31641247061d0e02f8103b6d5b4583b1c619 Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Tue, 19 Jan 2016 20:18:47 +0000 Subject: [PATCH 175/498] demote cyclic dependency under reactor-core reactor package -> reactor.Mono : reactor.core.publisher.Mono -> reactor.Flux : reactor.core.publisher.Flux -> reactor.Processors : reactor.core.publisher.Processors -> reactor.Timers : reactor.core.timer.Timers -> reactor.Subscribers : reactor.core.subscriber.Subscribers --- .../java/org/springframework/core/codec/Decoder.java | 2 +- .../java/org/springframework/core/codec/Encoder.java | 2 +- .../codec/support/AbstractRawByteStreamDecoder.java | 2 +- .../core/codec/support/ByteBufferDecoder.java | 2 +- .../core/codec/support/ByteBufferEncoder.java | 4 ++-- .../core/codec/support/JacksonJsonDecoder.java | 2 +- .../core/codec/support/JacksonJsonEncoder.java | 4 ++-- .../core/codec/support/Jaxb2Decoder.java | 2 +- .../core/codec/support/Jaxb2Encoder.java | 2 +- .../core/codec/support/JsonObjectDecoder.java | 2 +- .../core/codec/support/JsonObjectEncoder.java | 4 ++-- .../core/codec/support/StringDecoder.java | 2 +- .../core/codec/support/StringEncoder.java | 2 +- .../springframework/http/ReactiveHttpInputMessage.java | 2 +- .../http/ReactiveHttpOutputMessage.java | 2 +- .../server/reactive/AbstractServerHttpResponse.java | 4 ++-- .../http/server/reactive/HttpHandler.java | 2 +- .../server/reactive/ReactorHttpHandlerAdapter.java | 2 +- .../http/server/reactive/ReactorServerHttpRequest.java | 2 +- .../server/reactive/ReactorServerHttpResponse.java | 4 ++-- .../http/server/reactive/RxNettyServerHttpRequest.java | 2 +- .../server/reactive/RxNettyServerHttpResponse.java | 2 +- .../server/reactive/ServletHttpHandlerAdapter.java | 2 +- .../http/server/reactive/ServletServerHttpRequest.java | 2 +- .../server/reactive/ServletServerHttpResponse.java | 2 +- .../server/reactive/UndertowHttpHandlerAdapter.java | 2 +- .../server/reactive/UndertowServerHttpRequest.java | 2 +- .../server/reactive/UndertowServerHttpResponse.java | 2 +- .../web/reactive/DispatcherHandler.java | 4 ++-- .../springframework/web/reactive/HandlerAdapter.java | 2 +- .../springframework/web/reactive/HandlerMapping.java | 2 +- .../springframework/web/reactive/HandlerResult.java | 2 +- .../web/reactive/HandlerResultHandler.java | 2 +- .../web/reactive/ResponseStatusExceptionHandler.java | 2 +- .../reactive/handler/HttpHandlerHandlerAdapter.java | 2 +- .../reactive/handler/SimpleHandlerResultHandler.java | 2 +- .../web/reactive/handler/SimpleUrlHandlerMapping.java | 4 ++-- .../reactive/method/HandlerMethodArgumentResolver.java | 2 +- .../web/reactive/method/InvocableHandlerMethod.java | 2 +- .../method/annotation/RequestBodyArgumentResolver.java | 4 ++-- .../annotation/RequestMappingHandlerAdapter.java | 2 +- .../annotation/RequestMappingHandlerMapping.java | 4 ++-- .../annotation/RequestParamArgumentResolver.java | 2 +- .../method/annotation/ResponseBodyResultHandler.java | 4 +--- .../web/server/ExceptionHandlingWebHandler.java | 2 +- .../web/server/FilteringWebHandler.java | 2 +- .../web/server/WebExceptionHandler.java | 2 +- .../java/org/springframework/web/server/WebFilter.java | 2 +- .../org/springframework/web/server/WebFilterChain.java | 2 +- .../org/springframework/web/server/WebHandler.java | 2 +- .../web/server/WebHandlerDecorator.java | 2 +- .../web/server/WebToHttpHandlerAdapter.java | 2 +- .../http/server/reactive/AsyncIntegrationTests.java | 6 +++--- .../http/server/reactive/CookieIntegrationTests.java | 2 +- .../http/server/reactive/EchoHandler.java | 2 +- .../http/server/reactive/MockServerHttpRequest.java | 2 +- .../http/server/reactive/MockServerHttpResponse.java | 4 ++-- .../http/server/reactive/RandomHandler.java | 2 +- .../http/server/reactive/WriteWithOperatorTests.java | 2 +- .../http/server/reactive/XmlHandler.java | 4 ++-- .../reactive/codec/decoder/ByteBufferDecoderTests.java | 7 ++++--- .../codec/decoder/JacksonJsonDecoderTests.java | 9 ++++----- .../reactive/codec/decoder/Jaxb2DecoderTests.java | 9 ++++----- .../reactive/codec/decoder/JsonObjectDecoderTests.java | 7 ++++--- .../reactive/codec/decoder/StringDecoderTests.java | 10 +++++----- .../reactive/codec/encoder/ByteBufferEncoderTests.java | 7 ++++--- .../codec/encoder/JacksonJsonEncoderTests.java | 10 ++++------ .../reactive/codec/encoder/Jaxb2EncoderTests.java | 9 ++++----- .../reactive/codec/encoder/JsonObjectEncoderTests.java | 7 ++++--- .../reactive/codec/encoder/StringEncoderTests.java | 10 ++++------ .../web/reactive/DispatcherHandlerErrorTests.java | 7 ++----- .../SimpleUrlHandlerMappingIntegrationTests.java | 4 ++-- .../reactive/method/InvocableHandlerMethodTests.java | 4 ++-- .../annotation/RequestMappingHandlerMappingTests.java | 4 ++-- .../annotation/RequestMappingIntegrationTests.java | 4 ++-- .../web/server/ExceptionHandlingHttpHandlerTests.java | 2 +- .../web/server/FilteringWebHandlerTests.java | 2 +- 77 files changed, 126 insertions(+), 134 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/Decoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/Decoder.java index f78397dba8..7b3325110a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/Decoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/Decoder.java @@ -20,7 +20,7 @@ import java.nio.ByteBuffer; import java.util.List; import org.reactivestreams.Publisher; -import reactor.Flux; +import reactor.core.publisher.Flux; import org.springframework.core.ResolvableType; import org.springframework.util.MimeType; diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/Encoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/Encoder.java index bee2f78ed6..761ceb9b4b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/Encoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/Encoder.java @@ -20,7 +20,7 @@ import java.nio.ByteBuffer; import java.util.List; import org.reactivestreams.Publisher; -import reactor.Flux; +import reactor.core.publisher.Flux; import org.springframework.core.ResolvableType; import org.springframework.util.MimeType; diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractRawByteStreamDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractRawByteStreamDecoder.java index da49c6a983..21ec772cd4 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractRawByteStreamDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractRawByteStreamDecoder.java @@ -22,7 +22,7 @@ import java.util.concurrent.atomic.AtomicLongFieldUpdater; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; -import reactor.Flux; +import reactor.core.publisher.Flux; import reactor.core.subscriber.SubscriberBarrier; import reactor.core.subscription.BackpressureUtils; diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferDecoder.java index e24b722fe4..42595a351c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferDecoder.java @@ -19,7 +19,7 @@ package org.springframework.core.codec.support; import java.nio.ByteBuffer; import org.reactivestreams.Publisher; -import reactor.Flux; +import reactor.core.publisher.Flux; import org.springframework.core.ResolvableType; import org.springframework.util.MimeType; diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferEncoder.java index 71a2986581..7a76eb9232 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferEncoder.java @@ -19,7 +19,7 @@ package org.springframework.core.codec.support; import java.nio.ByteBuffer; import org.reactivestreams.Publisher; -import reactor.Flux; +import reactor.core.publisher.Flux; import org.springframework.core.ResolvableType; import org.springframework.util.MimeType; @@ -47,7 +47,7 @@ public class ByteBufferEncoder extends AbstractEncoder { MimeType mimeType, Object... hints) { //noinspection unchecked - return Flux.from((Publisher)inputStream); + return Flux.from(inputStream); } } \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonDecoder.java index 5042480450..e60a59a6cf 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonDecoder.java @@ -23,7 +23,7 @@ import java.nio.charset.StandardCharsets; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectReader; import org.reactivestreams.Publisher; -import reactor.Flux; +import reactor.core.publisher.Flux; import org.springframework.core.ResolvableType; import org.springframework.core.codec.CodecException; diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonEncoder.java index bd4426ce39..a42d655e95 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonEncoder.java @@ -22,8 +22,8 @@ import java.nio.charset.StandardCharsets; import com.fasterxml.jackson.databind.ObjectMapper; import org.reactivestreams.Publisher; -import reactor.Flux; -import reactor.Mono; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import reactor.io.buffer.Buffer; import org.springframework.core.ResolvableType; diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Decoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Decoder.java index b17150ed9b..94fda4c8ca 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Decoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Decoder.java @@ -34,7 +34,7 @@ import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.XMLReader; import org.xml.sax.helpers.XMLReaderFactory; -import reactor.Flux; +import reactor.core.publisher.Flux; import org.springframework.core.ResolvableType; import org.springframework.core.codec.CodecException; diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Encoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Encoder.java index d692c744ff..2482dd0fd0 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Encoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Encoder.java @@ -26,7 +26,7 @@ import javax.xml.bind.MarshalException; import javax.xml.bind.Marshaller; import org.reactivestreams.Publisher; -import reactor.Flux; +import reactor.core.publisher.Flux; import reactor.io.buffer.Buffer; import org.springframework.core.ResolvableType; diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectDecoder.java index 07ec097cd7..127a4ba74b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectDecoder.java @@ -25,7 +25,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; import org.reactivestreams.Publisher; -import reactor.Flux; +import reactor.core.publisher.Flux; import reactor.fn.Function; import org.springframework.core.ResolvableType; diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectEncoder.java index c39639f700..6fcb243024 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectEncoder.java @@ -23,8 +23,8 @@ import java.util.concurrent.atomic.AtomicLongFieldUpdater; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; -import reactor.Flux; -import reactor.Mono; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import reactor.core.subscriber.SubscriberBarrier; import reactor.core.subscription.BackpressureUtils; import reactor.io.buffer.Buffer; diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java index 332af997ea..9785ed5cbb 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java @@ -22,7 +22,7 @@ import java.nio.charset.StandardCharsets; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; -import reactor.Flux; +import reactor.core.publisher.Flux; import reactor.core.subscriber.SubscriberBarrier; import org.springframework.core.ResolvableType; diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringEncoder.java index 415e8d9f9a..50ea51a26d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringEncoder.java @@ -21,7 +21,7 @@ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import org.reactivestreams.Publisher; -import reactor.Flux; +import reactor.core.publisher.Flux; import org.springframework.core.ResolvableType; import org.springframework.util.MimeType; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpInputMessage.java b/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpInputMessage.java index 65778e219d..0cc0126ddd 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpInputMessage.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpInputMessage.java @@ -19,7 +19,7 @@ package org.springframework.http; import java.nio.ByteBuffer; import org.reactivestreams.Publisher; -import reactor.Flux; +import reactor.core.publisher.Flux; /** * An "reactive" HTTP input message that exposes the input as {@link Publisher}. diff --git a/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java b/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java index 1b76212bca..71fd66eec3 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java @@ -19,7 +19,7 @@ package org.springframework.http; import java.nio.ByteBuffer; import org.reactivestreams.Publisher; -import reactor.Mono; +import reactor.core.publisher.Mono; /** * A "reactive" HTTP output message that accepts output as a {@link Publisher}. diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java index f75f4be2ad..926c6c1692 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java @@ -18,8 +18,8 @@ package org.springframework.http.server.reactive; import java.nio.ByteBuffer; import org.reactivestreams.Publisher; -import reactor.Flux; -import reactor.Mono; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import org.springframework.http.HttpHeaders; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpHandler.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpHandler.java index e04512dceb..7ee64f161d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/HttpHandler.java @@ -16,7 +16,7 @@ package org.springframework.http.server.reactive; -import reactor.Mono; +import reactor.core.publisher.Mono; /** * Contract for handling HTTP requests in a non-blocking way. diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorHttpHandlerAdapter.java index 72ddbba872..cd3b722df5 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorHttpHandlerAdapter.java @@ -15,7 +15,7 @@ */ package org.springframework.http.server.reactive; -import reactor.Mono; +import reactor.core.publisher.Mono; import reactor.io.buffer.Buffer; import reactor.io.net.ReactiveChannelHandler; import reactor.io.net.http.HttpChannel; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java index 2ea2c9fcf9..49805625b7 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java @@ -22,7 +22,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; -import reactor.Flux; +import reactor.core.publisher.Flux; import reactor.io.buffer.Buffer; import reactor.io.net.http.HttpChannel; import reactor.io.net.http.model.Cookie; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java index 4e93607c5f..59ce84236c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java @@ -18,8 +18,8 @@ package org.springframework.http.server.reactive; import java.nio.ByteBuffer; import org.reactivestreams.Publisher; -import reactor.Flux; -import reactor.Mono; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import reactor.io.buffer.Buffer; import reactor.io.net.http.HttpChannel; import reactor.io.net.http.model.Cookie; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java index aa72c3f728..3cfa1a7175 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java @@ -26,7 +26,7 @@ import java.util.Map; import io.netty.buffer.ByteBuf; import io.netty.handler.codec.http.cookie.Cookie; import io.reactivex.netty.protocol.http.server.HttpServerRequest; -import reactor.Flux; +import reactor.core.publisher.Flux; import reactor.core.publisher.convert.RxJava1Converter; import rx.Observable; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java index 863d71f5ca..6fc60699ca 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java @@ -23,7 +23,7 @@ import io.netty.handler.codec.http.cookie.Cookie; import io.netty.handler.codec.http.cookie.DefaultCookie; import io.reactivex.netty.protocol.http.server.HttpServerResponse; import org.reactivestreams.Publisher; -import reactor.Mono; +import reactor.core.publisher.Mono; import reactor.core.publisher.convert.RxJava1Converter; import rx.Observable; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java index 41b229c6bc..11a29896ce 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java @@ -36,7 +36,7 @@ import org.apache.commons.logging.LogFactory; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; -import reactor.Mono; +import reactor.core.publisher.Mono; import org.springframework.http.HttpStatus; import org.springframework.util.Assert; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java index 6dd59e9379..ae570046f9 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java @@ -28,7 +28,7 @@ import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import org.reactivestreams.Publisher; -import reactor.Flux; +import reactor.core.publisher.Flux; import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java index 53f5dcc349..494031ce77 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java @@ -25,7 +25,7 @@ import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletResponse; import org.reactivestreams.Publisher; -import reactor.Mono; +import reactor.core.publisher.Mono; import org.springframework.http.HttpCookie; import org.springframework.http.HttpStatus; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java index 090caf179e..929c4a946b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java @@ -35,7 +35,7 @@ import org.reactivestreams.Subscription; import org.xnio.ChannelListener; import org.xnio.channels.StreamSinkChannel; import org.xnio.channels.StreamSourceChannel; -import reactor.Mono; +import reactor.core.publisher.Mono; import reactor.core.subscriber.BaseSubscriber; import reactor.core.subscription.BackpressureUtils; import reactor.core.support.Exceptions; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java index 6f8c35b7ff..9d04fcae69 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java @@ -27,7 +27,7 @@ import io.undertow.server.HttpServerExchange; import io.undertow.server.handlers.Cookie; import io.undertow.util.HeaderValues; import org.reactivestreams.Publisher; -import reactor.Flux; +import reactor.core.publisher.Flux; import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java index 3057396587..8d7a910de4 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java @@ -26,7 +26,7 @@ import io.undertow.server.handlers.Cookie; import io.undertow.server.handlers.CookieImpl; import io.undertow.util.HttpString; import org.reactivestreams.Publisher; -import reactor.Mono; +import reactor.core.publisher.Mono; import org.springframework.http.HttpCookie; import org.springframework.http.HttpStatus; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java index 3c4484a5ba..cc143cbfcd 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java @@ -23,8 +23,8 @@ import java.util.function.Function; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import reactor.Flux; -import reactor.Mono; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactoryUtils; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerAdapter.java index ad96158264..a8056ee3d7 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerAdapter.java @@ -18,7 +18,7 @@ package org.springframework.web.reactive; import java.util.function.Function; -import reactor.Mono; +import reactor.core.publisher.Mono; import org.springframework.web.server.WebServerExchange; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerMapping.java index d3c6f9ab6e..bc7f1bc06f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerMapping.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerMapping.java @@ -16,7 +16,7 @@ package org.springframework.web.reactive; -import reactor.Mono; +import reactor.core.publisher.Mono; import org.springframework.web.server.WebServerExchange; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java index 1e5660838a..9ffae86ba4 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java @@ -18,7 +18,7 @@ package org.springframework.web.reactive; import java.util.function.Function; -import reactor.Mono; +import reactor.core.publisher.Mono; import org.springframework.core.ResolvableType; import org.springframework.util.Assert; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResultHandler.java index d5a668a1ef..cbbfae5c76 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResultHandler.java @@ -16,7 +16,7 @@ package org.springframework.web.reactive; -import reactor.Mono; +import reactor.core.publisher.Mono; import org.springframework.web.server.WebServerExchange; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/ResponseStatusExceptionHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/ResponseStatusExceptionHandler.java index 8c19629d6c..f60ebbd464 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/ResponseStatusExceptionHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/ResponseStatusExceptionHandler.java @@ -15,7 +15,7 @@ */ package org.springframework.web.reactive; -import reactor.Mono; +import reactor.core.publisher.Mono; import org.springframework.web.ResponseStatusException; import org.springframework.web.server.WebExceptionHandler; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/HttpHandlerHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/HttpHandlerHandlerAdapter.java index b4994deb70..51212539b1 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/HttpHandlerHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/HttpHandlerHandlerAdapter.java @@ -17,7 +17,7 @@ package org.springframework.web.reactive.handler; import org.reactivestreams.Publisher; -import reactor.Mono; +import reactor.core.publisher.Mono; import org.springframework.core.ResolvableType; import org.springframework.web.reactive.DispatcherHandler; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java index 7c7c4126cd..cfaa7066fa 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java @@ -17,7 +17,7 @@ package org.springframework.web.reactive.handler; import org.reactivestreams.Publisher; -import reactor.Mono; +import reactor.core.publisher.Mono; import org.springframework.core.Ordered; import org.springframework.core.ResolvableType; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMapping.java index 5ff03ffdaf..1bca0d78ff 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMapping.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMapping.java @@ -19,8 +19,8 @@ package org.springframework.web.reactive.handler; import java.util.HashMap; import java.util.Map; -import reactor.Flux; -import reactor.Mono; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import org.springframework.web.reactive.HandlerMapping; import org.springframework.web.server.WebServerExchange; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/HandlerMethodArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/HandlerMethodArgumentResolver.java index c9fc9397c1..628d4380f1 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/HandlerMethodArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/HandlerMethodArgumentResolver.java @@ -16,7 +16,7 @@ package org.springframework.web.reactive.method; -import reactor.Mono; +import reactor.core.publisher.Mono; import org.springframework.core.MethodParameter; import org.springframework.web.server.WebServerExchange; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java index 1b30d6da7e..3508c1565c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java @@ -25,7 +25,7 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; -import reactor.Mono; +import reactor.core.publisher.Mono; import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.GenericTypeResolver; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestBodyArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestBodyArgumentResolver.java index 2ad7e92b1e..eb58b72e3c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestBodyArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestBodyArgumentResolver.java @@ -20,8 +20,8 @@ import java.nio.ByteBuffer; import java.util.List; import org.reactivestreams.Publisher; -import reactor.Flux; -import reactor.Mono; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java index 51d6539d30..41cfeaac93 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java @@ -25,7 +25,7 @@ import java.util.concurrent.ConcurrentHashMap; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import reactor.Mono; +import reactor.core.publisher.Mono; import org.springframework.beans.factory.InitializingBean; import org.springframework.core.codec.Decoder; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMapping.java index dff2c391e7..3d5761f4b8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMapping.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMapping.java @@ -27,8 +27,8 @@ import java.util.TreeSet; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import reactor.Flux; -import reactor.Mono; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ApplicationContext; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestParamArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestParamArgumentResolver.java index 8c056f4c37..3b09b36330 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestParamArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestParamArgumentResolver.java @@ -16,7 +16,7 @@ package org.springframework.web.reactive.method.annotation; -import reactor.Mono; +import reactor.core.publisher.Mono; import org.springframework.core.MethodParameter; import org.springframework.web.bind.annotation.RequestParam; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java index 1ebc9c23a5..1d6a5dd201 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java @@ -16,7 +16,6 @@ package org.springframework.web.reactive.method.annotation; -import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -28,12 +27,11 @@ import java.util.Set; import java.util.stream.Collectors; import org.reactivestreams.Publisher; -import reactor.Mono; +import reactor.core.publisher.Mono; import org.springframework.core.MethodParameter; import org.springframework.core.Ordered; import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.codec.Encoder; import org.springframework.core.convert.ConversionService; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/ExceptionHandlingWebHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/server/ExceptionHandlingWebHandler.java index 1c6ba76a65..5c662f0411 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/ExceptionHandlingWebHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/ExceptionHandlingWebHandler.java @@ -21,7 +21,7 @@ import java.util.List; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import reactor.Mono; +import reactor.core.publisher.Mono; import org.springframework.http.HttpStatus; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/FilteringWebHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/server/FilteringWebHandler.java index 128bbed47e..4228c8710a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/FilteringWebHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/FilteringWebHandler.java @@ -19,7 +19,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; -import reactor.Mono; +import reactor.core.publisher.Mono; /** * {@code WebHandler} that decorates another with a chain of {@link WebFilter}s. diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/WebExceptionHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/server/WebExceptionHandler.java index 84c12a65c5..438c9df82e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/WebExceptionHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/WebExceptionHandler.java @@ -15,7 +15,7 @@ */ package org.springframework.web.server; -import reactor.Mono; +import reactor.core.publisher.Mono; /** * Contract for handling exceptions during web server exchange processing. diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/WebFilter.java b/spring-web-reactive/src/main/java/org/springframework/web/server/WebFilter.java index 06c8b5e85c..2c003db3a6 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/WebFilter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/WebFilter.java @@ -16,7 +16,7 @@ package org.springframework.web.server; -import reactor.Mono; +import reactor.core.publisher.Mono; /** * Contract for interception-style, chained processing of Web requests that may diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/WebFilterChain.java b/spring-web-reactive/src/main/java/org/springframework/web/server/WebFilterChain.java index b071f97e73..e7ea8668b6 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/WebFilterChain.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/WebFilterChain.java @@ -15,7 +15,7 @@ */ package org.springframework.web.server; -import reactor.Mono; +import reactor.core.publisher.Mono; /** * Contract to allow a {@link WebFilter} to delegate to the next in the chain. diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/WebHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/server/WebHandler.java index 58cb9286fb..e0566e51f0 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/WebHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/WebHandler.java @@ -16,7 +16,7 @@ package org.springframework.web.server; -import reactor.Mono; +import reactor.core.publisher.Mono; /** * Contract to handle a web server exchange. diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/WebHandlerDecorator.java b/spring-web-reactive/src/main/java/org/springframework/web/server/WebHandlerDecorator.java index 42cc20e3ec..83cd7a8907 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/WebHandlerDecorator.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/WebHandlerDecorator.java @@ -15,7 +15,7 @@ */ package org.springframework.web.server; -import reactor.Mono; +import reactor.core.publisher.Mono; import org.springframework.util.Assert; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/WebToHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/server/WebToHttpHandlerAdapter.java index ec8b418a40..34684b10d4 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/WebToHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/WebToHttpHandlerAdapter.java @@ -17,7 +17,7 @@ package org.springframework.web.server; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import reactor.Mono; +import reactor.core.publisher.Mono; import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.HttpHandler; diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java index 6f18736c15..855840649c 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java @@ -24,10 +24,10 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; -import reactor.Mono; -import reactor.Processors; -import reactor.Timers; +import reactor.core.publisher.Mono; import reactor.core.publisher.ProcessorGroup; +import reactor.core.publisher.Processors; +import reactor.core.timer.Timers; import reactor.io.buffer.Buffer; import reactor.rx.Stream; diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/CookieIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/CookieIntegrationTests.java index 78dc08bd13..38f714f85c 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/CookieIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/CookieIntegrationTests.java @@ -25,7 +25,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; -import reactor.Mono; +import reactor.core.publisher.Mono; import org.springframework.http.HttpCookie; import org.springframework.http.RequestEntity; diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/EchoHandler.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/EchoHandler.java index c6f7a7b28a..b5bf612848 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/EchoHandler.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/EchoHandler.java @@ -16,7 +16,7 @@ package org.springframework.http.server.reactive; -import reactor.Mono; +import reactor.core.publisher.Mono; /** * @author Arjen Poutsma diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpRequest.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpRequest.java index db4412c6f5..183bddaede 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpRequest.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpRequest.java @@ -19,7 +19,7 @@ import java.net.URI; import java.nio.ByteBuffer; import org.reactivestreams.Publisher; -import reactor.Flux; +import reactor.core.publisher.Flux; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java index 0aab9caaf3..9b0db98752 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java @@ -18,8 +18,8 @@ package org.springframework.http.server.reactive; import java.nio.ByteBuffer; import org.reactivestreams.Publisher; -import reactor.Flux; -import reactor.Mono; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/RandomHandler.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/RandomHandler.java index 477114c892..e72e213b32 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/RandomHandler.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/RandomHandler.java @@ -23,7 +23,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; -import reactor.Mono; +import reactor.core.publisher.Mono; import reactor.io.buffer.Buffer; import static org.junit.Assert.assertEquals; diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/WriteWithOperatorTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/WriteWithOperatorTests.java index c4b8d22385..fd594b6823 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/WriteWithOperatorTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/WriteWithOperatorTests.java @@ -27,7 +27,7 @@ import org.junit.Test; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; -import reactor.Flux; +import reactor.core.publisher.Flux; import reactor.core.subscriber.SubscriberBarrier; import reactor.rx.Stream; import reactor.rx.stream.Signal; diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/XmlHandler.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/XmlHandler.java index 90c8a75998..74396d3c2f 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/XmlHandler.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/XmlHandler.java @@ -22,8 +22,8 @@ import javax.xml.bind.Unmarshaller; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import reactor.Flux; -import reactor.Mono; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import reactor.io.buffer.Buffer; import org.springframework.http.MediaType; diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/ByteBufferDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/ByteBufferDecoderTests.java index 1259a0019c..85106d1130 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/ByteBufferDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/ByteBufferDecoderTests.java @@ -20,17 +20,18 @@ import java.nio.ByteBuffer; import java.util.List; import java.util.stream.StreamSupport; -import static java.util.stream.Collectors.toList; -import static org.junit.Assert.*; import org.junit.Test; import org.reactivestreams.Publisher; -import reactor.Flux; +import reactor.core.publisher.Flux; import reactor.io.buffer.Buffer; import org.springframework.core.ResolvableType; import org.springframework.core.codec.support.ByteBufferDecoder; import org.springframework.http.MediaType; +import static java.util.stream.Collectors.toList; +import static org.junit.Assert.*; + /** * @author Sebastien Deleuze */ diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoderTests.java index e80bd0298d..8a0e476c43 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoderTests.java @@ -20,12 +20,8 @@ import java.nio.ByteBuffer; import java.util.List; import java.util.stream.StreamSupport; -import static java.util.stream.Collectors.toList; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; import org.junit.Test; -import reactor.Flux; +import reactor.core.publisher.Flux; import reactor.io.buffer.Buffer; import org.springframework.core.ResolvableType; @@ -33,6 +29,9 @@ import org.springframework.core.codec.support.JacksonJsonDecoder; import org.springframework.http.MediaType; import org.springframework.reactive.codec.Pojo; +import static java.util.stream.Collectors.toList; +import static org.junit.Assert.*; + /** * @author Sebastien Deleuze */ diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/Jaxb2DecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/Jaxb2DecoderTests.java index 69aaffdf8a..a50b061afc 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/Jaxb2DecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/Jaxb2DecoderTests.java @@ -20,12 +20,8 @@ import java.nio.ByteBuffer; import java.util.List; import java.util.stream.StreamSupport; -import static java.util.stream.Collectors.toList; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; import org.junit.Test; -import reactor.Flux; +import reactor.core.publisher.Flux; import reactor.io.buffer.Buffer; import org.springframework.core.ResolvableType; @@ -33,6 +29,9 @@ import org.springframework.core.codec.support.Jaxb2Decoder; import org.springframework.http.MediaType; import org.springframework.reactive.codec.Pojo; +import static java.util.stream.Collectors.toList; +import static org.junit.Assert.*; + /** * @author Sebastien Deleuze */ diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/JsonObjectDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/JsonObjectDecoderTests.java index 6f3a5e1e1c..c0b27df9f8 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/JsonObjectDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/JsonObjectDecoderTests.java @@ -21,14 +21,15 @@ import java.nio.charset.StandardCharsets; import java.util.List; import java.util.stream.StreamSupport; -import static java.util.stream.Collectors.toList; -import static org.junit.Assert.assertEquals; import org.junit.Test; -import reactor.Flux; +import reactor.core.publisher.Flux; import reactor.io.buffer.Buffer; import org.springframework.core.codec.support.JsonObjectDecoder; +import static java.util.stream.Collectors.toList; +import static org.junit.Assert.assertEquals; + /** * @author Sebastien Deleuze */ diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/StringDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/StringDecoderTests.java index 0d693ec4af..d185f61af7 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/StringDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/StringDecoderTests.java @@ -16,16 +16,13 @@ package org.springframework.reactive.codec.decoder; -import static java.util.stream.Collectors.*; -import static org.junit.Assert.*; - import java.nio.ByteBuffer; import java.util.List; import java.util.stream.StreamSupport; import org.junit.Test; -import reactor.Flux; -import reactor.Mono; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import reactor.core.publisher.convert.RxJava1SingleConverter; import reactor.io.buffer.Buffer; import rx.Single; @@ -34,6 +31,9 @@ import org.springframework.core.ResolvableType; import org.springframework.core.codec.support.StringDecoder; import org.springframework.http.MediaType; +import static java.util.stream.Collectors.toList; +import static org.junit.Assert.*; + /** * @author Sebastien Deleuze * @author Brian Clozel diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/ByteBufferEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/ByteBufferEncoderTests.java index c7612bb0aa..16eb7a5644 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/ByteBufferEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/ByteBufferEncoderTests.java @@ -20,17 +20,18 @@ import java.nio.ByteBuffer; import java.util.List; import java.util.stream.StreamSupport; -import static java.util.stream.Collectors.toList; -import static org.junit.Assert.*; import org.junit.Test; import org.reactivestreams.Publisher; -import reactor.Flux; +import reactor.core.publisher.Flux; import reactor.io.buffer.Buffer; import org.springframework.core.ResolvableType; import org.springframework.core.codec.support.ByteBufferEncoder; import org.springframework.http.MediaType; +import static java.util.stream.Collectors.toList; +import static org.junit.Assert.*; + /** * @author Sebastien Deleuze */ diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoderTests.java index 685c3d8e9f..18492f1d88 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoderTests.java @@ -16,22 +16,20 @@ package org.springframework.reactive.codec.encoder; -import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.List; import java.util.stream.StreamSupport; -import static java.util.stream.Collectors.toList; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; import org.junit.Test; -import reactor.Flux; +import reactor.core.publisher.Flux; import org.springframework.core.codec.support.JacksonJsonEncoder; import org.springframework.http.MediaType; import org.springframework.reactive.codec.Pojo; +import static java.util.stream.Collectors.toList; +import static org.junit.Assert.*; + /** * @author Sebastien Deleuze */ diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/Jaxb2EncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/Jaxb2EncoderTests.java index d2c4d62ca4..dbbb849d6c 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/Jaxb2EncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/Jaxb2EncoderTests.java @@ -20,17 +20,16 @@ import java.nio.charset.StandardCharsets; import java.util.List; import java.util.stream.StreamSupport; -import static java.util.stream.Collectors.toList; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; import org.junit.Test; -import reactor.Flux; +import reactor.core.publisher.Flux; import org.springframework.core.codec.support.Jaxb2Encoder; import org.springframework.http.MediaType; import org.springframework.reactive.codec.Pojo; +import static java.util.stream.Collectors.toList; +import static org.junit.Assert.*; + /** * @author Sebastien Deleuze */ diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/JsonObjectEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/JsonObjectEncoderTests.java index 0d31bfda8a..ee6ac9f07f 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/JsonObjectEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/JsonObjectEncoderTests.java @@ -19,14 +19,15 @@ package org.springframework.reactive.codec.encoder; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; -import static org.junit.Assert.assertEquals; import org.junit.Test; -import reactor.Flux; -import reactor.Mono; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import reactor.io.buffer.Buffer; import org.springframework.core.codec.support.JsonObjectEncoder; +import static org.junit.Assert.assertEquals; + /** * @author Sebastien Deleuze */ diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/StringEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/StringEncoderTests.java index 215c3d17ea..18a1c8993b 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/StringEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/StringEncoderTests.java @@ -16,22 +16,20 @@ package org.springframework.reactive.codec.encoder; -import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.List; import java.util.stream.StreamSupport; -import static java.util.stream.Collectors.toList; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; import org.junit.Test; -import reactor.Flux; +import reactor.core.publisher.Flux; import org.springframework.core.ResolvableType; import org.springframework.core.codec.support.StringEncoder; import org.springframework.http.MediaType; +import static java.util.stream.Collectors.toList; +import static org.junit.Assert.*; + /** * @author Sebastien Deleuze */ diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java index 216fb15c2e..443009da52 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java @@ -23,7 +23,7 @@ import java.util.List; import org.junit.Before; import org.junit.Test; import org.reactivestreams.Publisher; -import reactor.Mono; +import reactor.core.publisher.Mono; import reactor.rx.Stream; import reactor.rx.stream.Signal; @@ -57,10 +57,7 @@ import org.springframework.web.server.WebHandler; import org.springframework.web.server.WebServerExchange; import static org.hamcrest.CoreMatchers.startsWith; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertThat; +import static org.junit.Assert.*; /** * Test the effect of exceptions at different stages of request processing by diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java index c25ea1bb7e..8221d555a0 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java @@ -22,8 +22,8 @@ import java.util.HashMap; import java.util.Map; import org.junit.Test; -import reactor.Flux; -import reactor.Mono; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import reactor.io.buffer.Buffer; import org.springframework.context.support.StaticApplicationContext; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java index fef38f5552..dc4ffd88fc 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java @@ -25,8 +25,8 @@ import java.util.List; import org.junit.Before; import org.junit.Test; import org.reactivestreams.Publisher; -import reactor.Flux; -import reactor.Mono; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import reactor.rx.Stream; import reactor.rx.stream.Signal; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java index 1bc53dcdce..8ce0f57628 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java @@ -20,10 +20,10 @@ import java.net.URI; import java.util.List; import java.util.stream.StreamSupport; -import static java.util.stream.Collectors.toList; import org.junit.Before; import org.junit.Test; import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; import org.springframework.context.support.StaticApplicationContext; import org.springframework.http.HttpMethod; @@ -37,9 +37,9 @@ import org.springframework.web.method.HandlerMethod; import org.springframework.web.server.DefaultWebServerExchange; import org.springframework.web.server.WebServerExchange; +import static java.util.stream.Collectors.toList; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; -import reactor.Flux; /** * @author Sebastien Deleuze diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java index a10c0f23f3..393792e3d3 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java @@ -26,8 +26,8 @@ import java.util.concurrent.CompletableFuture; import org.junit.Ignore; import org.junit.Test; import org.reactivestreams.Publisher; -import reactor.Flux; -import reactor.Mono; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import reactor.io.buffer.Buffer; import reactor.rx.Promise; import reactor.rx.Stream; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/server/ExceptionHandlingHttpHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/server/ExceptionHandlingHttpHandlerTests.java index 48cf44bdd0..0360c17ace 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/server/ExceptionHandlingHttpHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/server/ExceptionHandlingHttpHandlerTests.java @@ -20,7 +20,7 @@ import java.net.URI; import org.junit.Before; import org.junit.Test; -import reactor.Mono; +import reactor.core.publisher.Mono; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/server/FilteringWebHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/server/FilteringWebHandlerTests.java index e06a9104a2..7f56a39cce 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/server/FilteringWebHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/server/FilteringWebHandlerTests.java @@ -20,7 +20,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.junit.Before; import org.junit.Test; -import reactor.Mono; +import reactor.core.publisher.Mono; import org.springframework.http.server.reactive.HttpHandler; import org.springframework.http.server.reactive.ServerHttpRequest; From 2dd9c9226781ba9105940b0404cfe46f125871e2 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 19 Jan 2016 21:34:28 -0500 Subject: [PATCH 176/498] Update reactor converter package --- .../ReactiveStreamsToCompletableFutureConverter.java | 4 ++-- .../convert/support/ReactiveStreamsToRxJava1Converter.java | 6 +++--- .../http/server/reactive/RxNettyHttpHandlerAdapter.java | 2 +- .../http/server/reactive/RxNettyServerHttpRequest.java | 2 +- .../http/server/reactive/RxNettyServerHttpResponse.java | 2 +- .../reactive/codec/decoder/StringDecoderTests.java | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToCompletableFutureConverter.java b/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToCompletableFutureConverter.java index dcb17c6de2..692633ec03 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToCompletableFutureConverter.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToCompletableFutureConverter.java @@ -44,10 +44,10 @@ public class ReactiveStreamsToCompletableFutureConverter implements GenericConve return null; } else if (CompletableFuture.class.isAssignableFrom(source.getClass())) { - return reactor.core.publisher.convert.CompletableFutureConverter.from((CompletableFuture) source); + return reactor.core.converter.CompletableFutureConverter.from((CompletableFuture) source); } else if (CompletableFuture.class.isAssignableFrom(targetType.getResolvableType().getRawClass())) { - return reactor.core.publisher.convert.CompletableFutureConverter.fromSingle((Publisher) source); + return reactor.core.converter.CompletableFutureConverter.fromSingle((Publisher) source); } return null; } diff --git a/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToRxJava1Converter.java b/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToRxJava1Converter.java index 546d49f560..97a1d3e3a5 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToRxJava1Converter.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToRxJava1Converter.java @@ -20,7 +20,7 @@ import java.util.LinkedHashSet; import java.util.Set; import org.reactivestreams.Publisher; -import reactor.core.publisher.convert.RxJava1Converter; +import reactor.core.converter.RxJava1Converter; import rx.Observable; import rx.Single; @@ -56,10 +56,10 @@ public final class ReactiveStreamsToRxJava1Converter implements GenericConverter return RxJava1Converter.from((Publisher) source); } else if (Single.class.isAssignableFrom(source.getClass())) { - return reactor.core.publisher.convert.RxJava1SingleConverter.from((Single) source); + return reactor.core.converter.RxJava1SingleConverter.from((Single) source); } else if (Single.class.isAssignableFrom(targetType.getResolvableType().getRawClass())) { - return reactor.core.publisher.convert.RxJava1SingleConverter.from((Publisher) source); + return reactor.core.converter.RxJava1SingleConverter.from((Publisher) source); } return null; } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyHttpHandlerAdapter.java index a3190c8f7c..91ae66ebdc 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyHttpHandlerAdapter.java @@ -21,7 +21,7 @@ import io.reactivex.netty.protocol.http.server.HttpServerRequest; import io.reactivex.netty.protocol.http.server.HttpServerResponse; import io.reactivex.netty.protocol.http.server.RequestHandler; import org.reactivestreams.Publisher; -import reactor.core.publisher.convert.RxJava1Converter; +import reactor.core.converter.RxJava1Converter; import rx.Observable; import org.springframework.util.Assert; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java index 3cfa1a7175..ae1bc28ce5 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java @@ -26,8 +26,8 @@ import java.util.Map; import io.netty.buffer.ByteBuf; import io.netty.handler.codec.http.cookie.Cookie; import io.reactivex.netty.protocol.http.server.HttpServerRequest; +import reactor.core.converter.RxJava1Converter; import reactor.core.publisher.Flux; -import reactor.core.publisher.convert.RxJava1Converter; import rx.Observable; import org.springframework.http.HttpCookie; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java index 6fc60699ca..a38685d675 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java @@ -24,7 +24,7 @@ import io.netty.handler.codec.http.cookie.DefaultCookie; import io.reactivex.netty.protocol.http.server.HttpServerResponse; import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; -import reactor.core.publisher.convert.RxJava1Converter; +import reactor.core.converter.RxJava1Converter; import rx.Observable; import org.springframework.http.HttpCookie; diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/StringDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/StringDecoderTests.java index d185f61af7..e3538a757b 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/StringDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/StringDecoderTests.java @@ -23,7 +23,7 @@ import java.util.stream.StreamSupport; import org.junit.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import reactor.core.publisher.convert.RxJava1SingleConverter; +import reactor.core.converter.RxJava1SingleConverter; import reactor.io.buffer.Buffer; import rx.Single; From 4572cfa38c3d3b91c2b23dd519fd4ee147cc6c47 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Wed, 20 Jan 2016 11:45:18 +0100 Subject: [PATCH 177/498] Fix RxJava1Converter -> RxJava1ObservableConverter --- .../support/ReactiveStreamsToRxJava1Converter.java | 11 ++++++----- .../server/reactive/RxNettyHttpHandlerAdapter.java | 4 ++-- .../server/reactive/RxNettyServerHttpRequest.java | 4 ++-- .../server/reactive/RxNettyServerHttpResponse.java | 6 +++--- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToRxJava1Converter.java b/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToRxJava1Converter.java index 97a1d3e3a5..a9f6c0557c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToRxJava1Converter.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToRxJava1Converter.java @@ -20,7 +20,8 @@ import java.util.LinkedHashSet; import java.util.Set; import org.reactivestreams.Publisher; -import reactor.core.converter.RxJava1Converter; +import reactor.core.converter.RxJava1ObservableConverter; +import reactor.core.converter.RxJava1SingleConverter; import rx.Observable; import rx.Single; @@ -50,16 +51,16 @@ public final class ReactiveStreamsToRxJava1Converter implements GenericConverter return null; } if (Observable.class.isAssignableFrom(source.getClass())) { - return RxJava1Converter.from((Observable) source); + return RxJava1ObservableConverter.from((Observable) source); } else if (Observable.class.isAssignableFrom(targetType.getResolvableType().getRawClass())) { - return RxJava1Converter.from((Publisher) source); + return RxJava1ObservableConverter.from((Publisher) source); } else if (Single.class.isAssignableFrom(source.getClass())) { - return reactor.core.converter.RxJava1SingleConverter.from((Single) source); + return RxJava1SingleConverter.from((Single) source); } else if (Single.class.isAssignableFrom(targetType.getResolvableType().getRawClass())) { - return reactor.core.converter.RxJava1SingleConverter.from((Publisher) source); + return RxJava1SingleConverter.from((Publisher) source); } return null; } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyHttpHandlerAdapter.java index 91ae66ebdc..7763281fe9 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyHttpHandlerAdapter.java @@ -21,7 +21,7 @@ import io.reactivex.netty.protocol.http.server.HttpServerRequest; import io.reactivex.netty.protocol.http.server.HttpServerResponse; import io.reactivex.netty.protocol.http.server.RequestHandler; import org.reactivestreams.Publisher; -import reactor.core.converter.RxJava1Converter; +import reactor.core.converter.RxJava1ObservableConverter; import rx.Observable; import org.springframework.util.Assert; @@ -44,7 +44,7 @@ public class RxNettyHttpHandlerAdapter implements RequestHandler result = this.httpHandler.handle(adaptedRequest, adaptedResponse); - return RxJava1Converter.from(result); + return RxJava1ObservableConverter.from(result); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java index ae1bc28ce5..26dbc8271d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java @@ -26,7 +26,7 @@ import java.util.Map; import io.netty.buffer.ByteBuf; import io.netty.handler.codec.http.cookie.Cookie; import io.reactivex.netty.protocol.http.server.HttpServerRequest; -import reactor.core.converter.RxJava1Converter; +import reactor.core.converter.RxJava1ObservableConverter; import reactor.core.publisher.Flux; import rx.Observable; @@ -91,7 +91,7 @@ public class RxNettyServerHttpRequest extends AbstractServerHttpRequest { public Flux getBody() { Observable content = this.request.getContent().map(ByteBuf::nioBuffer); content = content.concatWith(Observable.empty()); // See GH issue #58 - return RxJava1Converter.from(content); + return RxJava1ObservableConverter.from(content); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java index a38685d675..7dc1aa285f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java @@ -23,8 +23,8 @@ import io.netty.handler.codec.http.cookie.Cookie; import io.netty.handler.codec.http.cookie.DefaultCookie; import io.reactivex.netty.protocol.http.server.HttpServerResponse; import org.reactivestreams.Publisher; +import reactor.core.converter.RxJava1ObservableConverter; import reactor.core.publisher.Mono; -import reactor.core.converter.RxJava1Converter; import rx.Observable; import org.springframework.http.HttpCookie; @@ -59,9 +59,9 @@ public class RxNettyServerHttpResponse extends AbstractServerHttpResponse { @Override protected Mono setBodyInternal(Publisher publisher) { - Observable content = RxJava1Converter.from(publisher).map(this::toBytes); + Observable content = RxJava1ObservableConverter.from(publisher).map(this::toBytes); Observable completion = this.response.writeBytes(content); - return RxJava1Converter.from(completion).after(); + return RxJava1ObservableConverter.from(completion).after(); } private byte[] toBytes(ByteBuffer buffer) { From 7b3e9e09505e78077dab3df33bc16504a4d0ff8a Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Wed, 20 Jan 2016 18:01:07 +0000 Subject: [PATCH 178/498] move queue.disruptor and packagescope RingBufferUtils/RequestTask --- .../http/server/reactive/UndertowHttpHandlerAdapter.java | 2 +- .../springframework/http/server/reactive/WriteWithOperator.java | 2 +- .../http/server/reactive/boot/ReactorHttpServer.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java index 929c4a946b..f25bd17a9e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java @@ -38,7 +38,7 @@ import org.xnio.channels.StreamSourceChannel; import reactor.core.publisher.Mono; import reactor.core.subscriber.BaseSubscriber; import reactor.core.subscription.BackpressureUtils; -import reactor.core.support.Exceptions; +import reactor.core.util.Exceptions; import org.springframework.util.Assert; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/WriteWithOperator.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/WriteWithOperator.java index 2f23abf73c..873a0e6407 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/WriteWithOperator.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/WriteWithOperator.java @@ -19,7 +19,7 @@ import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import reactor.core.subscriber.SubscriberBarrier; -import reactor.core.support.Assert; +import reactor.core.util.Assert; import reactor.fn.Function; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java index e575fc82b8..757e6e6810 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java @@ -16,7 +16,7 @@ package org.springframework.http.server.reactive.boot; -import reactor.core.support.ReactiveState; +import reactor.core.util.ReactiveState; import reactor.io.buffer.Buffer; import reactor.io.net.ReactiveNet; From 4e162122af36446d43f8196b0c8fbf663d3cc4d5 Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Wed, 20 Jan 2016 20:50:28 +0000 Subject: [PATCH 179/498] sync update on reactor-core --- .../core/codec/support/AbstractRawByteStreamDecoder.java | 2 +- .../springframework/core/codec/support/JsonObjectEncoder.java | 2 +- .../http/server/reactive/UndertowHttpHandlerAdapter.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractRawByteStreamDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractRawByteStreamDecoder.java index 21ec772cd4..a77141afca 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractRawByteStreamDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractRawByteStreamDecoder.java @@ -24,7 +24,7 @@ import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import reactor.core.publisher.Flux; import reactor.core.subscriber.SubscriberBarrier; -import reactor.core.subscription.BackpressureUtils; +import reactor.core.util.BackpressureUtils; import org.springframework.core.ResolvableType; import org.springframework.core.codec.Decoder; diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectEncoder.java index 6fcb243024..a2c5c5d34c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectEncoder.java @@ -26,7 +26,7 @@ import org.reactivestreams.Subscriber; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.subscriber.SubscriberBarrier; -import reactor.core.subscription.BackpressureUtils; +import reactor.core.util.BackpressureUtils; import reactor.io.buffer.Buffer; import org.springframework.core.ResolvableType; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java index f25bd17a9e..8977890fed 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java @@ -37,7 +37,7 @@ import org.xnio.channels.StreamSinkChannel; import org.xnio.channels.StreamSourceChannel; import reactor.core.publisher.Mono; import reactor.core.subscriber.BaseSubscriber; -import reactor.core.subscription.BackpressureUtils; +import reactor.core.util.BackpressureUtils; import reactor.core.util.Exceptions; import org.springframework.util.Assert; From 30b007dc26437c25d404a0430cea8a2e95e5b7d1 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 20 Jan 2016 18:01:32 -0500 Subject: [PATCH 180/498] Based on fix in core for SPR-13876 --- .../server/reactive/ServletServerHttpRequest.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java index ae570046f9..89a3dbc3b4 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java @@ -69,11 +69,12 @@ public class ServletServerHttpRequest extends AbstractServerHttpRequest { @Override protected URI initUri() throws URISyntaxException { - return new URI(getServletRequest().getScheme(), null, - getServletRequest().getServerName(), - getServletRequest().getServerPort(), - getServletRequest().getRequestURI(), - getServletRequest().getQueryString(), null); + StringBuffer url = this.request.getRequestURL(); + String query = this.request.getQueryString(); + if (StringUtils.hasText(query)) { + url.append('?').append(query); + } + return new URI(url.toString()); } @Override From 0ba5e1d21ccdce5cc42f953d709bb6db2e7ff4a9 Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Thu, 21 Jan 2016 11:59:15 +0000 Subject: [PATCH 181/498] sync update on reactor-io Buffer move --- spring-web-reactive/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index 8dfe49f515..a35115c7e0 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -89,6 +89,7 @@ dependencies { optional "com.fasterxml.jackson.core:jackson-databind:2.6.2" optional "io.projectreactor:reactor-stream:${reactorVersion}" optional "io.projectreactor:reactor-net:${reactorVersion}" + optional "io.projectreactor:reactor-io:${reactorVersion}" optional "org.apache.tomcat:tomcat-util:${tomcatVersion}" optional "org.apache.tomcat.embed:tomcat-embed-core:${tomcatVersion}" optional 'io.undertow:undertow-core:1.3.5.Final' From a90789891070d4b7a247be0459cede380f667b44 Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Fri, 22 Jan 2016 23:11:31 +0000 Subject: [PATCH 182/498] fix signature --- .../server/reactive/boot/ReactorHttpServer.java | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java index 757e6e6810..2538a4588d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java @@ -16,7 +16,8 @@ package org.springframework.http.server.reactive.boot; -import reactor.core.util.ReactiveState; +import reactor.core.trait.Completable; +import reactor.core.trait.Connectable; import reactor.io.buffer.Buffer; import reactor.io.net.ReactiveNet; @@ -28,7 +29,7 @@ import org.springframework.util.Assert; * @author Stephane Maldini */ public class ReactorHttpServer extends HttpServerSupport - implements InitializingBean, HttpServer, ReactiveState.FeedbackLoop, ReactiveState.ActiveUpstream { + implements InitializingBean, HttpServer, Connectable, Completable { private ReactorHttpHandlerAdapter reactorHandler; @@ -52,12 +53,12 @@ public class ReactorHttpServer extends HttpServerSupport } @Override - public Object delegateInput() { + public Object connectedInput() { return reactorServer; } @Override - public Object delegateOutput() { + public Object connectedOutput() { return reactorServer; } @@ -92,4 +93,8 @@ public class ReactorHttpServer extends HttpServerSupport } } + @Override + public Object upstream() { + return null; + } } From ac94bf112013a779e7aed7b265b6f669e14de353 Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Mon, 25 Jan 2016 09:05:40 +0000 Subject: [PATCH 183/498] sync with reactor-stream package-scope change --- .../http/server/reactive/WriteWithOperatorTests.java | 2 +- .../web/reactive/DispatcherHandlerErrorTests.java | 2 +- .../web/reactive/ResponseStatusExceptionHandlerTests.java | 3 +-- .../web/reactive/method/InvocableHandlerMethodTests.java | 3 +-- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/WriteWithOperatorTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/WriteWithOperatorTests.java index fd594b6823..973909a12e 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/WriteWithOperatorTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/WriteWithOperatorTests.java @@ -29,8 +29,8 @@ import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import reactor.core.publisher.Flux; import reactor.core.subscriber.SubscriberBarrier; +import reactor.rx.Signal; import reactor.rx.Stream; -import reactor.rx.stream.Signal; import static org.junit.Assert.*; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java index 443009da52..aa3672c1c0 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java @@ -24,8 +24,8 @@ import org.junit.Before; import org.junit.Test; import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; +import reactor.rx.Signal; import reactor.rx.Stream; -import reactor.rx.stream.Signal; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java index 68a2ce3c72..a0c4e388b8 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java @@ -21,8 +21,8 @@ import java.util.List; import org.junit.Before; import org.junit.Test; import org.reactivestreams.Publisher; +import reactor.rx.Signal; import reactor.rx.Stream; -import reactor.rx.stream.Signal; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; @@ -32,7 +32,6 @@ import org.springframework.web.ResponseStatusException; import org.springframework.web.server.DefaultWebServerExchange; import org.springframework.web.server.WebServerExchange; -import static junit.framework.TestCase.assertSame; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java index dc4ffd88fc..c0385cde11 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java @@ -15,7 +15,6 @@ */ package org.springframework.web.reactive.method; - import java.lang.reflect.Method; import java.net.URI; import java.util.Arrays; @@ -27,8 +26,8 @@ import org.junit.Test; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.rx.Signal; import reactor.rx.Stream; -import reactor.rx.stream.Signal; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; From 21fff02dbd7cc0ab39480f0cdcf21f1c9287d839 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Tue, 26 Jan 2016 12:43:59 +0100 Subject: [PATCH 184/498] Fixing the build that bad boy @smaldini broke --- .../web/reactive/ResponseStatusExceptionHandlerTests.java | 1 + 1 file changed, 1 insertion(+) diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java index a0c4e388b8..8a992da11b 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java @@ -33,6 +33,7 @@ import org.springframework.web.server.DefaultWebServerExchange; import org.springframework.web.server.WebServerExchange; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; /** From 10f4f64e6f5c7afc7ee56635e9867f29bd669c5e Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Tue, 26 Jan 2016 23:15:55 +0000 Subject: [PATCH 185/498] sync updates with mono --- .../http/server/reactive/boot/ReactorHttpServer.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java index 2538a4588d..91a1098f5b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java @@ -1,11 +1,11 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright (c) 2011-2016 Pivotal Software Inc, All Rights Reserved. * * 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 + * 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, @@ -16,8 +16,8 @@ package org.springframework.http.server.reactive.boot; -import reactor.core.trait.Completable; -import reactor.core.trait.Connectable; +import reactor.core.graph.Connectable; +import reactor.core.state.Completable; import reactor.io.buffer.Buffer; import reactor.io.net.ReactiveNet; From 641a57ec9302ef694c39b52c8dd5cced509d237c Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Wed, 27 Jan 2016 14:59:19 +0000 Subject: [PATCH 186/498] no need to convert to array list with last zip signature --- .../web/reactive/method/InvocableHandlerMethod.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java index 3508c1565c..855d6dc5ca 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java @@ -112,7 +112,7 @@ public class InvocableHandlerMethod extends HandlerMethod { if (!ObjectUtils.isEmpty(providedArgs)) { for (Object providedArg : providedArgs) { if (param.getParameterType().isInstance(providedArg)) { - return Mono.just(providedArg); + return Mono.just(providedArg).log("reactor.resolved"); } } } @@ -123,7 +123,8 @@ public class InvocableHandlerMethod extends HandlerMethod { try { return resolver.resolveArgument(param, exchange) .defaultIfEmpty(NO_VALUE) - .otherwise(ex -> Mono.error(getArgError("Error resolving ", param, ex))); + .otherwise(ex -> Mono.error(getArgError("Error resolving ", param, ex))) + .log("reactor.unresolved"); } catch (Exception ex) { throw getArgError("Error resolving ", param, ex); @@ -131,8 +132,8 @@ public class InvocableHandlerMethod extends HandlerMethod { }) .collect(Collectors.toList()); - return Mono.when(monos).map(args -> - Stream.of(args.toArray()).map(o -> o != NO_VALUE ? o : null).toArray()); + return Mono.when(monos).log("reactor.unresolved").map(args -> + Stream.of(args).map(o -> o != NO_VALUE ? o : null).toArray()); } catch (Throwable ex) { return Mono.error(ex); From 38ab47f8a0d147d3b13cfb601d31f9957599983e Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Thu, 21 Jan 2016 10:33:47 +0100 Subject: [PATCH 187/498] Added DataBuffer abstraction Added DataBuffer and DataBufferAllocator, and provided a default NIO ByteBuffer-based implementation of those, as well as a Netty ByteBuf-based version. --- .../core/io/buffer/DataBuffer.java | 136 +++++++++ .../core/io/buffer/DataBufferAllocator.java | 66 ++++ .../core/io/buffer/DefaultDataBuffer.java | 288 ++++++++++++++++++ .../io/buffer/DefaultDataBufferAllocator.java | 85 ++++++ .../core/io/buffer/NettyDataBuffer.java | 199 ++++++++++++ .../io/buffer/NettyDataBufferAllocator.java | 94 ++++++ .../DataBufferPublisherInputStream.java | 151 +++++++++ .../io/buffer/support/DataBufferUtils.java | 49 +++ .../core/io/buffer/DataBufferTests.java | 207 +++++++++++++ 9 files changed, 1275 insertions(+) create mode 100644 spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DataBuffer.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DataBufferAllocator.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBufferAllocator.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/core/io/buffer/NettyDataBuffer.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/core/io/buffer/NettyDataBufferAllocator.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferPublisherInputStream.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/core/io/buffer/DataBufferTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DataBuffer.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DataBuffer.java new file mode 100644 index 0000000000..2af308228b --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DataBuffer.java @@ -0,0 +1,136 @@ +/* + * 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.io.buffer; + +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; + +/** + * Basic abstraction over byte buffers. + * + *

    Mainly for internal use within the framework; consider Netty's + * {@link io.netty.buffer.ByteBuf} for a more comprehensive byte buffer. + * + * @author Arjen Poutsma + */ +public interface DataBuffer { + + /** + * Gets the byte at the specified index. + * @param index the index + * @return the byte at the specified index + * @throws IndexOutOfBoundsException if the given index is out of bounds + */ + byte get(int index); + + /** + * Returns the number of bytes that can be read from this data buffer. + * @return the readable byte count + */ + int readableByteCount(); + + /** + * Reads a single byte from the current reading position of this data buffer. + * @return the byte at this buffer's current reading position + */ + byte read(); + + /** + * Reads this buffer's data into the specified destination, starting at the current + * reading position of this buffer. + * + * @param destination the array into which the bytes are to be written + * @return this buffer + */ + DataBuffer read(byte[] destination); + + /** + * Reads at most {@code length} bytes of this buffer into the specified destination, + * starting at the current reading position of this buffer. + * @param destination the array into which the bytes are to be written + * @param offset the index within {@code destination} of the first byte to be written + * @param length the maximum number of bytes to be written in {@code destination} + * @return this buffer + */ + DataBuffer read(byte[] destination, int offset, int length); + + /** + * Write a single byte into this buffer at the current writing position. + * @param b the byte to be written + * @return this buffer + */ + DataBuffer write(byte b); + + /** + * Writes the given source into this buffer, startin at the current writing position + * of this buffer. + * @param source the bytes to be written into this buffer + * @return this buffer + */ + DataBuffer write(byte[] source); + + /** + * Writes at most {@code length} bytes of the given source into this buffer, starting + * at the current writing position of this buffer. + * @param source the bytes to be written into this buffer + * @param offset the index withing {@code source} to start writing from + * @param length the maximum number of bytes to be written from {@code source} + * @return this buffer + */ + DataBuffer write(byte[] source, int offset, int length); + + /** + * Writes one or more {@link DataBuffer} to this buffer, starting at the current + * writing position. + * @param buffers the byte buffers to write into this buffer + * @return this buffer + */ + DataBuffer write(DataBuffer... buffers); + + /** + * Writes one or more {@link ByteBuffer} to this buffer, starting at the current + * writing position. + * @param buffers the byte buffers to write into this buffer + * @return this buffer + */ + DataBuffer write(ByteBuffer... buffers); + + /** + * Exposes this buffer's bytes as a {@link ByteBuffer}. Data between this {@code + * DataBuffer} and the returned {@code ByteBuffer} is shared; though changes in the + * returned buffer's {@linkplain ByteBuffer#position() position} will not be reflected + * in the position(s) of this data buffer. + * @return this data buffer as a byte buffer + */ + ByteBuffer asByteBuffer(); + + /** + * Exposes this buffer's data as an {@link InputStream}. Both data and position are + * shared between the returned stream and this data buffer. + * @return this data buffer as an input stream + */ + InputStream asInputStream(); + + /** + * Exposes this buffer's data as an {@link OutputStream}. Both data and position are + * shared between the returned stream and this data buffer. + * @return this data buffer as an output stream + */ + OutputStream asOutputStream(); + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DataBufferAllocator.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DataBufferAllocator.java new file mode 100644 index 0000000000..e4586100f6 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DataBufferAllocator.java @@ -0,0 +1,66 @@ +/* + * 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.io.buffer; + +import java.nio.ByteBuffer; + +/** + * A factory for {@link DataBuffer}s, allowing for allocation of heap-based and direct + * data buffers. + * + * @author Arjen Poutsma + * @see DataBuffer + */ +public interface DataBufferAllocator { + + /** + * Allocates a data buffer of a default initial capacity. Depending on the underlying + * implementation and its configuration, this will be heap-based or direct buffer. + * @return the allocated buffer + */ + DataBuffer allocateBuffer(); + + /** + * Allocates a data buffer of the given initial capacity. Depending on the underlying + * implementation and its configuration, this will be heap-based or direct buffer. + * @param initialCapacity the initial capacity of the buffer to allocateBuffer + * @return the allocated buffer + */ + DataBuffer allocateBuffer(int initialCapacity); + + /** + * Allocates a data buffer of the given initial capacity on the heap. + * @param initialCapacity the initial capacity of the buffer to allocate + * @return the allocated buffer + */ + DataBuffer allocateHeapBuffer(int initialCapacity); + + /** + * Allocates a direct data buffer of the given initial capacity. + * @param initialCapacity the initial capacity of the buffer to allocate + * @return the allocated buffer + */ + DataBuffer allocateDirectBuffer(int initialCapacity); + + /** + * Wraps the given {@link ByteBuffer} in a {@code DataBuffer}. + * @param byteBuffer the NIO byte buffer to wrap + * @return the wrapped buffer + */ + DataBuffer wrap(ByteBuffer byteBuffer); + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java new file mode 100644 index 0000000000..5795c2a74b --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java @@ -0,0 +1,288 @@ +/* + * 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.io.buffer; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.function.Function; + +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * Default implementation of the {@link DataBuffer} interface that uses a {@link + * ByteBuffer} internally, with separate read and write positions. Typically constructed + * using the {@link DefaultDataBufferAllocator}. + * + *

    This class is rather limited; consider using Netty's + * {@link io.netty.buffer.ByteBuf} and {@link NettyDataBuffer} for a more comprehensive byte buffer. + + * @author Arjen Poutsma + * @see DefaultDataBufferAllocator + */ +public class DefaultDataBuffer implements DataBuffer { + + private ByteBuffer byteBuffer; + + private int readPosition; + + private int writePosition; + + /** + * Creates a new {@code DefaultDataBuffer} based on the given {@code ByteBuffer}. Both + * reading and writing position of this buffer are based on the current {@linkplain + * ByteBuffer#position() position} of the given buffer. + * @param byteBuffer the buffer to base this buffer on + */ + DefaultDataBuffer(ByteBuffer byteBuffer) { + this(byteBuffer, byteBuffer.position(), byteBuffer.position()); + } + + DefaultDataBuffer(ByteBuffer byteBuffer, int readPosition, int writePosition) { + Assert.notNull(byteBuffer, "'byteBuffer' must not be null"); + Assert.isTrue(readPosition >= 0, "'readPosition' must be 0 or higher"); + Assert.isTrue(writePosition >= 0, "'writePosition' must be 0 or higher"); + Assert.isTrue(readPosition <= writePosition, + "'readPosition' must be smaller than or equal to 'writePosition'"); + + this.byteBuffer = byteBuffer; + this.readPosition = readPosition; + this.writePosition = writePosition; + } + + /** + * Directly exposes the native {@code ByteBuffer} that this buffer is based on. + * @return the wrapped byte buffer + */ + public ByteBuffer getNativeBuffer() { + return this.byteBuffer; + } + + @Override + public byte get(int index) { + return this.byteBuffer.get(index); + } + + @Override + public int readableByteCount() { + return this.writePosition - this.readPosition; + } + + @Override + public byte read() { + return readInternal(ByteBuffer::get); + } + + @Override + public DefaultDataBuffer read(byte[] destination) { + Assert.notNull(destination, "'destination' must not be null"); + + readInternal(b -> b.get(destination)); + + return this; + } + + @Override + public DefaultDataBuffer read(byte[] destination, int offset, int length) { + Assert.notNull(destination, "'destination' must not be null"); + + readInternal(b -> b.get(destination, offset, length)); + + return this; + } + + /** + * Internal read method that keeps track of the {@link #readPosition} before and after + * applying the given function on {@link #byteBuffer}. + */ + private T readInternal(Function function) { + this.byteBuffer.position(this.readPosition); + T result = function.apply(this.byteBuffer); + this.readPosition = this.byteBuffer.position(); + return result; + } + + @Override + public DefaultDataBuffer write(byte b) { + + ensureExtraCapacity(1); + writeInternal(buffer -> buffer.put(b)); + + return this; + } + + @Override + public DefaultDataBuffer write(byte[] source) { + Assert.notNull(source, "'source' must not be null"); + + ensureExtraCapacity(source.length); + writeInternal(buffer -> buffer.put(source)); + return this; + } + + @Override + public DefaultDataBuffer write(byte[] source, int offset, int length) { + Assert.notNull(source, "'source' must not be null"); + + ensureExtraCapacity(length); + writeInternal(buffer -> buffer.put(source, offset, length)); + return this; + } + + @Override + public DataBuffer write(DataBuffer... buffers) { + if (!ObjectUtils.isEmpty(buffers)) { + ByteBuffer[] byteBuffers = + Arrays.stream(buffers).map(DataBuffer::asByteBuffer) + .toArray(ByteBuffer[]::new); + write(byteBuffers); + } + return this; + } + + @Override + public DefaultDataBuffer write(ByteBuffer... byteBuffers) { + Assert.notEmpty(byteBuffers, "'byteBuffers' must not be empty"); + + int extraCapacity = + Arrays.stream(byteBuffers).mapToInt(ByteBuffer::remaining).sum(); + + ensureExtraCapacity(extraCapacity); + + Arrays.stream(byteBuffers) + .forEach(byteBuffer -> writeInternal(buffer -> buffer.put(byteBuffer))); + + return this; + } + + /** + * Internal write method that keeps track of the {@link #writePosition} befor eand + * after applying the given function on {@link #byteBuffer}. + */ + private T writeInternal(Function function) { + this.byteBuffer.position(this.writePosition); + T result = function.apply(this.byteBuffer); + this.writePosition = this.byteBuffer.position(); + return result; + } + + @Override + public ByteBuffer asByteBuffer() { + ByteBuffer duplicate = this.byteBuffer.duplicate(); + duplicate.position(this.readPosition); + duplicate.limit(this.writePosition); + return duplicate; + } + + @Override + public InputStream asInputStream() { + return new DefaultDataBufferInputStream(); + } + + @Override + public OutputStream asOutputStream() { + return new DefaultDataBufferOutputStream(); + } + + private void ensureExtraCapacity(int extraCapacity) { + int neededCapacity = this.writePosition + extraCapacity; + if (neededCapacity > this.byteBuffer.capacity()) { + grow(neededCapacity); + } + } + + private void grow(int minCapacity) { + ByteBuffer oldBuffer = this.byteBuffer; + ByteBuffer newBuffer = + (oldBuffer.isDirect() ? ByteBuffer.allocateDirect(minCapacity) : + ByteBuffer.allocate(minCapacity)); + + oldBuffer.position(this.readPosition); + newBuffer.put(oldBuffer); + + this.byteBuffer = newBuffer; + oldBuffer.clear(); + } + + @Override + public int hashCode() { + return this.byteBuffer.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + else if (obj instanceof DefaultDataBuffer) { + DefaultDataBuffer other = (DefaultDataBuffer) obj; + return this.byteBuffer.equals(other.byteBuffer); + } + return false; + } + + @Override + public String toString() { + return this.byteBuffer.toString(); + } + + private class DefaultDataBufferInputStream extends InputStream { + + @Override + public int available() throws IOException { + return byteBuffer.limit() - readPosition; + } + + @Override + public int read() { + return readInternal( + buffer -> buffer.hasRemaining() ? buffer.get() & 0xFF : -1); + } + + @Override + public int read(byte[] bytes, int off, int len) throws IOException { + return readInternal(buffer -> { + if (buffer.hasRemaining()) { + int minLen = Math.min(len, buffer.remaining()); + buffer.get(bytes, off, minLen); + return minLen; + } + else { + return -1; + } + }); + } + } + + private class DefaultDataBufferOutputStream extends OutputStream { + + @Override + public void write(int b) throws IOException { + ensureExtraCapacity(1); + writeInternal(buffer -> buffer.put((byte) b)); + } + + @Override + public void write(byte[] bytes, int off, int len) throws IOException { + ensureExtraCapacity(len); + writeInternal(buffer -> buffer.put(bytes, off, len)); + } + } +} diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBufferAllocator.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBufferAllocator.java new file mode 100644 index 0000000000..0f311978f5 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBufferAllocator.java @@ -0,0 +1,85 @@ +/* + * 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.io.buffer; + +import java.nio.ByteBuffer; + +/** + * Default implementation of the {@code DataBufferAllocator} interface. + * + *

    This class is rather limited; consider using Netty's + * {@link io.netty.buffer.ByteBuf} and {@link NettyDataBuffer} for a more comprehensive + * byte buffer. + * @author Arjen Poutsma + */ +public class DefaultDataBufferAllocator implements DataBufferAllocator { + + public static final int DEFAULT_INITIAL_CAPACITY = 256; + + + private final boolean preferDirect; + + /** + * Creates a new {@code DefaultDataBufferAllocator} with default settings. + */ + public DefaultDataBufferAllocator() { + this(false); + } + + /** + * Creates a new {@code DefaultDataBufferAllocator}, indicating whether direct buffers + * should be created by {@link #allocateBuffer(int)}. + * @param preferDirect {@code true} if direct buffers are to be preferred; {@code + * false} otherwise + */ + public DefaultDataBufferAllocator(boolean preferDirect) { + this.preferDirect = preferDirect; + } + + @Override + public DataBuffer allocateBuffer() { + return allocateBuffer(DEFAULT_INITIAL_CAPACITY); + } + + @Override + public DefaultDataBuffer allocateBuffer(int initialCapacity) { + return preferDirect ? allocateDirectBuffer(initialCapacity) : + allocateHeapBuffer(initialCapacity); + } + + @Override + public DefaultDataBuffer allocateHeapBuffer(int initialCapacity) { + return new DefaultDataBuffer(ByteBuffer.allocate(initialCapacity)); + } + + @Override + public DefaultDataBuffer allocateDirectBuffer(int initialCapacity) { + return new DefaultDataBuffer(ByteBuffer.allocateDirect(initialCapacity)); + } + + @Override + public DataBuffer wrap(ByteBuffer byteBuffer) { + ByteBuffer sliced = byteBuffer.slice(); + return new DefaultDataBuffer(sliced, 0, byteBuffer.remaining()); + } + + @Override + public String toString() { + return "DefaultDataBufferFactory"; + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/NettyDataBuffer.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/NettyDataBuffer.java new file mode 100644 index 0000000000..f2a4bcf841 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/NettyDataBuffer.java @@ -0,0 +1,199 @@ +/* + * 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.io.buffer; + +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.util.Arrays; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufInputStream; +import io.netty.buffer.ByteBufOutputStream; +import io.netty.buffer.CompositeByteBuf; +import io.netty.buffer.Unpooled; + +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * Implementation of the {@code DataBuffer} interface that wraps a Netty {@link ByteBuf}. + * Typically constructed using the {@link NettyDataBufferAllocator}. + * + * @author Arjen Poutsma + */ +public class NettyDataBuffer implements DataBuffer { + + private ByteBuf byteBuf; + + /** + * Creates a new {@code NettyDataBuffer} based on the given {@code ByteBuff}. + * @param byteBuf the buffer to base this buffer on + */ + public NettyDataBuffer(ByteBuf byteBuf) { + Assert.notNull(byteBuf, "'byteBuf' must not be null"); + + this.byteBuf = byteBuf; + } + + /** + * Directly exposes the native {@code ByteBuf} that this buffer is based on. + * @return the wrapped byte buffer + */ + public ByteBuf getNativeBuffer() { + return this.byteBuf; + } + + @Override + public byte get(int index) { + return this.byteBuf.getByte(index); + } + + @Override + public int readableByteCount() { + return this.byteBuf.readableBytes(); + } + + @Override + public byte read() { + return this.byteBuf.readByte(); + } + + @Override + public NettyDataBuffer read(byte[] destination) { + this.byteBuf.readBytes(destination); + return this; + } + + @Override + public NettyDataBuffer read(byte[] destination, int offset, int length) { + this.byteBuf.readBytes(destination, offset, length); + return this; + } + + @Override + public NettyDataBuffer write(byte b) { + this.byteBuf.writeByte(b); + return this; + } + + @Override + public NettyDataBuffer write(byte[] source) { + this.byteBuf.writeBytes(source); + return this; + } + + @Override + public NettyDataBuffer write(byte[] source, int offset, int length) { + this.byteBuf.writeBytes(source, offset, length); + return this; + } + + @Override + public NettyDataBuffer write(DataBuffer... buffers) { + if (!ObjectUtils.isEmpty(buffers)) { + if (buffers[0] instanceof NettyDataBuffer) { + NettyDataBuffer[] copy = + Arrays.copyOf(buffers, buffers.length, NettyDataBuffer[].class); + + ByteBuf[] nativeBuffers = + Arrays.stream(copy).map(NettyDataBuffer::getNativeBuffer) + .toArray(ByteBuf[]::new); + + write(nativeBuffers); + } + else { + ByteBuffer[] byteBuffers = + Arrays.stream(buffers).map(DataBuffer::asByteBuffer) + .toArray(ByteBuffer[]::new); + write(byteBuffers); + } + } + return this; + } + + @Override + public NettyDataBuffer write(ByteBuffer... buffers) { + Assert.notNull(buffers, "'buffers' must not be null"); + + ByteBuf[] wrappedBuffers = Arrays.stream(buffers).map(Unpooled::wrappedBuffer) + .toArray(ByteBuf[]::new); + + return write(wrappedBuffers); + } + + /** + * Writes one or more Netty {@link ByteBuf}s to this buffer, starting at the current + * writing position. + * @param byteBufs the buffers to write into this buffer + * @return this buffer + */ + public NettyDataBuffer write(ByteBuf... byteBufs) { + Assert.notNull(byteBufs, "'byteBufs' must not be null"); + + CompositeByteBuf composite = + new CompositeByteBuf(this.byteBuf.alloc(), this.byteBuf.isDirect(), + byteBufs.length + 1); + composite.addComponent(this.byteBuf); + Arrays.stream(byteBufs).forEach(composite::addComponent); + + int writerIndex = this.byteBuf.readableBytes() + + Arrays.stream(byteBufs).mapToInt(ByteBuf::readableBytes).sum(); + composite.writerIndex(writerIndex); + + this.byteBuf = composite; + + return this; + } + + @Override + public ByteBuffer asByteBuffer() { + return this.byteBuf.nioBuffer(); + } + + @Override + public InputStream asInputStream() { + return new ByteBufInputStream(this.byteBuf); + } + + @Override + public OutputStream asOutputStream() { + return new ByteBufOutputStream(this.byteBuf); + } + + @Override + public int hashCode() { + return this.byteBuf.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + else if (obj instanceof NettyDataBuffer) { + NettyDataBuffer other = (NettyDataBuffer) obj; + return this.byteBuf.equals(other.byteBuf); + } + return false; + } + + @Override + public String toString() { + return this.byteBuf.toString(); + } +} diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/NettyDataBufferAllocator.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/NettyDataBufferAllocator.java new file mode 100644 index 0000000000..6eea0f3298 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/NettyDataBufferAllocator.java @@ -0,0 +1,94 @@ +/* + * 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.io.buffer; + +import java.nio.ByteBuffer; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; + +import org.springframework.util.Assert; + +/** + * Implemtation of the {@code DataBufferAllocator} interface based on a Netty + * {@link ByteBufAllocator}. + * + * @author Arjen Poutsma + * @see io.netty.buffer.PooledByteBufAllocator + * @see io.netty.buffer.UnpooledByteBufAllocator + */ +public class NettyDataBufferAllocator implements DataBufferAllocator { + + private final ByteBufAllocator byteBufAllocator; + + /** + * Creates a new {@code NettyDataBufferAllocator} based on the given allocator. + * @param byteBufAllocator the allocator to use + * @see io.netty.buffer.PooledByteBufAllocator + * @see io.netty.buffer.UnpooledByteBufAllocator + */ + public NettyDataBufferAllocator(ByteBufAllocator byteBufAllocator) { + Assert.notNull(byteBufAllocator, "'byteBufAllocator' must not be null"); + + this.byteBufAllocator = byteBufAllocator; + } + + @Override + public NettyDataBuffer allocateBuffer() { + ByteBuf byteBuf = this.byteBufAllocator.buffer(); + return new NettyDataBuffer(byteBuf); + } + + @Override + public NettyDataBuffer allocateBuffer(int initialCapacity) { + ByteBuf byteBuf = this.byteBufAllocator.buffer(initialCapacity); + return new NettyDataBuffer(byteBuf); + } + + @Override + public NettyDataBuffer allocateHeapBuffer(int initialCapacity) { + ByteBuf byteBuf = this.byteBufAllocator.heapBuffer(initialCapacity); + return new NettyDataBuffer(byteBuf); + } + + @Override + public NettyDataBuffer allocateDirectBuffer(int initialCapacity) { + ByteBuf byteBuf = this.byteBufAllocator.directBuffer(initialCapacity); + return new NettyDataBuffer(byteBuf); + } + + @Override + public NettyDataBuffer wrap(ByteBuffer byteBuffer) { + ByteBuf byteBuf = Unpooled.wrappedBuffer(byteBuffer); + return new NettyDataBuffer(byteBuf); + } + + /** + * Wraps the given Netty {@link ByteBuf} in a {@code NettyDataBuffer}. + * @param byteBuf the Netty byte buffer to wrap + * @return the wrapped buffer + */ + public NettyDataBuffer wrap(ByteBuf byteBuf) { + return new NettyDataBuffer(byteBuf); + } + + @Override + public String toString() { + return "NettyDataBufferAllocator (" + this.byteBufAllocator + ")"; + } +} diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferPublisherInputStream.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferPublisherInputStream.java new file mode 100644 index 0000000000..abcd0ddfea --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferPublisherInputStream.java @@ -0,0 +1,151 @@ +/* + * 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.io.buffer.support; + +import java.io.IOException; +import java.io.InputStream; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscription; +import reactor.rx.Stream; + +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.util.Assert; + +/** + * @author Arjen Poutsma + */ +class DataBufferPublisherInputStream extends InputStream { + + private final AtomicBoolean completed = new AtomicBoolean(); + + private final BlockingQueue queue; + + private InputStream currentStream; + + /** + * Creates a new {@code ByteArrayPublisherInputStream} based on the given publisher. + * @param publisher the publisher to use + */ + public DataBufferPublisherInputStream(Publisher publisher) { + this(publisher, 1); + } + + /** + * Creates a new {@code ByteArrayPublisherInputStream} based on the given publisher. + * @param publisher the publisher to use + * @param requestSize the {@linkplain Subscription#request(long) request size} to use + * on the publisher bound to Integer MAX + */ + public DataBufferPublisherInputStream(Publisher publisher, + int requestSize) { + Assert.notNull(publisher, "'publisher' must not be null"); + + // TODO Avoid using Reactor Stream, it should not be a mandatory dependency of Spring Reactive + this.queue = Stream.from(publisher).toBlockingQueue(requestSize); + } + + @Override + public int available() throws IOException { + if (completed.get()) { + return 0; + } + InputStream is = currentStream(); + return is != null ? is.available() : 0; + } + + @Override + public int read() throws IOException { + if (completed.get()) { + return -1; + } + InputStream is = currentStream(); + while (is != null) { + int ch = is.read(); + if (ch != -1) { + return ch; + } + else { + is = currentStream(); + } + } + return -1; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + if (completed.get()) { + return -1; + } + InputStream is = currentStream(); + if (is == null) { + return -1; + } + else if (b == null) { + throw new NullPointerException(); + } + else if (off < 0 || len < 0 || len > b.length - off) { + throw new IndexOutOfBoundsException(); + } + else if (len == 0) { + return 0; + } + do { + int n = is.read(b, off, len); + if (n > 0) { + return n; + } + else { + is = currentStream(); + } + } + while (is != null); + + return -1; + } + + private InputStream currentStream() throws IOException { + try { + if (this.currentStream != null && this.currentStream.available() > 0) { + return this.currentStream; + } + else { + // take() blocks until next or complete() then return null, + // but that's OK since this is a *blocking* InputStream + DataBuffer signal = this.queue.take(); + if (signal == null) { + this.completed.set(true); + return null; + } + this.currentStream = signal.asInputStream(); + return this.currentStream; + } + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + catch (Throwable error) { + this.completed.set(true); + throw new IOException(error); + } + throw new IOException(); + } + + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java new file mode 100644 index 0000000000..4839958233 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java @@ -0,0 +1,49 @@ +/* + * 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.io.buffer.support; + +import java.io.InputStream; + +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; + +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.util.Assert; + +/** + * @author Arjen Poutsma + */ +public abstract class DataBufferUtils { + + public static Flux toPublisher(DataBuffer buffer) { + Assert.notNull(buffer, "'buffer' must not be null"); + + byte[] bytes1 = new byte[buffer.readableByteCount()]; + buffer.read(bytes1); + + Byte[] bytes2 = new Byte[bytes1.length]; + for (int i = 0; i < bytes1.length; i++) { + bytes2[i] = bytes1[i]; + } + return Flux.fromArray(bytes2); + } + + public static InputStream toInputStream(Publisher publisher) { + return new DataBufferPublisherInputStream(publisher); + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/DataBufferTests.java b/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/DataBufferTests.java new file mode 100644 index 0000000000..8c104a6069 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/DataBufferTests.java @@ -0,0 +1,207 @@ +/* + * 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.io.buffer; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.util.Arrays; + +import io.netty.buffer.PooledByteBufAllocator; +import io.netty.buffer.UnpooledByteBufAllocator; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +/** + * @author Arjen Poutsma + */ +@RunWith(Parameterized.class) +public class DataBufferTests { + + @Parameterized.Parameter(0) + public DataBufferAllocator allocator; + + @Parameterized.Parameter(1) + public boolean direct; + + @Parameterized.Parameters(name = "{0} - direct: {1}") + public static Object[][] buffers() { + + return new Object[][]{ + {new NettyDataBufferAllocator(new UnpooledByteBufAllocator(false)), true}, + {new NettyDataBufferAllocator(new UnpooledByteBufAllocator(false)), + false}, + {new NettyDataBufferAllocator(new PooledByteBufAllocator(false)), true}, + {new NettyDataBufferAllocator(new PooledByteBufAllocator(false)), false}, + {new DefaultDataBufferAllocator(), true}, + {new DefaultDataBufferAllocator(), false}}; + } + + private DataBuffer createDataBuffer(int capacity) { + return direct ? allocator.allocateDirectBuffer(capacity) : + allocator.allocateHeapBuffer(capacity); + } + + @Test + public void writeAndRead() { + + DataBuffer buffer = createDataBuffer(5); + buffer.write(new byte[]{'a', 'b', 'c'}); + + int ch = buffer.read(); + assertEquals('a', ch); + + buffer.write((byte) 'd'); + buffer.write((byte) 'e'); + + byte[] result = new byte[4]; + buffer.read(result); + + assertArrayEquals(new byte[]{'b', 'c', 'd', 'e'}, result); + } + + @Test + public void inputStream() throws IOException { + byte[] data = new byte[]{'a', 'b', 'c', 'd', 'e'}; + + DataBuffer buffer = createDataBuffer(4); + buffer.write(data); + + buffer.read(); // readIndex++ + + InputStream inputStream = buffer.asInputStream(); + + int available = inputStream.available(); + assertEquals(4, available); + + int result = inputStream.read(); + assertEquals('b', result); + + available = inputStream.available(); + assertEquals(3, available); + + byte[] bytes = new byte[2]; + int len = inputStream.read(bytes); + assertEquals(2, len); + assertArrayEquals(new byte[]{'c', 'd'}, bytes); + + Arrays.fill(bytes, (byte) 0); + len = inputStream.read(bytes); + assertEquals(1, len); + assertArrayEquals(new byte[]{'e', (byte) 0}, bytes); + } + + @Test + public void outputStream() throws IOException { + DataBuffer buffer = createDataBuffer(4); + buffer.write((byte) 'a'); + + OutputStream outputStream = buffer.asOutputStream(); + outputStream.write(new byte[]{'b', 'c', 'd'}); + + buffer.write((byte) 'e'); + + byte[] bytes = new byte[5]; + buffer.read(bytes); + assertArrayEquals(new byte[]{'a', 'b', 'c', 'd', 'e'}, bytes); + } + + @Test + public void expand() { + DataBuffer buffer = createDataBuffer(1); + buffer.write((byte) 'a'); + buffer.write((byte) 'b'); + + byte[] result = new byte[2]; + buffer.read(result); + assertArrayEquals(new byte[]{'a', 'b'}, result); + + buffer.write(new byte[]{'c', 'd'}); + + result = new byte[2]; + buffer.read(result); + assertArrayEquals(new byte[]{'c', 'd'}, result); + } + + @Test + public void writeByteBuffer() { + DataBuffer buffer1 = createDataBuffer(1); + buffer1.write((byte) 'a'); + ByteBuffer buffer2 = createByteBuffer(2); + buffer2.put((byte) 'b'); + buffer2.flip(); + ByteBuffer buffer3 = createByteBuffer(3); + buffer3.put((byte) 'c'); + buffer3.flip(); + + buffer1.write(buffer2, buffer3); + buffer1.write((byte) 'd'); // make sure the write index is correctly set + + assertEquals(4, buffer1.readableByteCount()); + byte[] result = new byte[4]; + buffer1.read(result); + + assertArrayEquals(new byte[]{'a', 'b', 'c', 'd'}, result); + } + + @Test + public void writeDataBuffer() { + DataBuffer buffer1 = createDataBuffer(1); + buffer1.write((byte) 'a'); + DataBuffer buffer2 = createDataBuffer(2); + buffer2.write((byte) 'b'); + DataBuffer buffer3 = createDataBuffer(3); + buffer3.write((byte) 'c'); + + buffer1.write(buffer2, buffer3); + buffer1.write((byte) 'd'); // make sure the write index is correctly set + + assertEquals(4, buffer1.readableByteCount()); + byte[] result = new byte[4]; + buffer1.read(result); + + assertArrayEquals(new byte[]{'a', 'b', 'c', 'd'}, result); + } + + private ByteBuffer createByteBuffer(int capacity) { + return direct ? ByteBuffer.allocateDirect(capacity) : + ByteBuffer.allocate(capacity); + } + + @Test + public void asByteBuffer() { + DataBuffer buffer = createDataBuffer(4); + buffer.write(new byte[]{'a', 'b', 'c'}); + buffer.read(); // skip a + + ByteBuffer result = buffer.asByteBuffer(); + + buffer.write((byte) 'd'); + assertEquals(2, result.remaining()); + byte[] resultBytes = new byte[2]; + buffer.read(resultBytes); + assertArrayEquals(new byte[]{'b', 'c'}, resultBytes); + + } + + +} \ No newline at end of file From 2981b5e6e88b25a4f9b743e34e815e2bac6cfc37 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Tue, 26 Jan 2016 14:45:08 +0100 Subject: [PATCH 188/498] Updated Encoder and Decoder to use DataBuffer --- .../springframework/core/codec/Decoder.java | 8 +- .../springframework/core/codec/Encoder.java | 5 +- .../support/AbstractAllocatingEncoder.java | 42 ++++++++++ .../support/AbstractRawByteStreamDecoder.java | 78 +++++++++++-------- .../core/codec/support/ByteBufferDecoder.java | 8 +- .../core/codec/support/ByteBufferEncoder.java | 20 +++-- .../codec/support/JacksonJsonDecoder.java | 18 ++--- .../codec/support/JacksonJsonEncoder.java | 41 +++++----- .../core/codec/support/Jaxb2Decoder.java | 11 +-- .../core/codec/support/Jaxb2Encoder.java | 25 +++--- .../core/codec/support/JsonObjectDecoder.java | 45 ++++++----- .../core/codec/support/JsonObjectEncoder.java | 54 ++++++------- .../core/codec/support/StringDecoder.java | 30 ++++--- .../core/codec/support/StringEncoder.java | 21 +++-- .../support/AbstractAllocatingTestCase.java | 59 ++++++++++++++ .../support}/ByteBufferDecoderTests.java | 29 ++++--- .../support}/ByteBufferEncoderTests.java | 51 ++++++++---- .../support}/JacksonJsonDecoderTests.java | 13 ++-- .../support}/JacksonJsonEncoderTests.java | 20 +++-- .../codec/support}/Jaxb2DecoderTests.java | 13 ++-- .../codec/support}/Jaxb2EncoderTests.java | 20 +++-- .../support}/JsonObjectDecoderTests.java | 67 ++++++++-------- .../support}/JsonObjectEncoderTests.java | 60 +++++++------- .../codec => core/codec/support}/Pojo.java | 20 ++++- .../codec/support}/StringDecoderTests.java | 26 ++++--- .../codec/support}/StringEncoderTests.java | 22 ++++-- 26 files changed, 509 insertions(+), 297 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractAllocatingEncoder.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/core/codec/support/AbstractAllocatingTestCase.java rename spring-web-reactive/src/test/java/org/springframework/{reactive/codec/decoder => core/codec/support}/ByteBufferDecoderTests.java (68%) rename spring-web-reactive/src/test/java/org/springframework/{reactive/codec/encoder => core/codec/support}/ByteBufferEncoderTests.java (51%) rename spring-web-reactive/src/test/java/org/springframework/{reactive/codec/decoder => core/codec/support}/JacksonJsonDecoderTests.java (79%) rename spring-web-reactive/src/test/java/org/springframework/{reactive/codec/encoder => core/codec/support}/JacksonJsonEncoderTests.java (80%) rename spring-web-reactive/src/test/java/org/springframework/{reactive/codec/decoder => core/codec/support}/Jaxb2DecoderTests.java (78%) rename spring-web-reactive/src/test/java/org/springframework/{reactive/codec/encoder => core/codec/support}/Jaxb2EncoderTests.java (83%) rename spring-web-reactive/src/test/java/org/springframework/{reactive/codec/decoder => core/codec/support}/JsonObjectDecoderTests.java (54%) rename spring-web-reactive/src/test/java/org/springframework/{reactive/codec/encoder => core/codec/support}/JsonObjectEncoderTests.java (62%) rename spring-web-reactive/src/test/java/org/springframework/{reactive/codec => core/codec/support}/Pojo.java (71%) rename spring-web-reactive/src/test/java/org/springframework/{reactive/codec/decoder => core/codec/support}/StringDecoderTests.java (80%) rename spring-web-reactive/src/test/java/org/springframework/{reactive/codec/encoder => core/codec/support}/StringEncoderTests.java (77%) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/Decoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/Decoder.java index 7b3325110a..1b3d0410bb 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/Decoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/Decoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -16,13 +16,13 @@ package org.springframework.core.codec; -import java.nio.ByteBuffer; import java.util.List; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import org.springframework.core.ResolvableType; +import org.springframework.core.io.buffer.DataBuffer; import org.springframework.util.MimeType; /** @@ -43,14 +43,14 @@ public interface Decoder { boolean canDecode(ResolvableType type, MimeType mimeType, Object... hints); /** - * Decode an input {@link ByteBuffer} stream to an output stream of {@code T}. + * Decode an input {@link DataBuffer} stream to an output stream of {@code T}. * @param inputStream the input stream to process. * @param type the stream element type to process. * @param mimeType the mime type to process. * @param hints Additional information about how to do decode, optional. * @return the output stream */ - Flux decode(Publisher inputStream, ResolvableType type, + Flux decode(Publisher inputStream, ResolvableType type, MimeType mimeType, Object... hints); /** diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/Encoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/Encoder.java index 761ceb9b4b..0f82c73081 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/Encoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/Encoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -23,6 +23,7 @@ import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import org.springframework.core.ResolvableType; +import org.springframework.core.io.buffer.DataBuffer; import org.springframework.util.MimeType; /** @@ -50,7 +51,7 @@ public interface Encoder { * @param hints Additional information about how to do decode, optional. * @return the output stream */ - Flux encode(Publisher inputStream, ResolvableType type, + Flux encode(Publisher inputStream, ResolvableType type, MimeType mimeType, Object... hints); /** diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractAllocatingEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractAllocatingEncoder.java new file mode 100644 index 0000000000..0275931fc8 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractAllocatingEncoder.java @@ -0,0 +1,42 @@ +/* + * 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.support; + +import org.springframework.core.io.buffer.DataBufferAllocator; +import org.springframework.util.Assert; +import org.springframework.util.MimeType; + +/** + * @author Arjen Poutsma + */ +public abstract class AbstractAllocatingEncoder extends AbstractEncoder { + + private final DataBufferAllocator allocator; + + public AbstractAllocatingEncoder(DataBufferAllocator allocator, + MimeType... supportedMimeTypes) { + super(supportedMimeTypes); + Assert.notNull(allocator, "'allocator' must not be null"); + + this.allocator = allocator; + } + + public DataBufferAllocator allocator() { + return allocator; + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractRawByteStreamDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractRawByteStreamDecoder.java index a77141afca..7fc77d8137 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractRawByteStreamDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractRawByteStreamDecoder.java @@ -16,7 +16,6 @@ package org.springframework.core.codec.support; -import java.nio.ByteBuffer; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; import java.util.concurrent.atomic.AtomicLongFieldUpdater; @@ -28,6 +27,9 @@ import reactor.core.util.BackpressureUtils; import org.springframework.core.ResolvableType; import org.springframework.core.codec.Decoder; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferAllocator; +import org.springframework.util.Assert; import org.springframework.util.MimeType; /** @@ -38,12 +40,19 @@ import org.springframework.util.MimeType; */ public abstract class AbstractRawByteStreamDecoder extends AbstractDecoder { - public AbstractRawByteStreamDecoder(MimeType... supportedMimeTypes) { + private final DataBufferAllocator allocator; + + public AbstractRawByteStreamDecoder(DataBufferAllocator allocator, + MimeType... supportedMimeTypes) { super(supportedMimeTypes); + Assert.notNull(allocator, "'allocator' must not be null"); + + this.allocator = allocator; } @Override - public Flux decode(Publisher inputStream, ResolvableType type, MimeType mimeType, Object... hints) { + public Flux decode(Publisher inputStream, ResolvableType type, + MimeType mimeType, Object... hints) { return decodeInternal(Flux.from(inputStream).lift(bbs -> subscriberBarrier(bbs)), type, mimeType, hints); @@ -55,17 +64,20 @@ public abstract class AbstractRawByteStreamDecoder extends AbstractDecoder *

    Implementations should provide their own {@link SubscriberBarrier} or use one of the * provided implementations by this class */ - public abstract SubscriberBarrier subscriberBarrier(Subscriber subscriber); + public abstract SubscriberBarrier subscriberBarrier( + Subscriber subscriber); - public abstract Flux decodeInternal(Publisher inputStream, ResolvableType type + public abstract Flux decodeInternal(Publisher inputStream, + ResolvableType type , MimeType mimeType, Object... hints); /** * {@code SubscriberBarrier} implementation that buffers all received elements and emits a single - * {@code ByteBuffer} once the incoming stream has been completed + * {@code DataBuffer} once the incoming stream has been completed */ - public static class ReduceSingleByteStreamBarrier extends SubscriberBarrier { + public static class ReduceSingleByteStreamBarrier + extends SubscriberBarrier { @SuppressWarnings("rawtypes") static final AtomicLongFieldUpdater REQUESTED = @@ -74,16 +86,16 @@ public abstract class AbstractRawByteStreamDecoder extends AbstractDecoder static final AtomicIntegerFieldUpdater TERMINATED = AtomicIntegerFieldUpdater.newUpdater(ReduceSingleByteStreamBarrier.class, "terminated"); - private volatile long requested; private volatile int terminated; - private ByteBuffer buffer; + private DataBuffer buffer; - public ReduceSingleByteStreamBarrier(Subscriber subscriber) { + public ReduceSingleByteStreamBarrier(Subscriber subscriber, + DataBufferAllocator allocator) { super(subscriber); - this.buffer = ByteBuffer.allocate(0); + this.buffer = allocator.allocateBuffer(); } @Override @@ -108,15 +120,12 @@ public abstract class AbstractRawByteStreamDecoder extends AbstractDecoder * TODO: when available, wrap buffers with a single buffer and avoid copying data for every method call. */ @Override - protected void doNext(ByteBuffer byteBuffer) { - this.buffer = ByteBuffer.allocate(this.buffer.capacity() + byteBuffer.capacity()) - .put(this.buffer).put(byteBuffer); - this.buffer.flip(); + protected void doNext(DataBuffer dataBuffer) { + this.buffer.write(dataBuffer); } protected void drainLast() { if (BackpressureUtils.getAndSub(REQUESTED, this, 1L) > 0) { - this.buffer.flip(); subscriber.onNext(this.buffer); super.doComplete(); } @@ -127,7 +136,8 @@ public abstract class AbstractRawByteStreamDecoder extends AbstractDecoder * {@code SubscriberBarrier} implementation that splits incoming elements * using line return delimiters: {@code "\n"} and {@code "\r\n"} */ - public static class SplitLinesByteStreamBarrier extends SubscriberBarrier { + public static class SplitLinesByteStreamBarrier + extends SubscriberBarrier { @SuppressWarnings("rawtypes") static final AtomicLongFieldUpdater REQUESTED = @@ -136,16 +146,20 @@ public abstract class AbstractRawByteStreamDecoder extends AbstractDecoder static final AtomicIntegerFieldUpdater TERMINATED = AtomicIntegerFieldUpdater.newUpdater(SplitLinesByteStreamBarrier.class, "terminated"); + private final DataBufferAllocator allocator; + private volatile long requested; private volatile int terminated; - private ByteBuffer buffer; + private DataBuffer buffer; - public SplitLinesByteStreamBarrier(Subscriber subscriber) { + public SplitLinesByteStreamBarrier(Subscriber subscriber, + DataBufferAllocator allocator) { super(subscriber); - this.buffer = ByteBuffer.allocate(0); + this.allocator = allocator; + this.buffer = allocator.allocateBuffer(); } @Override @@ -170,19 +184,20 @@ public abstract class AbstractRawByteStreamDecoder extends AbstractDecoder * TODO: when available, wrap buffers with a single buffer and avoid copying data for every method call. */ @Override - protected void doNext(ByteBuffer byteBuffer) { - this.buffer = ByteBuffer.allocate(this.buffer.capacity() + byteBuffer.capacity()) - .put(this.buffer).put(byteBuffer); + protected void doNext(DataBuffer dataBuffer) { + this.buffer.write(dataBuffer); while (REQUESTED.get(this) > 0) { int separatorIndex = findEndOfLine(this.buffer); if (separatorIndex != -1) { if (BackpressureUtils.getAndSub(REQUESTED, this, 1L) > 0) { byte[] message = new byte[separatorIndex]; - this.buffer.get(message); + this.buffer.read(message); consumeSeparator(this.buffer); - this.buffer = this.buffer.slice(); - super.doNext(ByteBuffer.wrap(message)); +// this.buffer = this.buffer.slice(); + DataBuffer buffer2 = allocator.allocateBuffer(message.length); + buffer2.write(message); + super.doNext(buffer2); } } else { @@ -191,9 +206,9 @@ public abstract class AbstractRawByteStreamDecoder extends AbstractDecoder } } - protected int findEndOfLine(ByteBuffer buffer) { + protected int findEndOfLine(DataBuffer buffer) { - final int n = buffer.limit(); + final int n = buffer.readableByteCount(); for (int i = 0; i < n; i++) { final byte b = buffer.get(i); if (b == '\n') { @@ -207,16 +222,15 @@ public abstract class AbstractRawByteStreamDecoder extends AbstractDecoder return -1; } - protected void consumeSeparator(ByteBuffer buffer) { - byte sep = buffer.get(); + protected void consumeSeparator(DataBuffer buffer) { + byte sep = buffer.read(); if (sep == '\r') { - buffer.get(); + buffer.read(); } } protected void drainLast() { if (BackpressureUtils.getAndSub(REQUESTED, this, 1L) > 0) { - this.buffer.flip(); subscriber.onNext(this.buffer); super.doComplete(); } diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferDecoder.java index 42595a351c..bea4ad9a1f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferDecoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -22,6 +22,7 @@ import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import org.springframework.core.ResolvableType; +import org.springframework.core.io.buffer.DataBuffer; import org.springframework.util.MimeType; import org.springframework.util.MimeTypeUtils; @@ -43,10 +44,9 @@ public class ByteBufferDecoder extends AbstractDecoder { } @Override - public Flux decode(Publisher inputStream, ResolvableType type, + public Flux decode(Publisher inputStream, ResolvableType type, MimeType mimeType, Object... hints) { - - return Flux.from(inputStream); + return Flux.from(inputStream).map(DataBuffer::asByteBuffer); } } \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferEncoder.java index 7a76eb9232..3b72c7e355 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferEncoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -22,17 +22,18 @@ 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.DataBufferAllocator; import org.springframework.util.MimeType; import org.springframework.util.MimeTypeUtils; /** * @author Sebastien Deleuze */ -public class ByteBufferEncoder extends AbstractEncoder { +public class ByteBufferEncoder extends AbstractAllocatingEncoder { - - public ByteBufferEncoder() { - super(MimeTypeUtils.ALL); + public ByteBufferEncoder(DataBufferAllocator allocator) { + super(allocator, MimeTypeUtils.ALL); } @@ -43,11 +44,16 @@ public class ByteBufferEncoder extends AbstractEncoder { } @Override - public Flux encode(Publisher inputStream, ResolvableType type, + public Flux encode(Publisher inputStream, + ResolvableType type, MimeType mimeType, Object... hints) { //noinspection unchecked - return Flux.from(inputStream); + return Flux.from(inputStream).map(byteBuffer -> { + DataBuffer dataBuffer = allocator().allocateBuffer(byteBuffer.remaining()); + dataBuffer.write(byteBuffer); + return dataBuffer; + }); } } \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonDecoder.java index e60a59a6cf..05f6dbf2c5 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonDecoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -17,7 +17,6 @@ package org.springframework.core.codec.support; import java.io.IOException; -import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import com.fasterxml.jackson.databind.ObjectMapper; @@ -28,7 +27,7 @@ import reactor.core.publisher.Flux; import org.springframework.core.ResolvableType; import org.springframework.core.codec.CodecException; import org.springframework.core.codec.Decoder; -import org.springframework.util.ByteBufferInputStream; +import org.springframework.core.io.buffer.DataBuffer; import org.springframework.util.MimeType; @@ -42,39 +41,38 @@ public class JacksonJsonDecoder extends AbstractDecoder { private final ObjectMapper mapper; - private Decoder preProcessor; + private Decoder preProcessor; public JacksonJsonDecoder() { this(new ObjectMapper(), null); } - public JacksonJsonDecoder(Decoder preProcessor) { + public JacksonJsonDecoder(Decoder preProcessor) { this(new ObjectMapper(), preProcessor); } - public JacksonJsonDecoder(ObjectMapper mapper, Decoder preProcessor) { + public JacksonJsonDecoder(ObjectMapper mapper, Decoder preProcessor) { super(new MimeType("application", "json", StandardCharsets.UTF_8), new MimeType("application", "*+json", StandardCharsets.UTF_8)); this.mapper = mapper; this.preProcessor = preProcessor; } - @Override - public Flux decode(Publisher inputStream, ResolvableType type, + public Flux decode(Publisher inputStream, ResolvableType type, MimeType mimeType, Object... hints) { ObjectReader reader = this.mapper.readerFor(type.getRawClass()); - Flux stream = Flux.from(inputStream); + Flux stream = Flux.from(inputStream); if (this.preProcessor != null) { stream = this.preProcessor.decode(inputStream, type, mimeType, hints); } return stream.map(content -> { try { - return reader.readValue(new ByteBufferInputStream(content)); + return reader.readValue(content.asInputStream()); } catch (IOException e) { throw new CodecException("Error while reading the data", e); diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonEncoder.java index a42d655e95..6e86446f8a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonEncoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -17,19 +17,19 @@ package org.springframework.core.codec.support; import java.io.IOException; -import java.nio.ByteBuffer; +import java.io.OutputStream; import java.nio.charset.StandardCharsets; import com.fasterxml.jackson.databind.ObjectMapper; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import reactor.io.buffer.Buffer; import org.springframework.core.ResolvableType; import org.springframework.core.codec.CodecException; import org.springframework.core.codec.Encoder; -import org.springframework.util.BufferOutputStream; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferAllocator; import org.springframework.util.MimeType; /** @@ -38,50 +38,49 @@ import org.springframework.util.MimeType; * @author Sebastien Deleuze * @see JacksonJsonDecoder */ -public class JacksonJsonEncoder extends AbstractEncoder { +public class JacksonJsonEncoder extends AbstractAllocatingEncoder { private final ObjectMapper mapper; - private Encoder postProcessor; + private Encoder postProcessor; - - public JacksonJsonEncoder() { - this(new ObjectMapper(), null); + public JacksonJsonEncoder(DataBufferAllocator allocator) { + this(allocator, new ObjectMapper(), null); } - public JacksonJsonEncoder(Encoder postProcessor) { - this(new ObjectMapper(), postProcessor); + public JacksonJsonEncoder(DataBufferAllocator allocator, + Encoder postProcessor) { + this(allocator, new ObjectMapper(), postProcessor); } - - public JacksonJsonEncoder(ObjectMapper mapper, Encoder postProcessor) { - super(new MimeType("application", "json", StandardCharsets.UTF_8), + public JacksonJsonEncoder(DataBufferAllocator allocator, ObjectMapper mapper, + Encoder postProcessor) { + super(allocator, new MimeType("application", "json", StandardCharsets.UTF_8), new MimeType("application", "*+json", StandardCharsets.UTF_8)); this.mapper = mapper; this.postProcessor = postProcessor; } @Override - public Flux encode(Publisher inputStream, + public Flux encode(Publisher inputStream, ResolvableType type, MimeType mimeType, Object... hints) { - Publisher stream = (inputStream instanceof Mono ? + Publisher stream = (inputStream instanceof Mono ? ((Mono)inputStream).map(this::serialize) : Flux.from(inputStream).map(this::serialize)); return (this.postProcessor == null ? Flux.from(stream) : this.postProcessor.encode(stream, type, mimeType, hints)); } - private ByteBuffer serialize(Object value) { - Buffer buffer = new Buffer(); - BufferOutputStream outputStream = new BufferOutputStream(buffer); + private DataBuffer serialize(Object value) { + DataBuffer buffer = allocator().allocateBuffer(); + OutputStream outputStream = buffer.asOutputStream(); try { this.mapper.writeValue(outputStream, value); } catch (IOException e) { throw new CodecException("Error while writing the data", e); } - buffer.flip(); - return buffer.byteBuffer(); + return buffer; } } diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Decoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Decoder.java index 94fda4c8ca..ec1e260fe9 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Decoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Decoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -16,7 +16,6 @@ package org.springframework.core.codec.support; -import java.nio.ByteBuffer; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import javax.xml.bind.JAXBContext; @@ -38,8 +37,9 @@ import reactor.core.publisher.Flux; import org.springframework.core.ResolvableType; import org.springframework.core.codec.CodecException; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.support.DataBufferUtils; import org.springframework.util.Assert; -import org.springframework.util.ByteBufferPublisherInputStream; import org.springframework.util.MimeType; import org.springframework.util.MimeTypeUtils; @@ -60,12 +60,13 @@ public class Jaxb2Decoder extends AbstractDecoder { @Override - public Flux decode(Publisher inputStream, ResolvableType type, + public Flux decode(Publisher inputStream, ResolvableType type, MimeType mimeType, Object... hints) { Class outputClass = type.getRawClass(); try { - Source source = processSource(new StreamSource(new ByteBufferPublisherInputStream(inputStream))); + Source source = processSource( + new StreamSource(DataBufferUtils.toInputStream(inputStream))); Unmarshaller unmarshaller = createUnmarshaller(outputClass); if (outputClass.isAnnotationPresent(XmlRootElement.class)) { return Flux.just(unmarshaller.unmarshal(source)); diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Encoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Encoder.java index 2482dd0fd0..652c7e3cdb 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Encoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Encoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -16,7 +16,7 @@ package org.springframework.core.codec.support; -import java.nio.ByteBuffer; +import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -27,12 +27,12 @@ import javax.xml.bind.Marshaller; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; -import reactor.io.buffer.Buffer; import org.springframework.core.ResolvableType; import org.springframework.core.codec.CodecException; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferAllocator; import org.springframework.util.Assert; -import org.springframework.util.BufferOutputStream; import org.springframework.util.ClassUtils; import org.springframework.util.MimeType; import org.springframework.util.MimeTypeUtils; @@ -43,30 +43,29 @@ import org.springframework.util.MimeTypeUtils; * @author Sebastien Deleuze * @see Jaxb2Decoder */ -public class Jaxb2Encoder extends AbstractEncoder { +public class Jaxb2Encoder extends AbstractAllocatingEncoder { private final ConcurrentMap, JAXBContext> jaxbContexts = new ConcurrentHashMap<>(64); - - public Jaxb2Encoder() { - super(MimeTypeUtils.APPLICATION_XML, MimeTypeUtils.TEXT_XML); + public Jaxb2Encoder(DataBufferAllocator allocator) { + super(allocator, MimeTypeUtils.APPLICATION_XML, MimeTypeUtils.TEXT_XML); } @Override - public Flux encode(Publisher messageStream, ResolvableType type, + public Flux encode(Publisher messageStream, + ResolvableType type, MimeType mimeType, Object... hints) { return Flux.from(messageStream).map(value -> { try { - Buffer buffer = new Buffer(); - BufferOutputStream outputStream = new BufferOutputStream(buffer); + DataBuffer buffer = allocator().allocateBuffer(1024); + OutputStream outputStream = buffer.asOutputStream(); Class clazz = ClassUtils.getUserClass(value); Marshaller marshaller = createMarshaller(clazz); marshaller.setProperty(Marshaller.JAXB_ENCODING, StandardCharsets.UTF_8.name()); marshaller.marshal(value, outputStream); - buffer.flip(); - return buffer.byteBuffer(); + return buffer; } catch (MarshalException ex) { throw new CodecException("Could not marshal [" + value + "]: " + ex.getMessage(), ex); diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectDecoder.java index 127a4ba74b..2bdc000320 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectDecoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -16,7 +16,6 @@ package org.springframework.core.codec.support; -import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; @@ -29,6 +28,8 @@ import reactor.core.publisher.Flux; import reactor.fn.Function; import org.springframework.core.ResolvableType; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferAllocator; import org.springframework.util.MimeType; /** @@ -44,7 +45,7 @@ import org.springframework.util.MimeType; * @author Sebastien Deleuze * @see JsonObjectEncoder */ -public class JsonObjectDecoder extends AbstractDecoder { +public class JsonObjectDecoder extends AbstractDecoder { private static final int ST_CORRUPTED = -1; @@ -54,38 +55,40 @@ public class JsonObjectDecoder extends AbstractDecoder { private static final int ST_DECODING_ARRAY_STREAM = 2; + private final DataBufferAllocator allocator; private final int maxObjectLength; private final boolean streamArrayElements; - - public JsonObjectDecoder() { + public JsonObjectDecoder(DataBufferAllocator allocator) { // 1 MB - this(1024 * 1024); + this(allocator, 1024 * 1024); } - public JsonObjectDecoder(int maxObjectLength) { - this(maxObjectLength, true); + public JsonObjectDecoder(DataBufferAllocator allocator, int maxObjectLength) { + this(allocator, maxObjectLength, true); } - public JsonObjectDecoder(boolean streamArrayElements) { - this(1024 * 1024, streamArrayElements); + public JsonObjectDecoder(DataBufferAllocator allocator, boolean streamArrayElements) { + this(allocator, 1024 * 1024, streamArrayElements); } /** + * @param allocator * @param maxObjectLength maximum number of bytes a JSON object/array may * use (including braces and all). Objects exceeding this length are dropped * and an {@link IllegalStateException} is thrown. * @param streamArrayElements if set to true and the "top level" JSON object * is an array, each of its entries is passed through the pipeline individually * and immediately after it was fully received, allowing for arrays with - * "infinitely" many elements. */ - public JsonObjectDecoder(int maxObjectLength, boolean streamArrayElements) { + public JsonObjectDecoder(DataBufferAllocator allocator, int maxObjectLength, + boolean streamArrayElements) { super(new MimeType("application", "json", StandardCharsets.UTF_8), new MimeType("application", "*+json", StandardCharsets.UTF_8)); + this.allocator = allocator; if (maxObjectLength < 1) { throw new IllegalArgumentException("maxObjectLength must be a positive int"); } @@ -94,10 +97,11 @@ public class JsonObjectDecoder extends AbstractDecoder { } @Override - public Flux decode(Publisher inputStream, ResolvableType type, + public Flux decode(Publisher inputStream, ResolvableType type, MimeType mimeType, Object... hints) { - return Flux.from(inputStream).flatMap(new Function>() { + return Flux.from(inputStream) + .flatMap(new Function>() { int openBraces; int index; @@ -107,14 +111,15 @@ public class JsonObjectDecoder extends AbstractDecoder { Integer writerIndex; @Override - public Publisher apply(ByteBuffer b) { - List chunks = new ArrayList<>(); + public Publisher apply(DataBuffer b) { + List chunks = new ArrayList<>(); if (this.input == null) { - this.input = Unpooled.copiedBuffer(b); + this.input = Unpooled.copiedBuffer(b.asByteBuffer()); this.writerIndex = this.input.writerIndex(); } else { - this.input = Unpooled.copiedBuffer(this.input, Unpooled.copiedBuffer(b)); + this.input = Unpooled.copiedBuffer(this.input, + Unpooled.copiedBuffer(b.asByteBuffer())); this.writerIndex = this.input.writerIndex(); } if (this.state == ST_CORRUPTED) { @@ -139,7 +144,7 @@ public class JsonObjectDecoder extends AbstractDecoder { ByteBuf json = extractObject(this.input, this.input.readerIndex(), this.index + 1 - this.input.readerIndex()); if (json != null) { - chunks.add(json.nioBuffer()); + chunks.add(allocator.wrap(json.nioBuffer())); } // The JSON object/array was extracted => discard the bytes from @@ -173,7 +178,7 @@ public class JsonObjectDecoder extends AbstractDecoder { idxNoSpaces + 1 - this.input.readerIndex()); if (json != null) { - chunks.add(json.nioBuffer()); + chunks.add(allocator.wrap(json.nioBuffer())); } this.input.readerIndex(this.index + 1); diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectEncoder.java index a2c5c5d34c..29e58fb4ff 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectEncoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -16,7 +16,6 @@ package org.springframework.core.codec.support; -import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; import java.util.concurrent.atomic.AtomicLongFieldUpdater; @@ -27,9 +26,10 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.subscriber.SubscriberBarrier; import reactor.core.util.BackpressureUtils; -import reactor.io.buffer.Buffer; import org.springframework.core.ResolvableType; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferAllocator; import org.springframework.util.MimeType; /** @@ -42,25 +42,25 @@ import org.springframework.util.MimeType; * * @see JsonObjectDecoder */ -public class JsonObjectEncoder extends AbstractEncoder { +public class JsonObjectEncoder extends AbstractAllocatingEncoder { - public JsonObjectEncoder() { - super(new MimeType("application", "json", StandardCharsets.UTF_8), + public JsonObjectEncoder(DataBufferAllocator allocator) { + super(allocator, new MimeType("application", "json", StandardCharsets.UTF_8), new MimeType("application", "*+json", StandardCharsets.UTF_8)); } @Override - public Flux encode(Publisher inputStream, + public Flux encode(Publisher inputStream, ResolvableType type, MimeType mimeType, Object... hints) { - if (inputStream instanceof Mono) { return Flux.from(inputStream); } - return Flux.from(inputStream).lift(s -> new JsonArrayEncoderBarrier(s)); + return Flux.from(inputStream) + .lift(s -> new JsonArrayEncoderBarrier(s, allocator())); } - - private static class JsonArrayEncoderBarrier extends SubscriberBarrier { + private static class JsonArrayEncoderBarrier + extends SubscriberBarrier { @SuppressWarnings("rawtypes") static final AtomicLongFieldUpdater REQUESTED = @@ -69,8 +69,9 @@ public class JsonObjectEncoder extends AbstractEncoder { static final AtomicIntegerFieldUpdater TERMINATED = AtomicIntegerFieldUpdater.newUpdater(JsonArrayEncoderBarrier.class, "terminated"); + private final DataBufferAllocator allocator; - private ByteBuffer prev = null; + private DataBuffer prev = null; private long count = 0; @@ -78,9 +79,10 @@ public class JsonObjectEncoder extends AbstractEncoder { private volatile int terminated; - - public JsonArrayEncoderBarrier(Subscriber subscriber) { + public JsonArrayEncoderBarrier(Subscriber subscriber, + DataBufferAllocator allocator) { super(subscriber); + this.allocator = allocator; } @@ -96,34 +98,32 @@ public class JsonObjectEncoder extends AbstractEncoder { } @Override - protected void doNext(ByteBuffer next) { + protected void doNext(DataBuffer next) { this.count++; - ByteBuffer tmp = this.prev; + DataBuffer tmp = this.prev; this.prev = next; - Buffer buffer = new Buffer(); + DataBuffer buffer = allocator.allocateBuffer(); if (this.count == 1) { - buffer.append("["); + buffer.write((byte) '['); } if (tmp != null) { - buffer.append(tmp); + buffer.write(tmp); } if (this.count > 1) { - buffer.append(","); + buffer.write((byte) ','); } - buffer.flip(); BackpressureUtils.getAndSub(REQUESTED, this, 1L); - subscriber.onNext(buffer.byteBuffer()); + subscriber.onNext(buffer); } protected void drainLast(){ if(BackpressureUtils.getAndSub(REQUESTED, this, 1L) > 0) { - Buffer buffer = new Buffer(); - buffer.append(this.prev); - buffer.append("]"); - buffer.flip(); - subscriber.onNext(buffer.byteBuffer()); + DataBuffer buffer = allocator.allocateBuffer(); + buffer.write(this.prev); + buffer.write((byte) ']'); + subscriber.onNext(buffer); super.doComplete(); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java index 9785ed5cbb..7889e1593e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java @@ -16,7 +16,6 @@ package org.springframework.core.codec.support; -import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; @@ -26,6 +25,8 @@ import reactor.core.publisher.Flux; import reactor.core.subscriber.SubscriberBarrier; import org.springframework.core.ResolvableType; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferAllocator; import org.springframework.util.MimeType; /** @@ -47,14 +48,16 @@ public class StringDecoder extends AbstractRawByteStreamDecoder { public final boolean reduceToSingleBuffer; + private final DataBufferAllocator allocator; + /** * Create a {@code StringDecoder} that decodes a bytes stream to a String stream * *

    By default, this decoder will buffer bytes and * emit a single String as a result. */ - public StringDecoder() { - this(true); + public StringDecoder(DataBufferAllocator allocator) { + this(allocator, true); } /** @@ -63,9 +66,10 @@ public class StringDecoder extends AbstractRawByteStreamDecoder { * @param reduceToSingleBuffer whether this decoder should buffer all received items * and decode a single consolidated String or re-emit items as they are provided */ - public StringDecoder(boolean reduceToSingleBuffer) { - super(new MimeType("text", "plain", DEFAULT_CHARSET)); + public StringDecoder(DataBufferAllocator allocator, boolean reduceToSingleBuffer) { + super(allocator, new MimeType("text", "plain", DEFAULT_CHARSET)); this.reduceToSingleBuffer = reduceToSingleBuffer; + this.allocator = allocator; } @Override @@ -75,18 +79,20 @@ public class StringDecoder extends AbstractRawByteStreamDecoder { } @Override - public SubscriberBarrier subscriberBarrier(Subscriber subscriber) { + public SubscriberBarrier subscriberBarrier( + Subscriber subscriber) { if (reduceToSingleBuffer) { - return new ReduceSingleByteStreamBarrier(subscriber); + return new ReduceSingleByteStreamBarrier(subscriber, allocator); } else { - return new SubscriberBarrier(subscriber); + return new SubscriberBarrier(subscriber); } } @Override - public Flux decodeInternal(Publisher inputStream, ResolvableType type, MimeType mimeType, Object... hints) { + public Flux decodeInternal(Publisher inputStream, + ResolvableType type, MimeType mimeType, Object... hints) { Charset charset; if (mimeType != null && mimeType.getCharSet() != null) { charset = mimeType.getCharSet(); @@ -94,7 +100,11 @@ public class StringDecoder extends AbstractRawByteStreamDecoder { else { charset = DEFAULT_CHARSET; } - return Flux.from(inputStream).map(content -> new String(content.duplicate().array(), charset)); + return Flux.from(inputStream).map(content -> { + byte[] bytes = new byte[content.readableByteCount()]; + content.read(bytes); + return new String(bytes, charset); + }); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringEncoder.java index 50ea51a26d..e7a80b693e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringEncoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -16,7 +16,6 @@ package org.springframework.core.codec.support; -import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; @@ -24,6 +23,8 @@ 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.DataBufferAllocator; import org.springframework.util.MimeType; /** @@ -32,13 +33,12 @@ import org.springframework.util.MimeType; * @author Sebastien Deleuze * @see StringDecoder */ -public class StringEncoder extends AbstractEncoder { +public class StringEncoder extends AbstractAllocatingEncoder { public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; - - public StringEncoder() { - super(new MimeType("text", "plain", DEFAULT_CHARSET)); + public StringEncoder(DataBufferAllocator allocator) { + super(allocator, new MimeType("text", "plain", DEFAULT_CHARSET)); } @@ -49,7 +49,7 @@ public class StringEncoder extends AbstractEncoder { } @Override - public Flux encode(Publisher elementStream, + public Flux encode(Publisher elementStream, ResolvableType type, MimeType mimeType, Object... hints) { Charset charset; @@ -59,7 +59,12 @@ public class StringEncoder extends AbstractEncoder { else { charset = DEFAULT_CHARSET; } - return Flux.from(elementStream).map(s -> ByteBuffer.wrap(s.getBytes(charset))); + return Flux.from(elementStream).map(s -> { + byte[] bytes = s.getBytes(charset); + DataBuffer dataBuffer = allocator().allocateBuffer(bytes.length); + dataBuffer.write(bytes); + return dataBuffer; + }); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/AbstractAllocatingTestCase.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/AbstractAllocatingTestCase.java new file mode 100644 index 0000000000..75710ac4d6 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/AbstractAllocatingTestCase.java @@ -0,0 +1,59 @@ +/* + * 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.support; + +import java.nio.charset.StandardCharsets; + +import io.netty.buffer.PooledByteBufAllocator; +import io.netty.buffer.UnpooledByteBufAllocator; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferAllocator; +import org.springframework.core.io.buffer.DefaultDataBufferAllocator; +import org.springframework.core.io.buffer.NettyDataBufferAllocator; + +/** + * @author Arjen Poutsma + */ +@RunWith(Parameterized.class) +public abstract class AbstractAllocatingTestCase { + + @Parameterized.Parameter + public DataBufferAllocator allocator; + + @Parameterized.Parameters(name = "{0}") + public static Object[][] allocators() { + return new Object[][]{ + {new NettyDataBufferAllocator(new UnpooledByteBufAllocator(true))}, + {new NettyDataBufferAllocator(new UnpooledByteBufAllocator(false))}, + {new NettyDataBufferAllocator(new PooledByteBufAllocator(true))}, + {new NettyDataBufferAllocator(new PooledByteBufAllocator(false))}, + {new DefaultDataBufferAllocator(true)}, + {new DefaultDataBufferAllocator(false)} + + }; + } + + protected DataBuffer stringBuffer(String value) { + byte[] bytes = value.getBytes(StandardCharsets.UTF_8); + DataBuffer buffer = allocator.allocateBuffer(bytes.length); + buffer.write(bytes); + return buffer; + } +} diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/ByteBufferDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ByteBufferDecoderTests.java similarity index 68% rename from spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/ByteBufferDecoderTests.java rename to spring-web-reactive/src/test/java/org/springframework/core/codec/support/ByteBufferDecoderTests.java index 85106d1130..6c59f5fb79 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/ByteBufferDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ByteBufferDecoderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.codec.decoder; +package org.springframework.core.codec.support; import java.nio.ByteBuffer; import java.util.List; @@ -26,7 +26,7 @@ import reactor.core.publisher.Flux; import reactor.io.buffer.Buffer; import org.springframework.core.ResolvableType; -import org.springframework.core.codec.support.ByteBufferDecoder; +import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.MediaType; import static java.util.stream.Collectors.toList; @@ -35,7 +35,7 @@ import static org.junit.Assert.*; /** * @author Sebastien Deleuze */ -public class ByteBufferDecoderTests { +public class ByteBufferDecoderTests extends AbstractAllocatingTestCase { private final ByteBufferDecoder decoder = new ByteBufferDecoder(); @@ -48,14 +48,25 @@ public class ByteBufferDecoderTests { @Test public void decode() throws InterruptedException { - ByteBuffer fooBuffer = Buffer.wrap("foo").byteBuffer(); - ByteBuffer barBuffer = Buffer.wrap("bar").byteBuffer(); - Flux source = Flux.just(fooBuffer, barBuffer); + DataBuffer fooBuffer = stringBuffer("foo"); + DataBuffer barBuffer = stringBuffer("bar"); + Flux source = Flux.just(fooBuffer, barBuffer); Flux output = decoder.decode(source, ResolvableType.forClassWithGenerics(Publisher.class, ByteBuffer.class), null); List results = StreamSupport.stream(output.toIterable().spliterator(), false).collect(toList()); assertEquals(2, results.size()); - assertEquals(fooBuffer, results.get(0)); - assertEquals(barBuffer, results.get(1)); + + assertBufferEquals(fooBuffer, results.get(0)); + assertBufferEquals(barBuffer, results.get(1)); + } + + public void assertBufferEquals(DataBuffer expected, ByteBuffer actual) { + byte[] byteBufferBytes = new byte[actual.remaining()]; + actual.get(byteBufferBytes); + + byte[] dataBufferBytes = new byte[expected.readableByteCount()]; + expected.read(dataBufferBytes); + + assertArrayEquals(dataBufferBytes, byteBufferBytes); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/ByteBufferEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ByteBufferEncoderTests.java similarity index 51% rename from spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/ByteBufferEncoderTests.java rename to spring-web-reactive/src/test/java/org/springframework/core/codec/support/ByteBufferEncoderTests.java index 16eb7a5644..ae4e817d64 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/ByteBufferEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ByteBufferEncoderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -14,19 +14,20 @@ * limitations under the License. */ -package org.springframework.reactive.codec.encoder; +package org.springframework.core.codec.support; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.util.List; import java.util.stream.StreamSupport; +import org.junit.Before; import org.junit.Test; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; -import reactor.io.buffer.Buffer; import org.springframework.core.ResolvableType; -import org.springframework.core.codec.support.ByteBufferEncoder; +import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.MediaType; import static java.util.stream.Collectors.toList; @@ -35,27 +36,47 @@ import static org.junit.Assert.*; /** * @author Sebastien Deleuze */ -public class ByteBufferEncoderTests { +public class ByteBufferEncoderTests extends AbstractAllocatingTestCase { - private final ByteBufferEncoder encoder = new ByteBufferEncoder(); + private ByteBufferEncoder encoder; + + @Before + public void createEncoder() { + encoder = new ByteBufferEncoder(allocator); + } @Test - public void canDecode() { + public void canEncode() { assertTrue(encoder.canEncode(ResolvableType.forClass(ByteBuffer.class), MediaType.TEXT_PLAIN)); assertFalse(encoder.canEncode(ResolvableType.forClass(Integer.class), MediaType.TEXT_PLAIN)); assertTrue(encoder.canEncode(ResolvableType.forClass(ByteBuffer.class), MediaType.APPLICATION_JSON)); } @Test - public void decode() throws InterruptedException { - ByteBuffer fooBuffer = Buffer.wrap("foo").byteBuffer(); - ByteBuffer barBuffer = Buffer.wrap("bar").byteBuffer(); - Flux source = Flux.just(fooBuffer, barBuffer); - Flux output = encoder.encode(source, ResolvableType.forClassWithGenerics(Publisher.class, ByteBuffer.class), null); - List results = StreamSupport.stream(output.toIterable().spliterator(), false).collect(toList()); + public void encode() throws Exception { + byte[] fooBytes = "foo".getBytes(StandardCharsets.UTF_8); + byte[] barBytes = "bar".getBytes(StandardCharsets.UTF_8); + Flux source = + Flux.just(ByteBuffer.wrap(fooBytes), ByteBuffer.wrap(barBytes)); + + Flux output = encoder.encode(source, + ResolvableType.forClassWithGenerics(Publisher.class, ByteBuffer.class), + null); + List results = + StreamSupport.stream(output.toIterable().spliterator(), false) + .collect(toList()); + assertEquals(2, results.size()); - assertEquals(fooBuffer, results.get(0)); - assertEquals(barBuffer, results.get(1)); + assertEquals(3, results.get(0).readableByteCount()); + assertEquals(3, results.get(1).readableByteCount()); + + byte[] buf = new byte[3]; + results.get(0).read(buf); + assertArrayEquals(fooBytes, buf); + + results.get(1).read(buf); + assertArrayEquals(barBytes, buf); + } } diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonDecoderTests.java similarity index 79% rename from spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoderTests.java rename to spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonDecoderTests.java index 8a0e476c43..83d7ac9e9a 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonDecoderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -14,9 +14,8 @@ * limitations under the License. */ -package org.springframework.reactive.codec.decoder; +package org.springframework.core.codec.support; -import java.nio.ByteBuffer; import java.util.List; import java.util.stream.StreamSupport; @@ -25,9 +24,8 @@ import reactor.core.publisher.Flux; import reactor.io.buffer.Buffer; import org.springframework.core.ResolvableType; -import org.springframework.core.codec.support.JacksonJsonDecoder; +import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.MediaType; -import org.springframework.reactive.codec.Pojo; import static java.util.stream.Collectors.toList; import static org.junit.Assert.*; @@ -35,7 +33,7 @@ import static org.junit.Assert.*; /** * @author Sebastien Deleuze */ -public class JacksonJsonDecoderTests { +public class JacksonJsonDecoderTests extends AbstractAllocatingTestCase { private final JacksonJsonDecoder decoder = new JacksonJsonDecoder(); @@ -47,7 +45,8 @@ public class JacksonJsonDecoderTests { @Test public void decode() throws InterruptedException { - Flux source = Flux.just(Buffer.wrap("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}").byteBuffer()); + Flux source = + Flux.just(stringBuffer("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}")); Flux output = decoder.decode(source, ResolvableType.forClass(Pojo.class), null); List results = StreamSupport.stream(output.toIterable().spliterator(), false).collect(toList()); assertEquals(1, results.size()); diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonEncoderTests.java similarity index 80% rename from spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoderTests.java rename to spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonEncoderTests.java index 18492f1d88..d31ad368c2 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonEncoderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -14,18 +14,17 @@ * limitations under the License. */ -package org.springframework.reactive.codec.encoder; +package org.springframework.core.codec.support; import java.nio.charset.StandardCharsets; import java.util.List; import java.util.stream.StreamSupport; +import org.junit.Before; import org.junit.Test; import reactor.core.publisher.Flux; -import org.springframework.core.codec.support.JacksonJsonEncoder; import org.springframework.http.MediaType; -import org.springframework.reactive.codec.Pojo; import static java.util.stream.Collectors.toList; import static org.junit.Assert.*; @@ -33,9 +32,14 @@ import static org.junit.Assert.*; /** * @author Sebastien Deleuze */ -public class JacksonJsonEncoderTests { +public class JacksonJsonEncoderTests extends AbstractAllocatingTestCase { - private final JacksonJsonEncoder encoder = new JacksonJsonEncoder(); + private JacksonJsonEncoder encoder; + + @Before + public void createEncoder() { + encoder = new JacksonJsonEncoder(allocator); + } @Test public void canWrite() { @@ -47,8 +51,8 @@ public class JacksonJsonEncoderTests { public void write() throws InterruptedException { Flux source = Flux.just(new Pojo("foofoo", "barbar"), new Pojo("foofoofoo", "barbarbar")); Flux output = encoder.encode(source, null, null).map(chunk -> { - byte[] b = new byte[chunk.remaining()]; - chunk.get(b); + byte[] b = new byte[chunk.readableByteCount()]; + chunk.read(b); return new String(b, StandardCharsets.UTF_8); }); List results = StreamSupport.stream(output.toIterable().spliterator(), false).collect(toList()); diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/Jaxb2DecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2DecoderTests.java similarity index 78% rename from spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/Jaxb2DecoderTests.java rename to spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2DecoderTests.java index a50b061afc..eaf983e56e 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/Jaxb2DecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2DecoderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -14,9 +14,8 @@ * limitations under the License. */ -package org.springframework.reactive.codec.decoder; +package org.springframework.core.codec.support; -import java.nio.ByteBuffer; import java.util.List; import java.util.stream.StreamSupport; @@ -25,9 +24,8 @@ import reactor.core.publisher.Flux; import reactor.io.buffer.Buffer; import org.springframework.core.ResolvableType; -import org.springframework.core.codec.support.Jaxb2Decoder; +import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.MediaType; -import org.springframework.reactive.codec.Pojo; import static java.util.stream.Collectors.toList; import static org.junit.Assert.*; @@ -35,7 +33,7 @@ import static org.junit.Assert.*; /** * @author Sebastien Deleuze */ -public class Jaxb2DecoderTests { +public class Jaxb2DecoderTests extends AbstractAllocatingTestCase { private final Jaxb2Decoder decoder = new Jaxb2Decoder(); @@ -48,7 +46,8 @@ public class Jaxb2DecoderTests { @Test public void decode() throws InterruptedException { - Flux source = Flux.just(Buffer.wrap("barbarfoofoo").byteBuffer()); + Flux source = Flux.just(stringBuffer( + "barbarfoofoo")); Flux output = decoder.decode(source, ResolvableType.forClass(Pojo.class), null); List results = StreamSupport.stream(output.toIterable().spliterator(), false).collect(toList()); assertEquals(1, results.size()); diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/Jaxb2EncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2EncoderTests.java similarity index 83% rename from spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/Jaxb2EncoderTests.java rename to spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2EncoderTests.java index dbbb849d6c..1d9dec12a4 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/Jaxb2EncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2EncoderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -14,18 +14,17 @@ * limitations under the License. */ -package org.springframework.reactive.codec.encoder; +package org.springframework.core.codec.support; import java.nio.charset.StandardCharsets; import java.util.List; import java.util.stream.StreamSupport; +import org.junit.Before; import org.junit.Test; import reactor.core.publisher.Flux; -import org.springframework.core.codec.support.Jaxb2Encoder; import org.springframework.http.MediaType; -import org.springframework.reactive.codec.Pojo; import static java.util.stream.Collectors.toList; import static org.junit.Assert.*; @@ -33,9 +32,14 @@ import static org.junit.Assert.*; /** * @author Sebastien Deleuze */ -public class Jaxb2EncoderTests { +public class Jaxb2EncoderTests extends AbstractAllocatingTestCase { - private final Jaxb2Encoder encoder = new Jaxb2Encoder(); + private Jaxb2Encoder encoder; + + @Before + public void createEncoder() { + encoder = new Jaxb2Encoder(allocator); + } @Test public void canEncode() { @@ -48,8 +52,8 @@ public class Jaxb2EncoderTests { public void encode() throws InterruptedException { Flux source = Flux.just(new Pojo("foofoo", "barbar"), new Pojo("foofoofoo", "barbarbar")); Flux output = encoder.encode(source, null, null).map(chunk -> { - byte[] b = new byte[chunk.remaining()]; - chunk.get(b); + byte[] b = new byte[chunk.readableByteCount()]; + chunk.read(b); return new String(b, StandardCharsets.UTF_8); }); List results = StreamSupport.stream(output.toIterable().spliterator(), false).collect(toList()); diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/JsonObjectDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JsonObjectDecoderTests.java similarity index 54% rename from spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/JsonObjectDecoderTests.java rename to spring-web-reactive/src/test/java/org/springframework/core/codec/support/JsonObjectDecoderTests.java index c0b27df9f8..3bb4f66f63 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/JsonObjectDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JsonObjectDecoderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -14,18 +14,16 @@ * limitations under the License. */ -package org.springframework.reactive.codec.decoder; +package org.springframework.core.codec.support; -import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.List; import java.util.stream.StreamSupport; import org.junit.Test; import reactor.core.publisher.Flux; -import reactor.io.buffer.Buffer; -import org.springframework.core.codec.support.JsonObjectDecoder; +import org.springframework.core.io.buffer.DataBuffer; import static java.util.stream.Collectors.toList; import static org.junit.Assert.assertEquals; @@ -33,17 +31,16 @@ import static org.junit.Assert.assertEquals; /** * @author Sebastien Deleuze */ -public class JsonObjectDecoderTests { +public class JsonObjectDecoderTests extends AbstractAllocatingTestCase { + @Test public void decodeSingleChunkToJsonObject() throws InterruptedException { - JsonObjectDecoder decoder = new JsonObjectDecoder(); - Flux source = Flux.just(Buffer.wrap("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}").byteBuffer()); - Flux output = decoder.decode(source, null, null).map(chunk -> { - byte[] b = new byte[chunk.remaining()]; - chunk.get(b); - return new String(b, StandardCharsets.UTF_8); - }); + JsonObjectDecoder decoder = new JsonObjectDecoder(allocator); + Flux source = + Flux.just(stringBuffer("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}")); + Flux output = + decoder.decode(source, null, null).map(JsonObjectDecoderTests::toString); List results = StreamSupport.stream(output.toIterable().spliterator(), false).collect(toList()); assertEquals(1, results.size()); assertEquals("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}", results.get(0)); @@ -51,13 +48,11 @@ public class JsonObjectDecoderTests { @Test public void decodeMultipleChunksToJsonObject() throws InterruptedException { - JsonObjectDecoder decoder = new JsonObjectDecoder(); - Flux source = Flux.just(Buffer.wrap("{\"foo\": \"foofoo\"").byteBuffer(), Buffer.wrap(", \"bar\": \"barbar\"}").byteBuffer()); - Flux output = decoder.decode(source, null, null).map(chunk -> { - byte[] b = new byte[chunk.remaining()]; - chunk.get(b); - return new String(b, StandardCharsets.UTF_8); - }); + JsonObjectDecoder decoder = new JsonObjectDecoder(allocator); + Flux source = Flux.just(stringBuffer("{\"foo\": \"foofoo\""), + stringBuffer(", \"bar\": \"barbar\"}")); + Flux output = + decoder.decode(source, null, null).map(JsonObjectDecoderTests::toString); List results = StreamSupport.stream(output.toIterable().spliterator(), false).collect(toList()); assertEquals(1, results.size()); assertEquals("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}", results.get(0)); @@ -65,13 +60,12 @@ public class JsonObjectDecoderTests { @Test public void decodeSingleChunkToArray() throws InterruptedException { - JsonObjectDecoder decoder = new JsonObjectDecoder(); - Flux source = Flux.just(Buffer.wrap("[{\"foo\": \"foofoo\", \"bar\": \"barbar\"},{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}]").byteBuffer()); - Flux output = decoder.decode(source, null, null).map(chunk -> { - byte[] b = new byte[chunk.remaining()]; - chunk.get(b); - return new String(b, StandardCharsets.UTF_8); - }); + JsonObjectDecoder decoder = new JsonObjectDecoder(allocator); + Flux source = Flux.just(stringBuffer( + "[{\"foo\": \"foofoo\", \"bar\": \"barbar\"},{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}]")); + Flux output = + decoder.decode(source, null, null).map(JsonObjectDecoderTests::toString); + List results = StreamSupport.stream(output.toIterable().spliterator(), false).collect(toList()); assertEquals(2, results.size()); assertEquals("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}", results.get(0)); @@ -80,17 +74,22 @@ public class JsonObjectDecoderTests { @Test public void decodeMultipleChunksToArray() throws InterruptedException { - JsonObjectDecoder decoder = new JsonObjectDecoder(); - Flux source = Flux.just(Buffer.wrap("[{\"foo\": \"foofoo\", \"bar\"").byteBuffer(), Buffer.wrap(": \"barbar\"},{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}]").byteBuffer()); - Flux output = decoder.decode(source, null, null).map(chunk -> { - byte[] b = new byte[chunk.remaining()]; - chunk.get(b); - return new String(b, StandardCharsets.UTF_8); - }); + JsonObjectDecoder decoder = new JsonObjectDecoder(allocator); + Flux source = + Flux.just(stringBuffer("[{\"foo\": \"foofoo\", \"bar\""), stringBuffer( + ": \"barbar\"},{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}]")); + Flux output = + decoder.decode(source, null, null).map(JsonObjectDecoderTests::toString); List results = StreamSupport.stream(output.toIterable().spliterator(), false).collect(toList()); assertEquals(2, results.size()); assertEquals("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}", results.get(0)); assertEquals("{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}", results.get(1)); } + private static String toString(DataBuffer buffer) { + byte[] b = new byte[buffer.readableByteCount()]; + buffer.read(b); + return new String(b, StandardCharsets.UTF_8); + } + } diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/JsonObjectEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JsonObjectEncoderTests.java similarity index 62% rename from spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/JsonObjectEncoderTests.java rename to spring-web-reactive/src/test/java/org/springframework/core/codec/support/JsonObjectEncoderTests.java index ee6ac9f07f..04034ea0b1 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/JsonObjectEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JsonObjectEncoderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -14,45 +14,52 @@ * limitations under the License. */ -package org.springframework.reactive.codec.encoder; +package org.springframework.core.codec.support; -import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; +import org.junit.Before; import org.junit.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import reactor.io.buffer.Buffer; -import org.springframework.core.codec.support.JsonObjectEncoder; +import org.springframework.core.io.buffer.DataBuffer; import static org.junit.Assert.assertEquals; /** * @author Sebastien Deleuze */ -public class JsonObjectEncoderTests { +public class JsonObjectEncoderTests extends AbstractAllocatingTestCase { + + private JsonObjectEncoder encoder; + + @Before + public void createEncoder() { + encoder = new JsonObjectEncoder(allocator); + } @Test public void encodeSingleElementFlux() throws InterruptedException { - JsonObjectEncoder encoder = new JsonObjectEncoder(); - Flux source = Flux.just(Buffer.wrap("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}").byteBuffer()); + Flux source = + Flux.just(stringBuffer("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}")); Iterable results = Flux.from(encoder.encode(source, null, null)).map(chunk -> { - byte[] b = new byte[chunk.remaining()]; - chunk.get(b); + byte[] b = new byte[chunk.readableByteCount()]; + chunk.read(b); return new String(b, StandardCharsets.UTF_8); }).toIterable(); String result = String.join("", results); assertEquals("[{\"foo\": \"foofoo\", \"bar\": \"barbar\"}]", result); } + @Test public void encodeSingleElementMono() throws InterruptedException { - JsonObjectEncoder encoder = new JsonObjectEncoder(); - Mono source = Mono.just(Buffer.wrap("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}").byteBuffer()); + Mono source = + Mono.just(stringBuffer("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}")); Iterable results = Flux.from(encoder.encode(source, null, null)).map(chunk -> { - byte[] b = new byte[chunk.remaining()]; - chunk.get(b); + byte[] b = new byte[chunk.readableByteCount()]; + chunk.read(b); return new String(b, StandardCharsets.UTF_8); }).toIterable(); String result = String.join("", results); @@ -61,13 +68,12 @@ public class JsonObjectEncoderTests { @Test public void encodeTwoElementsFlux() throws InterruptedException { - JsonObjectEncoder encoder = new JsonObjectEncoder(); - Flux source = Flux.just( - Buffer.wrap("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}").byteBuffer(), - Buffer.wrap("{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}").byteBuffer()); + Flux source = + Flux.just(stringBuffer("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}"), + stringBuffer("{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}")); Iterable results = Flux.from(encoder.encode(source, null, null)).map(chunk -> { - byte[] b = new byte[chunk.remaining()]; - chunk.get(b); + byte[] b = new byte[chunk.readableByteCount()]; + chunk.read(b); return new String(b, StandardCharsets.UTF_8); }).toIterable(); String result = String.join("", results); @@ -76,15 +82,15 @@ public class JsonObjectEncoderTests { @Test public void encodeThreeElementsFlux() throws InterruptedException { - JsonObjectEncoder encoder = new JsonObjectEncoder(); - Flux source = Flux.just( - Buffer.wrap("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}").byteBuffer(), - Buffer.wrap("{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}").byteBuffer(), - Buffer.wrap("{\"foo\": \"foofoofoofoo\", \"bar\": \"barbarbarbar\"}").byteBuffer() + Flux source = + Flux.just(stringBuffer("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}"), + stringBuffer("{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}"), + stringBuffer( + "{\"foo\": \"foofoofoofoo\", \"bar\": \"barbarbarbar\"}") ); Iterable results = Flux.from(encoder.encode(source, null, null)).map(chunk -> { - byte[] b = new byte[chunk.remaining()]; - chunk.get(b); + byte[] b = new byte[chunk.readableByteCount()]; + chunk.read(b); return new String(b, StandardCharsets.UTF_8); }).toIterable(); String result = String.join("", results); diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/Pojo.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Pojo.java similarity index 71% rename from spring-web-reactive/src/test/java/org/springframework/reactive/codec/Pojo.java rename to spring-web-reactive/src/test/java/org/springframework/core/codec/support/Pojo.java index ee55b9ec50..bcf0b24265 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/Pojo.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Pojo.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.reactive.codec; +package org.springframework.core.codec.support; import javax.xml.bind.annotation.XmlRootElement; @@ -52,4 +52,20 @@ public class Pojo { this.bar = bar; } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o instanceof Pojo) { + Pojo other = (Pojo) o; + return this.foo.equals(other.foo) && this.bar.equals(other.bar); + } + return false; + } + + @Override + public int hashCode() { + return 31 * foo.hashCode() + bar.hashCode(); + } } diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/StringDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringDecoderTests.java similarity index 80% rename from spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/StringDecoderTests.java rename to spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringDecoderTests.java index e3538a757b..7b45773de8 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/decoder/StringDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringDecoderTests.java @@ -14,12 +14,12 @@ * limitations under the License. */ -package org.springframework.reactive.codec.decoder; +package org.springframework.core.codec.support; -import java.nio.ByteBuffer; import java.util.List; import java.util.stream.StreamSupport; +import org.junit.Before; import org.junit.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -28,7 +28,7 @@ import reactor.io.buffer.Buffer; import rx.Single; import org.springframework.core.ResolvableType; -import org.springframework.core.codec.support.StringDecoder; +import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.MediaType; import static java.util.stream.Collectors.toList; @@ -38,9 +38,15 @@ import static org.junit.Assert.*; * @author Sebastien Deleuze * @author Brian Clozel */ -public class StringDecoderTests { +public class StringDecoderTests extends AbstractAllocatingTestCase { + + private StringDecoder decoder; + + @Before + public void createEncoder() { + decoder = new StringDecoder(allocator); + } - private final StringDecoder decoder = new StringDecoder(); @Test public void canDecode() { @@ -51,7 +57,7 @@ public class StringDecoderTests { @Test public void decode() throws InterruptedException { - Flux source = Flux.just(Buffer.wrap("foo").byteBuffer(), Buffer.wrap("bar").byteBuffer()); + Flux source = Flux.just(stringBuffer("foo"), stringBuffer("bar")); Flux output = this.decoder.decode(source, ResolvableType.forClassWithGenerics(Flux.class, String.class), null); List results = StreamSupport.stream(output.toIterable().spliterator(), false).collect(toList()); assertEquals(1, results.size()); @@ -60,8 +66,8 @@ public class StringDecoderTests { @Test public void decodeDoNotBuffer() throws InterruptedException { - StringDecoder decoder = new StringDecoder(false); - Flux source = Flux.just(Buffer.wrap("foo").byteBuffer(), Buffer.wrap("bar").byteBuffer()); + StringDecoder decoder = new StringDecoder(allocator, false); + Flux source = Flux.just(stringBuffer("foo"), stringBuffer("bar")); Flux output = decoder.decode(source, ResolvableType.forClassWithGenerics(Flux.class, String.class), null); List results = StreamSupport.stream(output.toIterable().spliterator(), false).collect(toList()); assertEquals(2, results.size()); @@ -71,7 +77,7 @@ public class StringDecoderTests { @Test public void decodeMono() throws InterruptedException { - Flux source = Flux.just(Buffer.wrap("foo").byteBuffer(), Buffer.wrap("bar").byteBuffer()); + Flux source = Flux.just(stringBuffer("foo"), stringBuffer("bar")); Mono mono = Mono.from(this.decoder.decode(source, ResolvableType.forClassWithGenerics(Mono.class, String.class), MediaType.TEXT_PLAIN)); @@ -81,7 +87,7 @@ public class StringDecoderTests { @Test public void decodeSingle() throws InterruptedException { - Flux source = Flux.just(Buffer.wrap("foo").byteBuffer(), Buffer.wrap("bar").byteBuffer()); + Flux source = Flux.just(stringBuffer("foo"), stringBuffer("bar")); Single single = RxJava1SingleConverter.from(this.decoder.decode(source, ResolvableType.forClassWithGenerics(Single.class, String.class), MediaType.TEXT_PLAIN)); diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/StringEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringEncoderTests.java similarity index 77% rename from spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/StringEncoderTests.java rename to spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringEncoderTests.java index 18a1c8993b..e1a509743e 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/StringEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringEncoderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -14,17 +14,19 @@ * limitations under the License. */ -package org.springframework.reactive.codec.encoder; +package org.springframework.core.codec.support; import java.nio.charset.StandardCharsets; import java.util.List; import java.util.stream.StreamSupport; +import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; import reactor.core.publisher.Flux; import org.springframework.core.ResolvableType; -import org.springframework.core.codec.support.StringEncoder; import org.springframework.http.MediaType; import static java.util.stream.Collectors.toList; @@ -33,9 +35,15 @@ import static org.junit.Assert.*; /** * @author Sebastien Deleuze */ -public class StringEncoderTests { +@RunWith(Parameterized.class) +public class StringEncoderTests extends AbstractAllocatingTestCase { - private final StringEncoder encoder = new StringEncoder(); + private StringEncoder encoder; + + @Before + public void createEncoder() { + encoder = new StringEncoder(allocator); + } @Test public void canWrite() { @@ -47,8 +55,8 @@ public class StringEncoderTests { @Test public void write() throws InterruptedException { Flux output = Flux.from(encoder.encode(Flux.just("foo"), null, null)).map(chunk -> { - byte[] b = new byte[chunk.remaining()]; - chunk.get(b); + byte[] b = new byte[chunk.readableByteCount()]; + chunk.read(b); return new String(b, StandardCharsets.UTF_8); }); List results = StreamSupport.stream(output.toIterable().spliterator(), false).collect(toList()); From 225179bc6f0b4c705a869a03b81df5f41345b6f7 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Thu, 21 Jan 2016 10:40:50 +0100 Subject: [PATCH 189/498] Updated http and web packages to use DataBuffer --- .../http/ReactiveHttpInputMessage.java | 8 +-- .../http/ReactiveHttpOutputMessage.java | 8 +-- .../reactive/AbstractServerHttpResponse.java | 9 ++- .../reactive/ReactorHttpHandlerAdapter.java | 11 +++- .../reactive/ReactorServerHttpRequest.java | 19 ++++-- .../reactive/ReactorServerHttpResponse.java | 10 ++-- .../reactive/RxNettyHttpHandlerAdapter.java | 14 +++-- .../reactive/RxNettyServerHttpRequest.java | 17 ++++-- .../reactive/RxNettyServerHttpResponse.java | 13 ++-- .../reactive/ServletHttpHandlerAdapter.java | 59 +++++++++++-------- .../reactive/ServletServerHttpRequest.java | 12 ++-- .../reactive/ServletServerHttpResponse.java | 10 ++-- .../reactive/UndertowHttpHandlerAdapter.java | 57 ++++++++++-------- .../reactive/UndertowServerHttpRequest.java | 12 ++-- .../reactive/UndertowServerHttpResponse.java | 10 ++-- .../reactive/boot/ReactorHttpServer.java | 23 +++++--- .../reactive/boot/RxNettyHttpServer.java | 40 ++++++++----- .../reactive/boot/UndertowHttpServer.java | 14 +++-- .../RequestBodyArgumentResolver.java | 6 +- .../RequestMappingHandlerAdapter.java | 14 ++++- .../reactive/AsyncIntegrationTests.java | 10 +++- .../reactive/MockServerHttpRequest.java | 13 ++-- .../reactive/MockServerHttpResponse.java | 10 ++-- .../http/server/reactive/RandomHandler.java | 19 +++--- .../http/server/reactive/XmlHandler.java | 19 +++--- .../reactive/DispatcherHandlerErrorTests.java | 23 +++++--- ...mpleUrlHandlerMappingIntegrationTests.java | 13 +++- .../RequestMappingIntegrationTests.java | 13 ++-- .../ResponseBodyResultHandlerTests.java | 10 ++-- 29 files changed, 303 insertions(+), 193 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpInputMessage.java b/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpInputMessage.java index 0cc0126ddd..15e4f041dc 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpInputMessage.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpInputMessage.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -16,11 +16,11 @@ package org.springframework.http; -import java.nio.ByteBuffer; - import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; +import org.springframework.core.io.buffer.DataBuffer; + /** * An "reactive" HTTP input message that exposes the input as {@link Publisher}. * @@ -35,6 +35,6 @@ public interface ReactiveHttpInputMessage extends HttpMessage { * Return the body of the message as a {@link Publisher}. * @return the body content publisher */ - Flux getBody(); + Flux getBody(); } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java b/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java index 71fd66eec3..ec4e63c192 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -16,11 +16,11 @@ package org.springframework.http; -import java.nio.ByteBuffer; - import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; +import org.springframework.core.io.buffer.DataBuffer; + /** * A "reactive" HTTP output message that accepts output as a {@link Publisher}. * @@ -38,6 +38,6 @@ public interface ReactiveHttpOutputMessage extends HttpMessage { * @param body the body content publisher * @return a publisher that indicates completion or error. */ - Mono setBody(Publisher body); + Mono setBody(Publisher body); } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java index 926c6c1692..2b2ee0ea63 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -15,12 +15,11 @@ */ package org.springframework.http.server.reactive; -import java.nio.ByteBuffer; - import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpHeaders; /** @@ -46,7 +45,7 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse { } @Override - public Mono setBody(Publisher publisher) { + public Mono setBody(Publisher publisher) { return Flux.from(publisher).lift(new WriteWithOperator<>(writeWithPublisher -> { writeHeaders(); return setBodyInternal(writeWithPublisher); @@ -57,7 +56,7 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse { * Implement this method to write to the underlying the response. * @param publisher the publisher to write with */ - protected abstract Mono setBodyInternal(Publisher publisher); + protected abstract Mono setBodyInternal(Publisher publisher); @Override public void writeHeaders() { diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorHttpHandlerAdapter.java index cd3b722df5..15a78c126e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorHttpHandlerAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -20,6 +20,7 @@ import reactor.io.buffer.Buffer; import reactor.io.net.ReactiveChannelHandler; import reactor.io.net.http.HttpChannel; +import org.springframework.core.io.buffer.DataBufferAllocator; import org.springframework.util.Assert; /** @@ -30,15 +31,19 @@ public class ReactorHttpHandlerAdapter private final HttpHandler httpHandler; + private final DataBufferAllocator allocator; - public ReactorHttpHandlerAdapter(HttpHandler httpHandler) { + public ReactorHttpHandlerAdapter(HttpHandler httpHandler, + DataBufferAllocator allocator) { Assert.notNull(httpHandler, "'httpHandler' is required."); this.httpHandler = httpHandler; + this.allocator = allocator; } @Override public Mono apply(HttpChannel channel) { - ReactorServerHttpRequest adaptedRequest = new ReactorServerHttpRequest(channel); + ReactorServerHttpRequest adaptedRequest = + new ReactorServerHttpRequest(channel, allocator); ReactorServerHttpResponse adaptedResponse = new ReactorServerHttpResponse(channel); return this.httpHandler.handle(adaptedRequest, adaptedResponse); } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java index 49805625b7..8f40ed958d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -27,6 +27,8 @@ import reactor.io.buffer.Buffer; import reactor.io.net.http.HttpChannel; import reactor.io.net.http.model.Cookie; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferAllocator; import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -41,10 +43,14 @@ public class ReactorServerHttpRequest extends AbstractServerHttpRequest { private final HttpChannel channel; + private final DataBufferAllocator allocator; - public ReactorServerHttpRequest(HttpChannel request) { - Assert.notNull("'request' must not be null."); + public ReactorServerHttpRequest(HttpChannel request, + DataBufferAllocator allocator) { + Assert.notNull("'request' must not be null"); + Assert.notNull(allocator, "'allocator' must not be null"); this.channel = request; + this.allocator = allocator; } @@ -84,8 +90,11 @@ public class ReactorServerHttpRequest extends AbstractServerHttpRequest { } @Override - public Flux getBody() { - return Flux.from(this.channel.input()).map(Buffer::byteBuffer); + public Flux getBody() { + return Flux.from(this.channel.input()).map(bytes -> { + ByteBuffer byteBuffer = bytes.byteBuffer(); + return allocator.wrap(byteBuffer); + }); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java index 59ce84236c..c6fbed8331 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -15,8 +15,6 @@ */ package org.springframework.http.server.reactive; -import java.nio.ByteBuffer; - import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -25,6 +23,7 @@ import reactor.io.net.http.HttpChannel; import reactor.io.net.http.model.Cookie; import reactor.io.net.http.model.Status; +import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpCookie; import org.springframework.http.HttpStatus; import org.springframework.util.Assert; @@ -56,8 +55,9 @@ public class ReactorServerHttpResponse extends AbstractServerHttpResponse { } @Override - protected Mono setBodyInternal(Publisher publisher) { - return Mono.from(this.channel.writeWith(Flux.from(publisher).map(Buffer::new))); + protected Mono setBodyInternal(Publisher publisher) { + return Mono.from(this.channel.writeWith( + Flux.from(publisher).map(buffer -> new Buffer(buffer.asByteBuffer())))); } @Override diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyHttpHandlerAdapter.java index 7763281fe9..6ed6f3e641 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyHttpHandlerAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -24,6 +24,7 @@ import org.reactivestreams.Publisher; import reactor.core.converter.RxJava1ObservableConverter; import rx.Observable; +import org.springframework.core.io.buffer.NettyDataBufferAllocator; import org.springframework.util.Assert; /** @@ -33,15 +34,20 @@ public class RxNettyHttpHandlerAdapter implements RequestHandler handle(HttpServerRequest request, HttpServerResponse response) { - RxNettyServerHttpRequest adaptedRequest = new RxNettyServerHttpRequest(request); + RxNettyServerHttpRequest adaptedRequest = + new RxNettyServerHttpRequest(request, allocator); RxNettyServerHttpResponse adaptedResponse = new RxNettyServerHttpResponse(response); Publisher result = this.httpHandler.handle(adaptedRequest, adaptedResponse); return RxJava1ObservableConverter.from(result); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java index 26dbc8271d..7fd9e01d2a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -18,7 +18,6 @@ package org.springframework.http.server.reactive; import java.net.URI; import java.net.URISyntaxException; -import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -30,6 +29,8 @@ import reactor.core.converter.RxJava1ObservableConverter; import reactor.core.publisher.Flux; import rx.Observable; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.NettyDataBufferAllocator; import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -45,9 +46,13 @@ public class RxNettyServerHttpRequest extends AbstractServerHttpRequest { private final HttpServerRequest request; + private final NettyDataBufferAllocator allocator; - public RxNettyServerHttpRequest(HttpServerRequest request) { - Assert.notNull("'request', request must not be null."); + public RxNettyServerHttpRequest(HttpServerRequest request, + NettyDataBufferAllocator allocator) { + Assert.notNull("'request', request must not be null"); + Assert.notNull(allocator, "'allocator' must not be null"); + this.allocator = allocator; this.request = request; } @@ -88,8 +93,8 @@ public class RxNettyServerHttpRequest extends AbstractServerHttpRequest { } @Override - public Flux getBody() { - Observable content = this.request.getContent().map(ByteBuf::nioBuffer); + public Flux getBody() { + Observable content = this.request.getContent().map(allocator::wrap); content = content.concatWith(Observable.empty()); // See GH issue #58 return RxJava1ObservableConverter.from(content); } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java index 7dc1aa285f..ddb6cedf3e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -16,8 +16,6 @@ package org.springframework.http.server.reactive; -import java.nio.ByteBuffer; - import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.cookie.Cookie; import io.netty.handler.codec.http.cookie.DefaultCookie; @@ -27,6 +25,7 @@ import reactor.core.converter.RxJava1ObservableConverter; import reactor.core.publisher.Mono; import rx.Observable; +import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpCookie; import org.springframework.http.HttpStatus; import org.springframework.util.Assert; @@ -58,15 +57,15 @@ public class RxNettyServerHttpResponse extends AbstractServerHttpResponse { } @Override - protected Mono setBodyInternal(Publisher publisher) { + protected Mono setBodyInternal(Publisher publisher) { Observable content = RxJava1ObservableConverter.from(publisher).map(this::toBytes); Observable completion = this.response.writeBytes(content); return RxJava1ObservableConverter.from(completion).after(); } - private byte[] toBytes(ByteBuffer buffer) { - byte[] bytes = new byte[buffer.remaining()]; - buffer.get(bytes); + private byte[] toBytes(DataBuffer buffer) { + byte[] bytes = new byte[buffer.readableByteCount()]; + buffer.read(bytes); return bytes; } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java index 11a29896ce..213164902a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -17,8 +17,6 @@ package org.springframework.http.server.reactive; import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.Arrays; import java.util.concurrent.atomic.AtomicLong; import javax.servlet.AsyncContext; import javax.servlet.ReadListener; @@ -38,6 +36,9 @@ import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import reactor.core.publisher.Mono; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferAllocator; +import org.springframework.core.io.buffer.DefaultDataBufferAllocator; import org.springframework.http.HttpStatus; import org.springframework.util.Assert; @@ -55,11 +56,16 @@ public class ServletHttpHandlerAdapter extends HttpServlet { private HttpHandler handler; + private DataBufferAllocator allocator = new DefaultDataBufferAllocator(); + public void setHandler(HttpHandler handler) { this.handler = handler; } + public void setAllocator(DataBufferAllocator allocator) { + this.allocator = allocator; + } @Override protected void service(HttpServletRequest servletRequest, HttpServletResponse servletResponse) @@ -68,11 +74,13 @@ public class ServletHttpHandlerAdapter extends HttpServlet { AsyncContext context = servletRequest.startAsync(); ServletAsyncContextSynchronizer synchronizer = new ServletAsyncContextSynchronizer(context); - RequestBodyPublisher requestBody = new RequestBodyPublisher(synchronizer, BUFFER_SIZE); + RequestBodyPublisher requestBody = + new RequestBodyPublisher(synchronizer, allocator, BUFFER_SIZE); ServletServerHttpRequest request = new ServletServerHttpRequest(servletRequest, requestBody); servletRequest.getInputStream().setReadListener(requestBody); - ResponseBodySubscriber responseBodySubscriber = new ResponseBodySubscriber(synchronizer); + ResponseBodySubscriber responseBodySubscriber = + new ResponseBodySubscriber(synchronizer, allocator); ServletServerHttpResponse response = new ServletServerHttpResponse(servletResponse, publisher -> Mono.from(subscriber -> publisher.subscribe(responseBodySubscriber))); servletResponse.getOutputStream().setWriteListener(responseBodySubscriber); @@ -81,30 +89,32 @@ public class ServletHttpHandlerAdapter extends HttpServlet { this.handler.handle(request, response).subscribe(resultSubscriber); } - - private static class RequestBodyPublisher implements ReadListener, Publisher { + private static class RequestBodyPublisher + implements ReadListener, Publisher { private final ServletAsyncContextSynchronizer synchronizer; + private final DataBufferAllocator allocator; + private final byte[] buffer; private final DemandCounter demand = new DemandCounter(); - private Subscriber subscriber; + private Subscriber subscriber; private boolean stalled; private boolean cancelled; - - public RequestBodyPublisher(ServletAsyncContextSynchronizer synchronizer, int bufferSize) { + public RequestBodyPublisher(ServletAsyncContextSynchronizer synchronizer, + DataBufferAllocator allocator, int bufferSize) { this.synchronizer = synchronizer; + this.allocator = allocator; this.buffer = new byte[bufferSize]; } - @Override - public void subscribe(Subscriber subscriber) { + public void subscribe(Subscriber subscriber) { if (subscriber == null) { throw new NullPointerException(); } @@ -146,11 +156,11 @@ public class ServletHttpHandlerAdapter extends HttpServlet { } else if (read > 0) { this.demand.decrement(); - byte[] copy = Arrays.copyOf(this.buffer, read); -// logger.debug("Next: " + new String(copy, UTF_8)); + DataBuffer dataBuffer = allocator.allocateBuffer(read); + dataBuffer.write(this.buffer, 0, read); - this.subscriber.onNext(ByteBuffer.wrap(copy)); + this.subscriber.onNext(dataBuffer); } } @@ -265,19 +275,23 @@ public class ServletHttpHandlerAdapter extends HttpServlet { } } - private static class ResponseBodySubscriber implements WriteListener, Subscriber { + private static class ResponseBodySubscriber + implements WriteListener, Subscriber { private final ServletAsyncContextSynchronizer synchronizer; + private final DataBufferAllocator allocator; + private Subscription subscription; - private ByteBuffer buffer; + private DataBuffer buffer; private volatile boolean subscriberComplete = false; - - public ResponseBodySubscriber(ServletAsyncContextSynchronizer synchronizer) { + public ResponseBodySubscriber(ServletAsyncContextSynchronizer synchronizer, + DataBufferAllocator allocator) { this.synchronizer = synchronizer; + this.allocator = allocator; } @@ -288,8 +302,7 @@ public class ServletHttpHandlerAdapter extends HttpServlet { } @Override - public void onNext(ByteBuffer bytes) { - + public void onNext(DataBuffer bytes) { Assert.isNull(buffer); this.buffer = bytes; @@ -321,8 +334,8 @@ public class ServletHttpHandlerAdapter extends HttpServlet { if (ready) { if (this.buffer != null) { - byte[] bytes = new byte[this.buffer.remaining()]; - this.buffer.get(bytes); + byte[] bytes = new byte[this.buffer.readableByteCount()]; + this.buffer.read(bytes); this.buffer = null; output.write(bytes); if (!subscriberComplete) { diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java index 89a3dbc3b4..5bc34daa71 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -18,7 +18,6 @@ package org.springframework.http.server.reactive; import java.net.URI; import java.net.URISyntaxException; -import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Enumeration; @@ -30,6 +29,7 @@ import javax.servlet.http.HttpServletRequest; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; +import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -47,10 +47,10 @@ public class ServletServerHttpRequest extends AbstractServerHttpRequest { private final HttpServletRequest request; - private final Flux requestBodyPublisher; + private final Flux requestBodyPublisher; - - public ServletServerHttpRequest(HttpServletRequest request, Publisher body) { + public ServletServerHttpRequest(HttpServletRequest request, + Publisher body) { Assert.notNull(request, "'request' must not be null."); Assert.notNull(body, "'body' must not be null."); this.request = request; @@ -125,7 +125,7 @@ public class ServletServerHttpRequest extends AbstractServerHttpRequest { } @Override - public Flux getBody() { + public Flux getBody() { return this.requestBodyPublisher; } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java index 494031ce77..adcf4284b4 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -16,7 +16,6 @@ package org.springframework.http.server.reactive; -import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.util.List; import java.util.Map; @@ -27,6 +26,7 @@ import javax.servlet.http.HttpServletResponse; import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; +import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpCookie; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -41,11 +41,11 @@ public class ServletServerHttpResponse extends AbstractServerHttpResponse { private final HttpServletResponse response; - private final Function, Mono> responseBodyWriter; + private final Function, Mono> responseBodyWriter; public ServletServerHttpResponse(HttpServletResponse response, - Function, Mono> responseBodyWriter) { + Function, Mono> responseBodyWriter) { Assert.notNull(response, "'response' must not be null"); Assert.notNull(responseBodyWriter, "'responseBodyWriter' must not be null"); @@ -64,7 +64,7 @@ public class ServletServerHttpResponse extends AbstractServerHttpResponse { } @Override - protected Mono setBodyInternal(Publisher publisher) { + protected Mono setBodyInternal(Publisher publisher) { return this.responseBodyWriter.apply(publisher); } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java index 8977890fed..03e109575d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -33,6 +33,8 @@ import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import org.xnio.ChannelListener; +import org.xnio.ChannelListeners; +import org.xnio.IoUtils; import org.xnio.channels.StreamSinkChannel; import org.xnio.channels.StreamSourceChannel; import reactor.core.publisher.Mono; @@ -40,13 +42,10 @@ import reactor.core.subscriber.BaseSubscriber; import reactor.core.util.BackpressureUtils; import reactor.core.util.Exceptions; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferAllocator; import org.springframework.util.Assert; -import static org.xnio.ChannelListeners.closingChannelExceptionHandler; -import static org.xnio.ChannelListeners.flushingChannelListener; -import static org.xnio.IoUtils.safeClose; - - /** * @author Marek Hawrylczak * @author Rossen Stoyanchev @@ -58,17 +57,21 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle private final HttpHandler delegate; + private final DataBufferAllocator allocator; - public UndertowHttpHandlerAdapter(HttpHandler delegate) { - Assert.notNull(delegate, "'delegate' is required."); + public UndertowHttpHandlerAdapter(HttpHandler delegate, + DataBufferAllocator allocator) { + Assert.notNull(delegate, "'delegate' is required"); + Assert.notNull(allocator, "'allocator' must not be null"); this.delegate = delegate; + this.allocator = allocator; } @Override public void handleRequest(HttpServerExchange exchange) throws Exception { - RequestBodyPublisher requestBody = new RequestBodyPublisher(exchange); + RequestBodyPublisher requestBody = new RequestBodyPublisher(exchange, allocator); ServerHttpRequest request = new UndertowServerHttpRequest(exchange, requestBody); ResponseBodySubscriber responseBodySubscriber = new ResponseBodySubscriber(exchange); @@ -107,8 +110,7 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle }); } - - private static class RequestBodyPublisher implements Publisher { + private static class RequestBodyPublisher implements Publisher { private static final AtomicLongFieldUpdater DEMAND = AtomicLongFieldUpdater.newUpdater(RequestBodySubscription.class, "demand"); @@ -116,16 +118,18 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle private final HttpServerExchange exchange; - private Subscriber subscriber; + private final DataBufferAllocator allocator; + private Subscriber subscriber; - public RequestBodyPublisher(HttpServerExchange exchange) { + public RequestBodyPublisher(HttpServerExchange exchange, + DataBufferAllocator allocator) { this.exchange = exchange; + this.allocator = allocator; } - @Override - public void subscribe(Subscriber subscriber) { + public void subscribe(Subscriber subscriber) { if (subscriber == null) { throw Exceptions.spec_2_13_exception(); } @@ -175,11 +179,11 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle private void close() { if (this.pooledBuffer != null) { - safeClose(this.pooledBuffer); + IoUtils.safeClose(this.pooledBuffer); this.pooledBuffer = null; } if (this.channel != null) { - safeClose(this.channel); + IoUtils.safeClose(this.channel); this.channel = null; } } @@ -251,7 +255,8 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle private void doOnNext(ByteBuffer buffer) { this.draining = false; buffer.flip(); - subscriber.onNext(buffer); + DataBuffer dataBuffer = allocator.wrap(buffer); + subscriber.onNext(dataBuffer); } private void doOnComplete() { @@ -315,7 +320,7 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle } } - private static class ResponseBodySubscriber extends BaseSubscriber + private static class ResponseBodySubscriber extends BaseSubscriber implements ChannelListener { private final HttpServerExchange exchange; @@ -343,8 +348,10 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle } @Override - public void onNext(ByteBuffer buffer) { - super.onNext(buffer); + public void onNext(DataBuffer dataBuffer) { + super.onNext(dataBuffer); + + ByteBuffer buffer = dataBuffer.asByteBuffer(); if (this.responseChannel == null) { this.responseChannel = exchange.getResponseChannel(); @@ -407,7 +414,7 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle } while (buffer.hasRemaining() && c > 0); if (!buffer.hasRemaining()) { - safeClose(this.buffers.remove()); + IoUtils.safeClose(this.buffers.remove()); } } while (!this.buffers.isEmpty() && c > 0); @@ -461,8 +468,10 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle this.responseChannel.shutdownWrites(); if (!this.responseChannel.flush()) { - this.responseChannel.getWriteSetter().set(flushingChannelListener( - o -> safeClose(this.responseChannel), closingChannelExceptionHandler())); + this.responseChannel.getWriteSetter().set(ChannelListeners + .flushingChannelListener( + o -> IoUtils.safeClose(this.responseChannel), + ChannelListeners.closingChannelExceptionHandler())); this.responseChannel.resumeWrites(); } this.responseChannel = null; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java index 9d04fcae69..da38edd041 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -18,7 +18,6 @@ package org.springframework.http.server.reactive; import java.net.URI; import java.net.URISyntaxException; -import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -29,6 +28,7 @@ import io.undertow.util.HeaderValues; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; +import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -44,10 +44,10 @@ public class UndertowServerHttpRequest extends AbstractServerHttpRequest { private final HttpServerExchange exchange; - private final Flux body; + private final Flux body; - - public UndertowServerHttpRequest(HttpServerExchange exchange, Publisher body) { + public UndertowServerHttpRequest(HttpServerExchange exchange, + Publisher body) { Assert.notNull(exchange, "'exchange' is required."); Assert.notNull(exchange, "'body' is required."); this.exchange = exchange; @@ -92,7 +92,7 @@ public class UndertowServerHttpRequest extends AbstractServerHttpRequest { } @Override - public Flux getBody() { + public Flux getBody() { return this.body; } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java index 8d7a910de4..2517837181 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -16,7 +16,6 @@ package org.springframework.http.server.reactive; -import java.nio.ByteBuffer; import java.util.List; import java.util.Map; import java.util.function.Function; @@ -28,6 +27,7 @@ import io.undertow.util.HttpString; import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; +import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpCookie; import org.springframework.http.HttpStatus; import org.springframework.util.Assert; @@ -42,11 +42,11 @@ public class UndertowServerHttpResponse extends AbstractServerHttpResponse { private final HttpServerExchange exchange; - private final Function, Mono> responseBodyWriter; + private final Function, Mono> responseBodyWriter; public UndertowServerHttpResponse(HttpServerExchange exchange, - Function, Mono> responseBodyWriter) { + Function, Mono> responseBodyWriter) { Assert.notNull(exchange, "'exchange' is required."); Assert.notNull(responseBodyWriter, "'responseBodyWriter' must not be null"); @@ -66,7 +66,7 @@ public class UndertowServerHttpResponse extends AbstractServerHttpResponse { } @Override - protected Mono setBodyInternal(Publisher publisher) { + protected Mono setBodyInternal(Publisher publisher) { return this.responseBodyWriter.apply(publisher); } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java index 91a1098f5b..245b5d1204 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java @@ -1,11 +1,11 @@ /* - * Copyright (c) 2011-2016 Pivotal Software Inc, All Rights Reserved. + * 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 + * 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, @@ -21,7 +21,8 @@ import reactor.core.state.Completable; import reactor.io.buffer.Buffer; import reactor.io.net.ReactiveNet; -import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.io.buffer.DataBufferAllocator; +import org.springframework.core.io.buffer.DefaultDataBufferAllocator; import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter; import org.springframework.util.Assert; @@ -29,29 +30,35 @@ import org.springframework.util.Assert; * @author Stephane Maldini */ public class ReactorHttpServer extends HttpServerSupport - implements InitializingBean, HttpServer, Connectable, Completable { + implements HttpServer, Connectable, Completable { private ReactorHttpHandlerAdapter reactorHandler; private reactor.io.net.http.HttpServer reactorServer; + private DataBufferAllocator allocator = new DefaultDataBufferAllocator(); + private boolean running; - @Override - public boolean isRunning() { - return this.running; + public void setAllocator(DataBufferAllocator allocator) { + this.allocator = allocator; } @Override public void afterPropertiesSet() throws Exception { Assert.notNull(getHttpHandler()); - this.reactorHandler = new ReactorHttpHandlerAdapter(getHttpHandler()); + this.reactorHandler = new ReactorHttpHandlerAdapter(getHttpHandler(), allocator); this.reactorServer = (getPort() != -1 ? ReactiveNet.httpServer(getPort()) : ReactiveNet.httpServer()); } + @Override + public boolean isRunning() { + return this.running; + } + @Override public Object connectedInput() { return reactorServer; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/RxNettyHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/RxNettyHttpServer.java index 24c056f3f0..bb23123281 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/RxNettyHttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/RxNettyHttpServer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -17,8 +17,10 @@ package org.springframework.http.server.reactive.boot; import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.UnpooledByteBufAllocator; -import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.io.buffer.NettyDataBufferAllocator; import org.springframework.http.server.reactive.RxNettyHttpHandlerAdapter; import org.springframework.util.Assert; @@ -26,14 +28,34 @@ import org.springframework.util.Assert; /** * @author Rossen Stoyanchev */ -public class RxNettyHttpServer extends HttpServerSupport implements InitializingBean, HttpServer { +public class RxNettyHttpServer extends HttpServerSupport implements HttpServer { private RxNettyHttpHandlerAdapter rxNettyHandler; private io.reactivex.netty.protocol.http.server.HttpServer rxNettyServer; + private NettyDataBufferAllocator allocator; + private boolean running; + public void setAllocator(ByteBufAllocator allocator) { + Assert.notNull(allocator, "'allocator' must not be null"); + this.allocator = new NettyDataBufferAllocator(allocator); + } + + @Override + public void afterPropertiesSet() throws Exception { + Assert.notNull(getHttpHandler()); + if (allocator == null) { + allocator = new NettyDataBufferAllocator(UnpooledByteBufAllocator.DEFAULT); + } + this.rxNettyHandler = new RxNettyHttpHandlerAdapter(getHttpHandler(), allocator); + + this.rxNettyServer = (getPort() != -1 ? + io.reactivex.netty.protocol.http.server.HttpServer.newServer(getPort()) : + io.reactivex.netty.protocol.http.server.HttpServer.newServer()); + } + @Override public boolean isRunning() { @@ -41,18 +63,6 @@ public class RxNettyHttpServer extends HttpServerSupport implements Initializing } - @Override - public void afterPropertiesSet() throws Exception { - - Assert.notNull(getHttpHandler()); - this.rxNettyHandler = new RxNettyHttpHandlerAdapter(getHttpHandler()); - - this.rxNettyServer = (getPort() != -1 ? - io.reactivex.netty.protocol.http.server.HttpServer.newServer(getPort()) : - io.reactivex.netty.protocol.http.server.HttpServer.newServer()); - } - - @Override public void start() { if (!this.running) { diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/UndertowHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/UndertowHttpServer.java index 3de5b5da1e..7226e505d9 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/UndertowHttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/UndertowHttpServer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -19,24 +19,30 @@ package org.springframework.http.server.reactive.boot; import io.undertow.Undertow; import io.undertow.server.HttpHandler; -import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.io.buffer.DataBufferAllocator; +import org.springframework.core.io.buffer.DefaultDataBufferAllocator; import org.springframework.http.server.reactive.UndertowHttpHandlerAdapter; import org.springframework.util.Assert; /** * @author Marek Hawrylczak */ -public class UndertowHttpServer extends HttpServerSupport implements InitializingBean, HttpServer { +public class UndertowHttpServer extends HttpServerSupport implements HttpServer { private Undertow server; + private DataBufferAllocator allocator = new DefaultDataBufferAllocator(); + private boolean running; + public void setAllocator(DataBufferAllocator allocator) { + this.allocator = allocator; + } @Override public void afterPropertiesSet() throws Exception { Assert.notNull(getHttpHandler()); - HttpHandler handler = new UndertowHttpHandlerAdapter(getHttpHandler()); + HttpHandler handler = new UndertowHttpHandlerAdapter(getHttpHandler(), allocator); int port = (getPort() != -1 ? getPort() : 8080); this.server = Undertow.builder().addHttpListener(port, "localhost") .setHandler(handler).build(); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestBodyArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestBodyArgumentResolver.java index eb58b72e3c..3f0bff5c98 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestBodyArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestBodyArgumentResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -16,7 +16,6 @@ package org.springframework.web.reactive.method.annotation; -import java.nio.ByteBuffer; import java.util.List; import org.reactivestreams.Publisher; @@ -27,6 +26,7 @@ import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.core.codec.Decoder; import org.springframework.core.convert.ConversionService; +import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.MediaType; import org.springframework.util.Assert; import org.springframework.web.bind.annotation.RequestBody; @@ -64,7 +64,7 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve mediaType = MediaType.APPLICATION_OCTET_STREAM; } ResolvableType type = ResolvableType.forMethodParameter(parameter); - Flux body = exchange.getRequest().getBody(); + Flux body = exchange.getRequest().getBody(); Flux elementFlux = body; ResolvableType elementType = type.hasGenerics() ? type.getGeneric(0) : type; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java index 41cfeaac93..acc907ce7d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -23,6 +23,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import io.netty.buffer.UnpooledByteBufAllocator; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import reactor.core.publisher.Mono; @@ -35,6 +36,8 @@ import org.springframework.core.codec.support.JsonObjectDecoder; import org.springframework.core.codec.support.StringDecoder; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.core.io.buffer.DataBufferAllocator; +import org.springframework.core.io.buffer.NettyDataBufferAllocator; import org.springframework.util.ObjectUtils; import org.springframework.web.method.HandlerMethod; import org.springframework.web.method.annotation.ExceptionHandlerMethodResolver; @@ -57,6 +60,9 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Initializin private ConversionService conversionService = new DefaultConversionService(); + private DataBufferAllocator allocator = + new NettyDataBufferAllocator(new UnpooledByteBufAllocator(false)); + private final Map, ExceptionHandlerMethodResolver> exceptionHandlerCache = new ConcurrentHashMap, ExceptionHandlerMethodResolver>(64); @@ -85,13 +91,17 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Initializin return this.conversionService; } + public void setAllocator(DataBufferAllocator allocator) { + this.allocator = allocator; + } @Override public void afterPropertiesSet() throws Exception { if (ObjectUtils.isEmpty(this.argumentResolvers)) { List> decoders = Arrays.asList(new ByteBufferDecoder(), - new StringDecoder(), new JacksonJsonDecoder(new JsonObjectDecoder())); + new StringDecoder(allocator), + new JacksonJsonDecoder(new JsonObjectDecoder(allocator))); this.argumentResolvers.add(new RequestParamArgumentResolver()); this.argumentResolvers.add(new RequestBodyArgumentResolver(decoders, this.conversionService)); diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java index 855840649c..4d2fe6c94f 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java @@ -1,11 +1,11 @@ /* - * Copyright (c) 2011-2016 Pivotal Software Inc, All Rights Reserved. + * 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 + * 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, @@ -31,6 +31,8 @@ import reactor.core.timer.Timers; import reactor.io.buffer.Buffer; import reactor.rx.Stream; +import org.springframework.core.io.buffer.DataBufferAllocator; +import org.springframework.core.io.buffer.DefaultDataBufferAllocator; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; import org.springframework.http.server.reactive.boot.HttpServer; @@ -52,6 +54,8 @@ public class AsyncIntegrationTests { private final ProcessorGroup asyncGroup = Processors.asyncGroup(); + private final DataBufferAllocator allocator = new DefaultDataBufferAllocator(); + protected int port; @Parameterized.Parameter(0) @@ -109,7 +113,7 @@ public class AsyncIntegrationTests { .dispatchOn(asyncGroup) .collect(Buffer::new, Buffer::append) .doOnSuccess(Buffer::flip) - .map(Buffer::byteBuffer) + .map((bytes) -> allocator.wrap(bytes.byteBuffer())) ); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpRequest.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpRequest.java index 183bddaede..01612d73d1 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpRequest.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -16,11 +16,11 @@ package org.springframework.http.server.reactive; import java.net.URI; -import java.nio.ByteBuffer; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; +import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -35,7 +35,7 @@ public class MockServerHttpRequest implements ServerHttpRequest { private HttpHeaders headers = new HttpHeaders(); - private Flux body; + private Flux body; public MockServerHttpRequest(HttpMethod httpMethod, URI uri) { @@ -43,7 +43,8 @@ public class MockServerHttpRequest implements ServerHttpRequest { this.uri = uri; } - public MockServerHttpRequest(Publisher body, HttpMethod httpMethod, URI uri) { + public MockServerHttpRequest(Publisher body, HttpMethod httpMethod, + URI uri) { this.body = Flux.from(body); this.httpMethod = httpMethod; this.uri = uri; @@ -78,11 +79,11 @@ public class MockServerHttpRequest implements ServerHttpRequest { } @Override - public Flux getBody() { + public Flux getBody() { return this.body; } - public void setBody(Publisher body) { + public void setBody(Publisher body) { this.body = Flux.from(body); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java index 9b0db98752..7aa6046041 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -15,12 +15,12 @@ */ package org.springframework.http.server.reactive; -import java.nio.ByteBuffer; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -33,7 +33,7 @@ public class MockServerHttpResponse implements ServerHttpResponse { private HttpHeaders headers = new HttpHeaders(); - private Publisher body; + private Publisher body; @Override @@ -51,12 +51,12 @@ public class MockServerHttpResponse implements ServerHttpResponse { } @Override - public Mono setBody(Publisher body) { + public Mono setBody(Publisher body) { this.body = body; return Flux.from(body).after(); } - public Publisher getBody() { + public Publisher getBody() { return this.body; } diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/RandomHandler.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/RandomHandler.java index e72e213b32..14b17ec1c2 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/RandomHandler.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/RandomHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -16,7 +16,6 @@ package org.springframework.http.server.reactive; -import java.nio.ByteBuffer; import java.util.Random; import org.apache.commons.logging.Log; @@ -24,7 +23,9 @@ import org.apache.commons.logging.LogFactory; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import reactor.core.publisher.Mono; -import reactor.io.buffer.Buffer; + +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DefaultDataBufferAllocator; import static org.junit.Assert.assertEquals; @@ -42,7 +43,7 @@ public class RandomHandler implements HttpHandler { @Override public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { - request.getBody().subscribe(new Subscriber() { + request.getBody().subscribe(new Subscriber() { private Subscription s; private int requestSize = 0; @@ -54,8 +55,8 @@ public class RandomHandler implements HttpHandler { } @Override - public void onNext(ByteBuffer bytes) { - requestSize += new Buffer(bytes).limit(); + public void onNext(DataBuffer bytes) { + requestSize += bytes.readableByteCount(); } @Override @@ -72,7 +73,11 @@ public class RandomHandler implements HttpHandler { }); response.getHeaders().setContentLength(RESPONSE_SIZE); - return response.setBody(Mono.just(ByteBuffer.wrap(randomBytes()))); + byte[] randomBytes = randomBytes(); + DataBuffer buffer = + new DefaultDataBufferAllocator().allocateBuffer(randomBytes.length); + buffer.write(randomBytes); + return response.setBody(Mono.just(buffer)); } private byte[] randomBytes() { diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/XmlHandler.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/XmlHandler.java index 74396d3c2f..7346fbc985 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/XmlHandler.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/XmlHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -16,6 +16,8 @@ package org.springframework.http.server.reactive; +import java.io.InputStream; +import java.io.OutputStream; import javax.xml.bind.JAXBContext; import javax.xml.bind.Marshaller; import javax.xml.bind.Unmarshaller; @@ -26,9 +28,10 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.io.buffer.Buffer; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DefaultDataBufferAllocator; +import org.springframework.core.io.buffer.support.DataBufferUtils; import org.springframework.http.MediaType; -import org.springframework.util.BufferOutputStream; -import org.springframework.util.ByteBufferPublisherInputStream; import static org.junit.Assert.fail; @@ -49,7 +52,7 @@ public class XmlHandler implements HttpHandler { Runnable r = () -> { try { - ByteBufferPublisherInputStream bis = new ByteBufferPublisherInputStream(request.getBody()); + InputStream bis = DataBufferUtils.toInputStream(request.getBody()); XmlHandlerIntegrationTests.Person johnDoe = (XmlHandlerIntegrationTests.Person) unmarshaller.unmarshal(bis); @@ -67,13 +70,13 @@ public class XmlHandler implements HttpHandler { response.getHeaders().setContentType(MediaType.APPLICATION_XML); XmlHandlerIntegrationTests.Person janeDoe = new XmlHandlerIntegrationTests.Person("Jane Doe"); - Buffer buffer = new Buffer(); - BufferOutputStream bos = new BufferOutputStream(buffer); + + DataBuffer buffer = new DefaultDataBufferAllocator().allocateBuffer(); + OutputStream bos = buffer.asOutputStream(); marshaller.marshal(janeDoe, bos); bos.close(); - buffer.flip(); - return response.setBody(Flux.just(buffer.byteBuffer())); + return response.setBody(Flux.just(buffer)); } catch (Exception ex) { logger.error(ex, ex); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java index aa3672c1c0..033148a846 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -13,10 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.web.reactive; import java.net.URI; -import java.nio.ByteBuffer; import java.util.Collections; import java.util.List; @@ -33,6 +33,8 @@ import org.springframework.context.annotation.Configuration; import org.springframework.core.codec.Encoder; import org.springframework.core.codec.support.StringEncoder; import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DefaultDataBufferAllocator; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -109,7 +111,7 @@ public class DispatcherHandlerErrorTests { @Test public void noResolverForArgument() throws Exception { - this.request.setUri(new URI("/uknown-argument-type")); + this.request.setUri(new URI("/unknown-argument-type")); Publisher publisher = this.dispatcherHandler.handle(this.exchange); Throwable ex = awaitErrorSignal(publisher); @@ -153,7 +155,9 @@ public class DispatcherHandlerErrorTests { public void notAcceptable() throws Exception { this.request.setUri(new URI("/request-body")); this.request.getHeaders().setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); - this.request.setBody(Mono.just(ByteBuffer.wrap("body".getBytes("UTF-8")))); + DataBuffer buffer = new DefaultDataBufferAllocator().allocateBuffer() + .write("body".getBytes("UTF-8")); + this.request.setBody(Mono.just(buffer)); Publisher publisher = this.dispatcherHandler.handle(this.exchange); Throwable ex = awaitErrorSignal(publisher); @@ -178,7 +182,7 @@ public class DispatcherHandlerErrorTests { @Test public void dispatcherHandlerWithHttpExceptionHandler() throws Exception { - this.request.setUri(new URI("/uknown-argument-type")); + this.request.setUri(new URI("/unknown-argument-type")); WebExceptionHandler exceptionHandler = new ServerError500ExceptionHandler(); WebHandler webHandler = new ExceptionHandlingWebHandler(this.dispatcherHandler, exceptionHandler); @@ -190,7 +194,7 @@ public class DispatcherHandlerErrorTests { @Test public void filterChainWithHttpExceptionHandler() throws Exception { - this.request.setUri(new URI("/uknown-argument-type")); + this.request.setUri(new URI("/unknown-argument-type")); WebHandler webHandler = new FilteringWebHandler(this.dispatcherHandler, new TestWebFilter()); webHandler = new ExceptionHandlingWebHandler(webHandler, new ServerError500ExceptionHandler()); @@ -224,7 +228,8 @@ public class DispatcherHandlerErrorTests { @Bean public ResponseBodyResultHandler resultHandler() { - List> encoders = Collections.singletonList(new StringEncoder()); + List> encoders = Collections + .singletonList(new StringEncoder(new DefaultDataBufferAllocator())); return new ResponseBodyResultHandler(encoders, new DefaultConversionService()); } @@ -238,8 +243,8 @@ public class DispatcherHandlerErrorTests { @SuppressWarnings("unused") private static class TestController { - @RequestMapping("/uknown-argument-type") - public void uknownArgumentType(Foo arg) { + @RequestMapping("/unknown-argument-type") + public void unknownArgumentType(Foo arg) { } @RequestMapping("/error-signal") diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java index 8221d555a0..fd7745a2d1 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -18,6 +18,7 @@ package org.springframework.web.reactive.handler; import java.net.URI; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; @@ -27,6 +28,8 @@ import reactor.core.publisher.Mono; import reactor.io.buffer.Buffer; import org.springframework.context.support.StaticApplicationContext; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DefaultDataBufferAllocator; import org.springframework.http.HttpStatus; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; @@ -139,7 +142,9 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler @Override public Mono handle(WebServerExchange exchange) { - return exchange.getResponse().setBody(Flux.just(Buffer.wrap("foo").byteBuffer())); + DataBuffer buffer = new DefaultDataBufferAllocator().allocateBuffer() + .write("foo".getBytes(StandardCharsets.UTF_8)); + return exchange.getResponse().setBody(Flux.just(buffer)); } } @@ -147,7 +152,9 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler @Override public Mono handle(WebServerExchange exchange) { - return exchange.getResponse().setBody(Flux.just(Buffer.wrap("bar").byteBuffer())); + DataBuffer buffer = new DefaultDataBufferAllocator().allocateBuffer() + .write("bar".getBytes(StandardCharsets.UTF_8)); + return exchange.getResponse().setBody(Flux.just(buffer)); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java index 393792e3d3..d9f1804147 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -48,6 +48,9 @@ import org.springframework.core.convert.support.GenericConversionService; import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter; import org.springframework.core.convert.support.ReactiveStreamsToReactorStreamConverter; import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferAllocator; +import org.springframework.core.io.buffer.DefaultDataBufferAllocator; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.RequestEntity; @@ -380,8 +383,10 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @Bean public ResponseBodyResultHandler responseBodyResultHandler() { + DataBufferAllocator allocator = new DefaultDataBufferAllocator(); return new ResponseBodyResultHandler(Arrays.asList( - new ByteBufferEncoder(), new StringEncoder(), new JacksonJsonEncoder(new JsonObjectEncoder())), + new ByteBufferEncoder(allocator), new StringEncoder(allocator), + new JacksonJsonEncoder(allocator, new JsonObjectEncoder(allocator))), conversionService()); } @@ -426,9 +431,9 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @RequestMapping("/raw") public Publisher rawResponseBody() { - JacksonJsonEncoder encoder = new JacksonJsonEncoder(); + JacksonJsonEncoder encoder = new JacksonJsonEncoder(new DefaultDataBufferAllocator()); return encoder.encode(Stream.just(new Person("Robert")), - ResolvableType.forClass(Person.class), MediaType.APPLICATION_JSON); + ResolvableType.forClass(Person.class), MediaType.APPLICATION_JSON).map(DataBuffer::asByteBuffer); } @RequestMapping("/stream-result") diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandlerTests.java index 32f30c20c5..3fd05e77c2 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -22,11 +22,12 @@ import org.junit.Test; import org.reactivestreams.Publisher; import org.springframework.core.ResolvableType; -import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.codec.support.StringEncoder; -import org.springframework.web.reactive.HandlerResult; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.core.io.buffer.DefaultDataBufferAllocator; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.method.HandlerMethod; +import org.springframework.web.reactive.HandlerResult; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -40,7 +41,8 @@ public class ResponseBodyResultHandlerTests { @Test public void supports() throws NoSuchMethodException { ResponseBodyResultHandler handler = new ResponseBodyResultHandler(Collections.singletonList( - new StringEncoder()), new DefaultConversionService()); + new StringEncoder(new DefaultDataBufferAllocator())), + new DefaultConversionService()); TestController controller = new TestController(); HandlerMethod hm = new HandlerMethod(controller,TestController.class.getMethod("notAnnotated")); From b8f2388d60e6cd203cf837f97a8c49af169f0ad6 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Thu, 21 Jan 2016 13:11:45 +0100 Subject: [PATCH 190/498] Removed unused classes --- .../util/BufferOutputStream.java | 53 ------ .../util/ByteBufferInputStream.java | 55 ------- .../util/ByteBufferPublisherInputStream.java | 153 ------------------ .../springframework/util/package-info.java | 20 --- 4 files changed, 281 deletions(-) delete mode 100644 spring-web-reactive/src/main/java/org/springframework/util/BufferOutputStream.java delete mode 100644 spring-web-reactive/src/main/java/org/springframework/util/ByteBufferInputStream.java delete mode 100644 spring-web-reactive/src/main/java/org/springframework/util/ByteBufferPublisherInputStream.java delete mode 100644 spring-web-reactive/src/main/java/org/springframework/util/package-info.java diff --git a/spring-web-reactive/src/main/java/org/springframework/util/BufferOutputStream.java b/spring-web-reactive/src/main/java/org/springframework/util/BufferOutputStream.java deleted file mode 100644 index 8f826f67c6..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/util/BufferOutputStream.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2002-2015 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.util; - -import java.io.IOException; -import java.io.OutputStream; - -import reactor.io.buffer.Buffer; - -/** - * Simple extension of {@link OutputStream} that uses {@link Buffer} to stream - * the content - * - * @author Sebastien Deleuze - */ -public class BufferOutputStream extends OutputStream { - - private Buffer buffer; - - public BufferOutputStream(Buffer buffer) { - this.buffer = buffer; - } - - @Override - public void write(int b) throws IOException { - buffer.append(b); - } - - @Override - public void write(byte[] bytes, int off, int len) - throws IOException { - buffer.append(bytes, off, len); - } - - public Buffer getBuffer() { - return buffer; - } - -} diff --git a/spring-web-reactive/src/main/java/org/springframework/util/ByteBufferInputStream.java b/spring-web-reactive/src/main/java/org/springframework/util/ByteBufferInputStream.java deleted file mode 100644 index b3a4945117..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/util/ByteBufferInputStream.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2002-2015 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.util; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.ByteBuffer; - -/** - * Simple {@link InputStream} implementation that exposes currently - * available content of a {@link ByteBuffer}. - * - * From Jackson ByteBufferBackedInputStream - */ -public class ByteBufferInputStream extends InputStream { - - protected final ByteBuffer b; - - public ByteBufferInputStream(ByteBuffer buf) { - b = buf; - } - - @Override - public int available() { - return b.remaining(); - } - - @Override - public int read() throws IOException { - return b.hasRemaining() ? (b.get() & 0xFF) : -1; - } - - @Override - public int read(byte[] bytes, int off, int len) throws IOException { - if (!b.hasRemaining()) return -1; - len = Math.min(len, b.remaining()); - b.get(bytes, off, len); - return len; - } - -} diff --git a/spring-web-reactive/src/main/java/org/springframework/util/ByteBufferPublisherInputStream.java b/spring-web-reactive/src/main/java/org/springframework/util/ByteBufferPublisherInputStream.java deleted file mode 100644 index abc1df3c44..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/util/ByteBufferPublisherInputStream.java +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright 2002-2015 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.util; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.ByteBuffer; -import java.util.concurrent.BlockingQueue; - -import org.reactivestreams.Publisher; -import org.reactivestreams.Subscription; -import reactor.rx.Stream; - -/** - * {@code InputStream} implementation based on a byte array {@link Publisher}. - * - * @author Arjen Poutsma - * @author Sebastien Deleuze - * @author Stephane Maldini - */ -public class ByteBufferPublisherInputStream extends InputStream { - - private final BlockingQueue queue; - - private ByteBufferInputStream currentStream; - - private boolean completed; - - - /** - * Creates a new {@code ByteArrayPublisherInputStream} based on the given publisher. - * - * @param publisher the publisher to use - */ - public ByteBufferPublisherInputStream(Publisher publisher) { - this(publisher, 1); - } - - /** - * Creates a new {@code ByteArrayPublisherInputStream} based on the given publisher. - * - * @param publisher the publisher to use - * @param requestSize the {@linkplain Subscription#request(long) request size} to use - * on the publisher bound to Integer MAX - */ - public ByteBufferPublisherInputStream(Publisher publisher, int requestSize) { - Assert.notNull(publisher, "'publisher' must not be null"); - - // TODO Avoid using Reactor Stream, it should not be a mandatory dependency of Spring Reactive - this.queue = Stream.from(publisher).toBlockingQueue(requestSize); - } - - - @Override - public int available() throws IOException { - if (completed) { - return 0; - } - InputStream is = currentStream(); - return is != null ? is.available() : 0; - } - - @Override - public int read() throws IOException { - if (completed) { - return -1; - } - InputStream is = currentStream(); - while (is != null) { - int ch = is.read(); - if (ch != -1) { - return ch; - } - else { - is = currentStream(); - } - } - return -1; - } - - @Override - public int read(byte[] b, int off, int len) throws IOException { - if (completed) { - return -1; - } - InputStream is = currentStream(); - if (is == null) { - return -1; - } - else if (b == null) { - throw new NullPointerException(); - } - else if (off < 0 || len < 0 || len > b.length - off) { - throw new IndexOutOfBoundsException(); - } - else if (len == 0) { - return 0; - } - do { - int n = is.read(b, off, len); - if (n > 0) { - return n; - } - else { - is = currentStream(); - } - } - while (is != null); - - return -1; - } - - private InputStream currentStream() throws IOException { - try { - if (this.currentStream != null && this.currentStream.available() > 0) { - return this.currentStream; - } else { - // take() blocks until next or complete() then return null, - // but that's OK since this is a *blocking* InputStream - ByteBuffer signal = this.queue.take(); - if(signal == null){ - this.completed = true; - return null; - } - this.currentStream = new ByteBufferInputStream(signal); - return this.currentStream; - } - } - catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - } - catch (Throwable error ){ - this.completed = true; - throw new IOException(error); - } - throw new IOException(); - } - -} diff --git a/spring-web-reactive/src/main/java/org/springframework/util/package-info.java b/spring-web-reactive/src/main/java/org/springframework/util/package-info.java deleted file mode 100644 index 8c69995741..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/util/package-info.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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. - */ - -/** - * Miscellaneous utility classes, such as {@code InputStream}/{@code OutputStream} manipulation utilities. - */ -package org.springframework.util; From 66c424daf9f310f9dc333d06fa706245f968295e Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Tue, 26 Jan 2016 12:39:32 +0100 Subject: [PATCH 191/498] Removed DataBufferAllocator.allocateHeapBuffer and allocateDirectBuffer in favor of allocateBuffer. --- .../core/io/buffer/DataBufferAllocator.java | 14 ------------ .../io/buffer/DefaultDataBufferAllocator.java | 15 +++---------- .../io/buffer/NettyDataBufferAllocator.java | 12 ---------- .../core/io/buffer/DataBufferTests.java | 22 +++++++------------ 4 files changed, 11 insertions(+), 52 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DataBufferAllocator.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DataBufferAllocator.java index e4586100f6..881de92684 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DataBufferAllocator.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DataBufferAllocator.java @@ -42,20 +42,6 @@ public interface DataBufferAllocator { */ DataBuffer allocateBuffer(int initialCapacity); - /** - * Allocates a data buffer of the given initial capacity on the heap. - * @param initialCapacity the initial capacity of the buffer to allocate - * @return the allocated buffer - */ - DataBuffer allocateHeapBuffer(int initialCapacity); - - /** - * Allocates a direct data buffer of the given initial capacity. - * @param initialCapacity the initial capacity of the buffer to allocate - * @return the allocated buffer - */ - DataBuffer allocateDirectBuffer(int initialCapacity); - /** * Wraps the given {@link ByteBuffer} in a {@code DataBuffer}. * @param byteBuffer the NIO byte buffer to wrap diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBufferAllocator.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBufferAllocator.java index 0f311978f5..fe320c995f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBufferAllocator.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBufferAllocator.java @@ -57,18 +57,9 @@ public class DefaultDataBufferAllocator implements DataBufferAllocator { @Override public DefaultDataBuffer allocateBuffer(int initialCapacity) { - return preferDirect ? allocateDirectBuffer(initialCapacity) : - allocateHeapBuffer(initialCapacity); - } - - @Override - public DefaultDataBuffer allocateHeapBuffer(int initialCapacity) { - return new DefaultDataBuffer(ByteBuffer.allocate(initialCapacity)); - } - - @Override - public DefaultDataBuffer allocateDirectBuffer(int initialCapacity) { - return new DefaultDataBuffer(ByteBuffer.allocateDirect(initialCapacity)); + return this.preferDirect ? + new DefaultDataBuffer(ByteBuffer.allocateDirect(initialCapacity)) : + new DefaultDataBuffer(ByteBuffer.allocate(initialCapacity)); } @Override diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/NettyDataBufferAllocator.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/NettyDataBufferAllocator.java index 6eea0f3298..c77db97911 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/NettyDataBufferAllocator.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/NettyDataBufferAllocator.java @@ -60,18 +60,6 @@ public class NettyDataBufferAllocator implements DataBufferAllocator { return new NettyDataBuffer(byteBuf); } - @Override - public NettyDataBuffer allocateHeapBuffer(int initialCapacity) { - ByteBuf byteBuf = this.byteBufAllocator.heapBuffer(initialCapacity); - return new NettyDataBuffer(byteBuf); - } - - @Override - public NettyDataBuffer allocateDirectBuffer(int initialCapacity) { - ByteBuf byteBuf = this.byteBufAllocator.directBuffer(initialCapacity); - return new NettyDataBuffer(byteBuf); - } - @Override public NettyDataBuffer wrap(ByteBuffer byteBuffer) { ByteBuf byteBuf = Unpooled.wrappedBuffer(byteBuffer); diff --git a/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/DataBufferTests.java b/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/DataBufferTests.java index 8c104a6069..da27d1194b 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/DataBufferTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/DataBufferTests.java @@ -37,28 +37,23 @@ import static org.junit.Assert.assertEquals; @RunWith(Parameterized.class) public class DataBufferTests { - @Parameterized.Parameter(0) + @Parameterized.Parameter public DataBufferAllocator allocator; - @Parameterized.Parameter(1) - public boolean direct; - - @Parameterized.Parameters(name = "{0} - direct: {1}") + @Parameterized.Parameters(name = "{0}") public static Object[][] buffers() { return new Object[][]{ - {new NettyDataBufferAllocator(new UnpooledByteBufAllocator(false)), true}, - {new NettyDataBufferAllocator(new UnpooledByteBufAllocator(false)), - false}, - {new NettyDataBufferAllocator(new PooledByteBufAllocator(false)), true}, - {new NettyDataBufferAllocator(new PooledByteBufAllocator(false)), false}, + {new NettyDataBufferAllocator(new UnpooledByteBufAllocator(true))}, + {new NettyDataBufferAllocator(new UnpooledByteBufAllocator(false))}, + {new NettyDataBufferAllocator(new PooledByteBufAllocator(true))}, + {new NettyDataBufferAllocator(new PooledByteBufAllocator(false))}, {new DefaultDataBufferAllocator(), true}, {new DefaultDataBufferAllocator(), false}}; } private DataBuffer createDataBuffer(int capacity) { - return direct ? allocator.allocateDirectBuffer(capacity) : - allocator.allocateHeapBuffer(capacity); + return allocator.allocateBuffer(capacity); } @Test @@ -183,8 +178,7 @@ public class DataBufferTests { } private ByteBuffer createByteBuffer(int capacity) { - return direct ? ByteBuffer.allocateDirect(capacity) : - ByteBuffer.allocate(capacity); + return ByteBuffer.allocate(capacity); } @Test From c84ef6cbf3ff837c365fa4323ea82179150c9536 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Tue, 26 Jan 2016 13:47:33 +0100 Subject: [PATCH 192/498] Incorporated misc. suggestions from the PR. --- .../springframework/core/io/buffer/DataBuffer.java | 3 --- .../core/io/buffer/DefaultDataBuffer.java | 5 +---- .../core/io/buffer/DefaultDataBufferAllocator.java | 5 +---- .../core/io/buffer/NettyDataBuffer.java | 11 ++++------- .../core/io/buffer/NettyDataBufferAllocator.java | 2 +- .../core/io/buffer/DataBufferTests.java | 4 ++-- 6 files changed, 9 insertions(+), 21 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DataBuffer.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DataBuffer.java index 2af308228b..49f5f557e0 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DataBuffer.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DataBuffer.java @@ -23,9 +23,6 @@ import java.nio.ByteBuffer; /** * Basic abstraction over byte buffers. * - *

    Mainly for internal use within the framework; consider Netty's - * {@link io.netty.buffer.ByteBuf} for a more comprehensive byte buffer. - * * @author Arjen Poutsma */ public interface DataBuffer { diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java index 5795c2a74b..4a1c73bb34 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java @@ -28,12 +28,9 @@ import org.springframework.util.ObjectUtils; /** * Default implementation of the {@link DataBuffer} interface that uses a {@link - * ByteBuffer} internally, with separate read and write positions. Typically constructed + * ByteBuffer} internally, with separate read and write positions. Constructed * using the {@link DefaultDataBufferAllocator}. * - *

    This class is rather limited; consider using Netty's - * {@link io.netty.buffer.ByteBuf} and {@link NettyDataBuffer} for a more comprehensive byte buffer. - * @author Arjen Poutsma * @see DefaultDataBufferAllocator */ diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBufferAllocator.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBufferAllocator.java index fe320c995f..2c414e0de3 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBufferAllocator.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBufferAllocator.java @@ -21,9 +21,6 @@ import java.nio.ByteBuffer; /** * Default implementation of the {@code DataBufferAllocator} interface. * - *

    This class is rather limited; consider using Netty's - * {@link io.netty.buffer.ByteBuf} and {@link NettyDataBuffer} for a more comprehensive - * byte buffer. * @author Arjen Poutsma */ public class DefaultDataBufferAllocator implements DataBufferAllocator { @@ -70,7 +67,7 @@ public class DefaultDataBufferAllocator implements DataBufferAllocator { @Override public String toString() { - return "DefaultDataBufferFactory"; + return "DefaultDataBufferFactory - preferDirect: " + this.preferDirect; } } diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/NettyDataBuffer.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/NettyDataBuffer.java index f2a4bcf841..0c03db43e6 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/NettyDataBuffer.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/NettyDataBuffer.java @@ -107,12 +107,9 @@ public class NettyDataBuffer implements DataBuffer { public NettyDataBuffer write(DataBuffer... buffers) { if (!ObjectUtils.isEmpty(buffers)) { if (buffers[0] instanceof NettyDataBuffer) { - NettyDataBuffer[] copy = - Arrays.copyOf(buffers, buffers.length, NettyDataBuffer[].class); - - ByteBuf[] nativeBuffers = - Arrays.stream(copy).map(NettyDataBuffer::getNativeBuffer) - .toArray(ByteBuf[]::new); + ByteBuf[] nativeBuffers = Arrays.stream(buffers) + .map(b -> ((NettyDataBuffer) b).getNativeBuffer()) + .toArray(ByteBuf[]::new); write(nativeBuffers); } @@ -149,7 +146,7 @@ public class NettyDataBuffer implements DataBuffer { new CompositeByteBuf(this.byteBuf.alloc(), this.byteBuf.isDirect(), byteBufs.length + 1); composite.addComponent(this.byteBuf); - Arrays.stream(byteBufs).forEach(composite::addComponent); + composite.addComponents(byteBufs); int writerIndex = this.byteBuf.readableBytes() + Arrays.stream(byteBufs).mapToInt(ByteBuf::readableBytes).sum(); diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/NettyDataBufferAllocator.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/NettyDataBufferAllocator.java index c77db97911..092e33518b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/NettyDataBufferAllocator.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/NettyDataBufferAllocator.java @@ -25,7 +25,7 @@ import io.netty.buffer.Unpooled; import org.springframework.util.Assert; /** - * Implemtation of the {@code DataBufferAllocator} interface based on a Netty + * Implementation of the {@code DataBufferAllocator} interface based on a Netty * {@link ByteBufAllocator}. * * @author Arjen Poutsma diff --git a/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/DataBufferTests.java b/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/DataBufferTests.java index da27d1194b..8169f5e142 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/DataBufferTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/DataBufferTests.java @@ -48,8 +48,8 @@ public class DataBufferTests { {new NettyDataBufferAllocator(new UnpooledByteBufAllocator(false))}, {new NettyDataBufferAllocator(new PooledByteBufAllocator(true))}, {new NettyDataBufferAllocator(new PooledByteBufAllocator(false))}, - {new DefaultDataBufferAllocator(), true}, - {new DefaultDataBufferAllocator(), false}}; + {new DefaultDataBufferAllocator(true)}, + {new DefaultDataBufferAllocator(false)}}; } private DataBuffer createDataBuffer(int capacity) { From 506c4bc27b3956b8b8f252f5be5beb477deb7d29 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Wed, 27 Jan 2016 16:58:59 +0100 Subject: [PATCH 193/498] Finish to cleanup Reactor Buffer usages We use it only for Reactor Net support now. --- .../core/codec/support/ByteBufferDecoderTests.java | 1 - .../core/codec/support/JacksonJsonDecoderTests.java | 1 - .../core/codec/support/Jaxb2DecoderTests.java | 1 - .../core/codec/support/StringDecoderTests.java | 1 - .../http/server/reactive/AsyncIntegrationTests.java | 10 ++++------ .../http/server/reactive/XmlHandler.java | 1 - .../SimpleUrlHandlerMappingIntegrationTests.java | 1 - .../annotation/RequestMappingIntegrationTests.java | 5 ++--- 8 files changed, 6 insertions(+), 15 deletions(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ByteBufferDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ByteBufferDecoderTests.java index 6c59f5fb79..bf7ae3c86c 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ByteBufferDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ByteBufferDecoderTests.java @@ -23,7 +23,6 @@ import java.util.stream.StreamSupport; import org.junit.Test; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; -import reactor.io.buffer.Buffer; import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.DataBuffer; diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonDecoderTests.java index 83d7ac9e9a..ca01aa945f 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonDecoderTests.java @@ -21,7 +21,6 @@ import java.util.stream.StreamSupport; import org.junit.Test; import reactor.core.publisher.Flux; -import reactor.io.buffer.Buffer; import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.DataBuffer; diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2DecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2DecoderTests.java index eaf983e56e..5422869d81 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2DecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2DecoderTests.java @@ -21,7 +21,6 @@ import java.util.stream.StreamSupport; import org.junit.Test; import reactor.core.publisher.Flux; -import reactor.io.buffer.Buffer; import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.DataBuffer; diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringDecoderTests.java index 7b45773de8..937bdc8290 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringDecoderTests.java @@ -24,7 +24,6 @@ import org.junit.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.converter.RxJava1SingleConverter; -import reactor.io.buffer.Buffer; import rx.Single; import org.springframework.core.ResolvableType; diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java index 4d2fe6c94f..d499a43149 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java @@ -28,7 +28,6 @@ import reactor.core.publisher.Mono; import reactor.core.publisher.ProcessorGroup; import reactor.core.publisher.Processors; import reactor.core.timer.Timers; -import reactor.io.buffer.Buffer; import reactor.rx.Stream; import org.springframework.core.io.buffer.DataBufferAllocator; @@ -98,7 +97,7 @@ public class AsyncIntegrationTests { public void basicTest() throws Exception { URI url = new URI("http://localhost:" + port); ResponseEntity response = new RestTemplate().exchange(RequestEntity.get(url) - .build(), String.class); + .build(), String.class); assertThat(response.getBody(), Matchers.equalTo("hello")); } @@ -107,14 +106,13 @@ public class AsyncIntegrationTests { @Override public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { + DataBufferAllocator allocator = new DefaultDataBufferAllocator(); return response.setBody(Stream.just("h", "e", "l", "l", "o") .timer(Timers.global()) .throttleRequest(100) .dispatchOn(asyncGroup) - .collect(Buffer::new, Buffer::append) - .doOnSuccess(Buffer::flip) - .map((bytes) -> allocator.wrap(bytes.byteBuffer())) - ); + .collect(allocator::allocateBuffer, + (buffer, str) -> buffer.write(str.getBytes()))); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/XmlHandler.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/XmlHandler.java index 7346fbc985..3b72aed9d9 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/XmlHandler.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/XmlHandler.java @@ -26,7 +26,6 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import reactor.io.buffer.Buffer; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DefaultDataBufferAllocator; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java index fd7745a2d1..5546b4cb61 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java @@ -25,7 +25,6 @@ import java.util.Map; import org.junit.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import reactor.io.buffer.Buffer; import org.springframework.context.support.StaticApplicationContext; import org.springframework.core.io.buffer.DataBuffer; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java index d9f1804147..84d3f55c34 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java @@ -28,7 +28,6 @@ import org.junit.Test; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import reactor.io.buffer.Buffer; import reactor.rx.Promise; import reactor.rx.Stream; import rx.Observable; @@ -443,12 +442,12 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @RequestMapping("/raw-flux") public Flux rawFluxResponseBody() { - return Flux.just(Buffer.wrap("Hello!").byteBuffer()); + return Flux.just(ByteBuffer.wrap("Hello!".getBytes())); } @RequestMapping("/raw-observable") public Observable rawObservableResponseBody() { - return Observable.just(Buffer.wrap("Hello!").byteBuffer()); + return Observable.just(ByteBuffer.wrap("Hello!".getBytes())); } @RequestMapping("/mono") From dc91c81fc468a9828dfd7805f05162efa1ca2d2c Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Wed, 27 Jan 2016 17:04:08 +0100 Subject: [PATCH 194/498] Polishing --- .../http/server/reactive/AsyncIntegrationTests.java | 1 - 1 file changed, 1 deletion(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java index d499a43149..2205511a7e 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java @@ -106,7 +106,6 @@ public class AsyncIntegrationTests { @Override public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { - DataBufferAllocator allocator = new DefaultDataBufferAllocator(); return response.setBody(Stream.just("h", "e", "l", "l", "o") .timer(Timers.global()) .throttleRequest(100) From 8a6fa02887e7b96024a94e43569688533ba01ab0 Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Wed, 27 Jan 2016 17:41:19 +0000 Subject: [PATCH 195/498] Sync with RSC move graph to flow --- .../http/server/reactive/boot/ReactorHttpServer.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java index 245b5d1204..f5c2946b8d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java @@ -1,11 +1,11 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright (c) 2011-2016 Pivotal Software Inc, All Rights Reserved. * * 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 + * 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, @@ -16,7 +16,7 @@ package org.springframework.http.server.reactive.boot; -import reactor.core.graph.Connectable; +import reactor.core.flow.Loopback; import reactor.core.state.Completable; import reactor.io.buffer.Buffer; import reactor.io.net.ReactiveNet; @@ -30,7 +30,7 @@ import org.springframework.util.Assert; * @author Stephane Maldini */ public class ReactorHttpServer extends HttpServerSupport - implements HttpServer, Connectable, Completable { + implements HttpServer, Loopback, Completable { private ReactorHttpHandlerAdapter reactorHandler; From 03e6d7dabfbfc4547bea6a842898406095e4bcb4 Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Wed, 27 Jan 2016 20:49:18 +0000 Subject: [PATCH 196/498] minor sync --- .../http/server/reactive/boot/ReactorHttpServer.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java index f5c2946b8d..9e61091331 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java @@ -99,9 +99,4 @@ public class ReactorHttpServer extends HttpServerSupport this.running = false; } } - - @Override - public Object upstream() { - return null; - } } From 3744549a3e1395ab4fe1d7df1a34547c31619e5e Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 19 Jan 2016 22:17:18 -0500 Subject: [PATCH 197/498] Add setComplete + beforeCommit to ServerHttpResponse setComplete replaces writeHeaders as a more general lifecycle method to perform any kind of handling at the end of request processing, for example to ensure headers are written if not already. beforeCommit provides an extension point for an action to be invoked just before the response is committed, e.g. adding headers/cookies. --- .../reactive/AbstractServerHttpResponse.java | 88 ++++++--- .../reactive/ReactorServerHttpResponse.java | 2 +- .../reactive/RxNettyServerHttpResponse.java | 2 +- .../server/reactive/ServerHttpResponse.java | 26 ++- .../reactive/ServletServerHttpResponse.java | 2 +- .../reactive/UndertowServerHttpResponse.java | 2 +- .../web/server/WebToHttpHandlerAdapter.java | 8 +- .../reactive/CookieIntegrationTests.java | 47 +---- .../reactive/MockServerHttpResponse.java | 30 ++- .../reactive/ServerHttpResponseTests.java | 176 ++++++++++++++++++ .../web/server/FilteringWebHandlerTests.java | 9 +- 11 files changed, 294 insertions(+), 98 deletions(-) create mode 100644 spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java index 2b2ee0ea63..9dbcf0a2bb 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java @@ -15,12 +15,20 @@ */ package org.springframework.http.server.reactive; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; + import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.rx.Stream; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpHeaders; +import org.springframework.util.Assert; + /** * Base class for {@link ServerHttpResponse} implementations. @@ -31,7 +39,9 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse { private final HttpHeaders headers; - private boolean headersWritten = false; + private AtomicReference state = new AtomicReference<>(State.NEW); + + private final List>> beforeCommitActions = new ArrayList<>(4); protected AbstractServerHttpResponse() { @@ -41,17 +51,54 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse { @Override public HttpHeaders getHeaders() { - return (this.headersWritten ? org.springframework.http.HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers); + if (State.COMITTED.equals(this.state.get())) { + return HttpHeaders.readOnlyHttpHeaders(this.headers); + } + return this.headers; } @Override public Mono setBody(Publisher publisher) { - return Flux.from(publisher).lift(new WriteWithOperator<>(writeWithPublisher -> { - writeHeaders(); - return setBodyInternal(writeWithPublisher); - })).after(); + return Flux.from(publisher) + .lift(new WriteWithOperator<>(writePublisher -> + applyBeforeCommit().after(() -> setBodyInternal(writePublisher)))) + .after(); } + private Mono applyBeforeCommit() { + return Stream.defer(() -> { + Mono mono = Mono.empty(); + if (this.state.compareAndSet(State.NEW, State.COMMITTING)) { + for (Supplier> action : this.beforeCommitActions) { + mono = mono.after(() -> action.get()); + } + mono = mono.otherwise(ex -> { + // Ignore errors from beforeCommit actions + return Mono.empty(); + }); + mono = mono.after(() -> { + this.state.set(State.COMITTED); + writeHeaders(); + writeCookies(); + return Mono.empty(); + }); + } + return mono; + }).after(); + } + + /** + * Implement this method to apply header changes from {@link #getHeaders()} + * to the underlying response. This method is called once only. + */ + protected abstract void writeHeaders(); + + /** + * Implement this method to add cookies from {@link #getHeaders()} to the + * underlying response. This method is called once only. + */ + protected abstract void writeCookies(); + /** * Implement this method to write to the underlying the response. * @param publisher the publisher to write with @@ -59,28 +106,17 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse { protected abstract Mono setBodyInternal(Publisher publisher); @Override - public void writeHeaders() { - if (!this.headersWritten) { - try { - writeHeadersInternal(); - writeCookies(); - } - finally { - this.headersWritten = true; - } - } + public void beforeCommit(Supplier> action) { + Assert.notNull(action); + this.beforeCommitActions.add(action); } - /** - * Implement this method to apply header changes from {@link #getHeaders()} - * to the underlying response. This method is called once only. - */ - protected abstract void writeHeadersInternal(); + @Override + public Mono setComplete() { + return applyBeforeCommit(); + } - /** - * Implement this method to add cookies from {@link #getHeaders()} to the - * underlying response. This method is called once only. - */ - protected abstract void writeCookies(); + + private enum State { NEW, COMMITTING, COMITTED } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java index c6fbed8331..8b0f3bcd19 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java @@ -61,7 +61,7 @@ public class ReactorServerHttpResponse extends AbstractServerHttpResponse { } @Override - protected void writeHeadersInternal() { + protected void writeHeaders() { for (String name : getHeaders().keySet()) { for (String value : getHeaders().get(name)) { this.channel.responseHeaders().add(name, value); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java index ddb6cedf3e..417e6b0a50 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java @@ -70,7 +70,7 @@ public class RxNettyServerHttpResponse extends AbstractServerHttpResponse { } @Override - protected void writeHeadersInternal() { + protected void writeHeaders() { for (String name : getHeaders().keySet()) { for (String value : getHeaders().get(name)) this.response.addHeader(name, value); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java index 82320e9858..d6c830641a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java @@ -16,7 +16,10 @@ package org.springframework.http.server.reactive; +import java.util.function.Supplier; + import org.reactivestreams.Publisher; +import reactor.core.publisher.Mono; import org.springframework.http.HttpStatus; import org.springframework.http.ReactiveHttpOutputMessage; @@ -35,15 +38,20 @@ public interface ServerHttpResponse extends ReactiveHttpOutputMessage { void setStatusCode(HttpStatus status); /** - * Use this method to apply header changes made via {@link #getHeaders()} to - * the underlying server response. By default changes made via - * {@link #getHeaders()} are cached until a call to {@link #setBody} - * implicitly applies header changes or until this method is called. - * - *

    Note: After this method is called, - * {@link #getHeaders() headers} become read-only and any additional calls - * to this method are ignored. + * Register an action to be applied just before the response is committed. + * @param action the action */ - void writeHeaders(); + void beforeCommit(Supplier> action); + + /** + * Indicate that request handling is complete, allowing for any cleanup or + * end-of-processing tasks to be performed such as applying header changes + * made via {@link #getHeaders()} to the underlying server response (if not + * applied already). + *

    This method should be automatically invoked at the end of request + * processing so typically applications should not have to invoke it. + * If invoked multiple times it should have no side effects. + */ + Mono setComplete(); } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java index adcf4284b4..887baa567a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java @@ -69,7 +69,7 @@ public class ServletServerHttpResponse extends AbstractServerHttpResponse { } @Override - protected void writeHeadersInternal() { + protected void writeHeaders() { for (Map.Entry> entry : getHeaders().entrySet()) { String headerName = entry.getKey(); for (String headerValue : entry.getValue()) { diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java index 2517837181..c4d8929b34 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java @@ -71,7 +71,7 @@ public class UndertowServerHttpResponse extends AbstractServerHttpResponse { } @Override - protected void writeHeadersInternal() { + protected void writeHeaders() { for (Map.Entry> entry : getHeaders().entrySet()) { HttpString headerName = HttpString.tryFromString(entry.getKey()); this.exchange.getResponseHeaders().addAll(headerName, entry.getValue()); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/WebToHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/server/WebToHttpHandlerAdapter.java index 34684b10d4..6b6b203327 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/WebToHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/WebToHttpHandlerAdapter.java @@ -43,17 +43,15 @@ public class WebToHttpHandlerAdapter extends WebHandlerDecorator implements Http @Override public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { WebServerExchange exchange = createWebServerExchange(request, response); - return getDelegate().handle(exchange).otherwise(ex -> { + return getDelegate().handle(exchange) + .otherwise(ex -> { if (logger.isDebugEnabled()) { logger.debug("Could not complete request", ex); } response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR); return Mono.empty(); }) - .doOnTerminate((aVoid, ex) -> { - response.writeHeaders(); - }); - + .after(response::setComplete); } protected WebServerExchange createWebServerExchange(ServerHttpRequest request, ServerHttpResponse response) { diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/CookieIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/CookieIntegrationTests.java index 38f714f85c..3a20401b74 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/CookieIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/CookieIntegrationTests.java @@ -20,8 +20,6 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; -import org.junit.After; -import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -30,13 +28,6 @@ import reactor.core.publisher.Mono; import org.springframework.http.HttpCookie; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; -import org.springframework.http.server.reactive.boot.HttpServer; -import org.springframework.http.server.reactive.boot.JettyHttpServer; -import org.springframework.http.server.reactive.boot.ReactorHttpServer; -import org.springframework.http.server.reactive.boot.RxNettyHttpServer; -import org.springframework.http.server.reactive.boot.TomcatHttpServer; -import org.springframework.http.server.reactive.boot.UndertowHttpServer; -import org.springframework.util.SocketUtils; import org.springframework.web.client.RestTemplate; import static org.hamcrest.CoreMatchers.equalTo; @@ -49,47 +40,16 @@ import static org.junit.Assert.assertThat; * @author Rossen Stoyanchev */ @RunWith(Parameterized.class) -public class CookieIntegrationTests { - - protected int port; - - @Parameterized.Parameter(0) - public HttpServer server; +public class CookieIntegrationTests extends AbstractHttpHandlerIntegrationTests { private CookieHandler cookieHandler; - - @Parameterized.Parameters(name = "server [{0}]") - public static Object[][] arguments() { - return new Object[][] { - {new JettyHttpServer()}, - {new RxNettyHttpServer()}, - {new ReactorHttpServer()}, - {new TomcatHttpServer()}, - {new UndertowHttpServer()} - }; - } - - - @Before - public void setup() throws Exception { - this.port = SocketUtils.findAvailableTcpPort(); - this.server.setPort(this.port); - this.server.setHandler(createHttpHandler()); - this.server.afterPropertiesSet(); - this.server.start(); - } - + @Override protected HttpHandler createHttpHandler() { this.cookieHandler = new CookieHandler(); return this.cookieHandler; } - @After - public void tearDown() throws Exception { - this.server.stop(); - } - @SuppressWarnings("unchecked") @Test @@ -145,9 +105,8 @@ public class CookieIntegrationTests { .path("/").secure().httpOnly().build()); response.getHeaders().addCookie(HttpCookie.serverCookie("lang", "en-US") .domain("example.com").path("/").build()); - response.writeHeaders(); - return Mono.empty(); + return response.setComplete(); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java index 7aa6046041..cfd7f18305 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java @@ -15,6 +15,7 @@ */ package org.springframework.http.server.reactive; +import java.util.function.Supplier; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -27,7 +28,7 @@ import org.springframework.http.HttpStatus; /** * @author Rossen Stoyanchev */ -public class MockServerHttpResponse implements ServerHttpResponse { +public class MockServerHttpResponse extends AbstractServerHttpResponse { private HttpStatus status; @@ -50,18 +51,31 @@ public class MockServerHttpResponse implements ServerHttpResponse { return this.headers; } - @Override - public Mono setBody(Publisher body) { - this.body = body; - return Flux.from(body).after(); - } - public Publisher getBody() { return this.body; } @Override - public void writeHeaders() { + protected Mono setBodyInternal(Publisher body) { + this.body = body; + return Flux.from(this.body).after(); + } + + @Override + protected void writeHeaders() { + } + + @Override + protected void writeCookies() { + } + + @Override + public void beforeCommit(Supplier> action) { + } + + @Override + public Mono setComplete() { + return Mono.empty(); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java new file mode 100644 index 0000000000..af049f0f29 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java @@ -0,0 +1,176 @@ +/* + * 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.http.server.reactive; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DefaultDataBufferAllocator; +import org.springframework.http.HttpCookie; +import org.springframework.http.HttpStatus; + +import static junit.framework.TestCase.assertTrue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; + +/** + * @author Rossen Stoyanchev + */ +public class ServerHttpResponseTests { + + public static final Charset UTF_8 = Charset.forName("UTF-8"); + + + @Test + public void setBody() throws Exception { + TestServerHttpResponse response = new TestServerHttpResponse(); + response.setBody(Flux.just(wrap("a"), wrap("b"), wrap("c"))).get(); + + assertTrue(response.headersWritten); + assertTrue(response.cookiesWritten); + + assertEquals(3, response.content.size()); + assertEquals("a", new String(response.content.get(0).asByteBuffer().array(), UTF_8)); + assertEquals("b", new String(response.content.get(1).asByteBuffer().array(), UTF_8)); + assertEquals("c", new String(response.content.get(2).asByteBuffer().array(), UTF_8)); + } + + @Test + public void setBodyWithError() throws Exception { + TestServerHttpResponse response = new TestServerHttpResponse(); + IllegalStateException error = new IllegalStateException("boo"); + response.setBody(Flux.error(error)).otherwise(ex -> Mono.empty()).get(); + + assertFalse(response.headersWritten); + assertFalse(response.cookiesWritten); + assertTrue(response.content.isEmpty()); + } + + @Test + public void setComplete() throws Exception { + TestServerHttpResponse response = new TestServerHttpResponse(); + response.setComplete().get(); + + assertTrue(response.headersWritten); + assertTrue(response.cookiesWritten); + assertTrue(response.content.isEmpty()); + } + + @Test + public void beforeCommitWithSetBody() throws Exception { + HttpCookie cookie = HttpCookie.serverCookie("ID", "123").build(); + TestServerHttpResponse response = new TestServerHttpResponse(); + response.beforeCommit(() -> { + response.getHeaders().addCookie(cookie); + return Mono.empty(); + }); + response.setBody(Flux.just(wrap("a"), wrap("b"), wrap("c"))).get(); + + assertTrue(response.headersWritten); + assertTrue(response.cookiesWritten); + assertSame(cookie, response.getHeaders().getCookies().get("ID").get(0)); + + assertEquals(3, response.content.size()); + assertEquals("a", new String(response.content.get(0).asByteBuffer().array(), UTF_8)); + assertEquals("b", new String(response.content.get(1).asByteBuffer().array(), UTF_8)); + assertEquals("c", new String(response.content.get(2).asByteBuffer().array(), UTF_8)); + } + + @Test + public void beforeCommitActionWithError() throws Exception { + TestServerHttpResponse response = new TestServerHttpResponse(); + IllegalStateException error = new IllegalStateException("boo"); + response.beforeCommit(() -> Mono.error(error)); + response.setBody(Flux.just(wrap("a"), wrap("b"), wrap("c"))).get(); + + assertTrue("beforeCommit action errors should be ignored", response.headersWritten); + assertTrue("beforeCommit action errors should be ignored", response.cookiesWritten); + assertNull(response.getHeaders().getCookies().get("ID")); + + assertEquals(3, response.content.size()); + assertEquals("a", new String(response.content.get(0).asByteBuffer().array(), UTF_8)); + assertEquals("b", new String(response.content.get(1).asByteBuffer().array(), UTF_8)); + assertEquals("c", new String(response.content.get(2).asByteBuffer().array(), UTF_8)); + } + + @Test + public void beforeCommitActionWithSetComplete() throws Exception { + HttpCookie cookie = HttpCookie.serverCookie("ID", "123").build(); + TestServerHttpResponse response = new TestServerHttpResponse(); + response.beforeCommit(() -> { + response.getHeaders().addCookie(cookie); + return Mono.empty(); + }); + response.setComplete().get(); + + assertTrue(response.headersWritten); + assertTrue(response.cookiesWritten); + assertTrue(response.content.isEmpty()); + assertSame(cookie, response.getHeaders().getCookies().get("ID").get(0)); + } + + + + private DataBuffer wrap(String a) { + return new DefaultDataBufferAllocator().wrap(ByteBuffer.wrap(a.getBytes(UTF_8))); + } + + + private static class TestServerHttpResponse extends AbstractServerHttpResponse { + + private boolean headersWritten; + + private boolean cookiesWritten; + + private final List content = new ArrayList<>(); + + + @Override + public void setStatusCode(HttpStatus status) { + } + + @Override + protected void writeHeaders() { + assertFalse(this.headersWritten); + this.headersWritten = true; + } + + @Override + protected void writeCookies() { + assertFalse(this.cookiesWritten); + this.cookiesWritten = true; + } + + @Override + protected Mono setBodyInternal(Publisher publisher) { + return Flux.from(publisher).map(b -> { + this.content.add(b); + return b; + }).after(); + } + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/server/FilteringWebHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/server/FilteringWebHandlerTests.java index 7f56a39cce..aaddff9233 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/server/FilteringWebHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/server/FilteringWebHandlerTests.java @@ -16,13 +16,18 @@ package org.springframework.web.server; +import java.net.URI; + import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.junit.Before; import org.junit.Test; import reactor.core.publisher.Mono; +import org.springframework.http.HttpMethod; import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; @@ -45,8 +50,8 @@ public class FilteringWebHandlerTests { @Before public void setUp() throws Exception { - this.request = mock(ServerHttpRequest.class); - this.response = mock(ServerHttpResponse.class); + this.request = new MockServerHttpRequest(HttpMethod.GET, new URI("http://localhost")); + this.response = new MockServerHttpResponse(); } @Test From 407d11a58a9dbead6de34ff0acfd5af7e084134b Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 13 Jan 2016 15:41:52 -0500 Subject: [PATCH 198/498] Server-side session support This commit adds initial support for a maintaining a server-side session with attributes across HTTP requests. The WebSession abstraction can be accessed via WebServerExchange from a WebFilter or the target WebHandler. The session sub-package contains additional abstractions for creating and managing sessions providing a basis for extensibility (e.g. Spring Session). Those include WebSessionManager, SessionIdStrategy, and SessionStore along with a cookie-based session id strategy and an in-memory session store in use by default. Note that the current API does not provide a way to invalidate or re-create the session from server side code. --- .../org/springframework/http/HttpCookie.java | 24 +-- .../reactive/AbstractServerHttpResponse.java | 34 ++-- .../reactive/ReactorServerHttpResponse.java | 32 ++-- .../reactive/RxNettyServerHttpResponse.java | 4 +- .../reactive/ServletServerHttpRequest.java | 17 +- .../reactive/ServletServerHttpResponse.java | 4 +- .../reactive/UndertowServerHttpResponse.java | 4 +- .../web/server/DefaultWebServerExchange.java | 32 +++- .../web/server/WebServerExchange.java | 13 +- .../web/server/WebSession.java | 109 +++++++++++ .../web/server/WebToHttpHandlerAdapter.java | 25 ++- .../web/server/WebToHttpHandlerBuilder.java | 20 ++- .../session/ConfigurableWebSession.java | 45 +++++ .../session/CookieWebSessionIdResolver.java | 93 ++++++++++ .../web/server/session/DefaultWebSession.java | 169 ++++++++++++++++++ .../session/DefaultWebSessionManager.java | 157 ++++++++++++++++ .../session/InMemoryWebSessionStore.java | 52 ++++++ .../server/session/WebSessionIdResolver.java | 49 +++++ .../web/server/session/WebSessionManager.java | 48 +++++ .../web/server/session/WebSessionStore.java | 51 ++++++ .../web/server/session/package-info.java | 4 + .../reactive/DispatcherHandlerErrorTests.java | 6 +- .../ResponseStatusExceptionHandlerTests.java | 5 +- .../method/InvocableHandlerMethodTests.java | 4 +- .../RequestMappingHandlerMappingTests.java | 12 +- .../ExceptionHandlingHttpHandlerTests.java | 5 +- .../DefaultWebSessionManagerTests.java | 154 ++++++++++++++++ .../session/WebSessionIntegrationTests.java | 167 +++++++++++++++++ 28 files changed, 1268 insertions(+), 71 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/server/WebSession.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/server/session/ConfigurableWebSession.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/server/session/CookieWebSessionIdResolver.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/server/session/DefaultWebSession.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/server/session/DefaultWebSessionManager.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/server/session/InMemoryWebSessionStore.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/server/session/WebSessionIdResolver.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/server/session/WebSessionManager.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/server/session/WebSessionStore.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/server/session/package-info.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/server/session/DefaultWebSessionManagerTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/server/session/WebSessionIntegrationTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/http/HttpCookie.java b/spring-web-reactive/src/main/java/org/springframework/http/HttpCookie.java index 5021b62614..81a762f352 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/HttpCookie.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/HttpCookie.java @@ -15,6 +15,8 @@ */ package org.springframework.http; +import java.time.Duration; + import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -34,7 +36,7 @@ public final class HttpCookie { private final String value; - private final int maxAge; + private final Duration maxAge; private final String domain; @@ -46,17 +48,17 @@ public final class HttpCookie { private HttpCookie(String name, String value) { - this(name, value, -1, null, null, false, false); + this(name, value, Duration.ofSeconds(-1), null, null, false, false); } - private HttpCookie(String name, String value, int maxAge, String domain, String path, + private HttpCookie(String name, String value, Duration maxAge, String domain, String path, boolean secure, boolean httpOnly) { Assert.hasLength(name, "'name' is required and must not be empty."); - Assert.hasLength(value, "'value' is required and must not be empty."); + Assert.notNull(maxAge); this.name = name; - this.value = value; - this.maxAge = (maxAge > -1 ? maxAge : -1); + this.value = (value != null ? value : ""); + this.maxAge = maxAge; this.domain = domain; this.path = path; this.secure = secure; @@ -85,7 +87,7 @@ public final class HttpCookie { * A negative value means no "Max-Age" attribute in which case the cookie * is removed when the browser is closed. */ - public int getMaxAge() { + public Duration getMaxAge() { return this.maxAge; } @@ -162,7 +164,7 @@ public final class HttpCookie { return new HttpCookieBuilder() { - private int maxAge = -1; + private Duration maxAge = Duration.ofSeconds(-1); private String domain; @@ -174,7 +176,7 @@ public final class HttpCookie { @Override - public HttpCookieBuilder maxAge(int maxAge) { + public HttpCookieBuilder maxAge(Duration maxAge) { this.maxAge = maxAge; return this; } @@ -217,14 +219,14 @@ public final class HttpCookie { public interface HttpCookieBuilder { /** - * Set the cookie "Max-Age" attribute in seconds. + * Set the cookie "Max-Age" attribute. * *

    A positive value indicates when the cookie should expire relative * to the current time. A value of 0 means the cookie should expire * immediately. A negative value results in no "Max-Age" attribute in * which case the cookie is removed when the browser is closed. */ - HttpCookieBuilder maxAge(int maxAge); + HttpCookieBuilder maxAge(Duration maxAge); /** * Set the cookie "Path" attribute. diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java index 9dbcf0a2bb..704ddf4db6 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java @@ -66,25 +66,23 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse { } private Mono applyBeforeCommit() { - return Stream.defer(() -> { - Mono mono = Mono.empty(); - if (this.state.compareAndSet(State.NEW, State.COMMITTING)) { - for (Supplier> action : this.beforeCommitActions) { - mono = mono.after(() -> action.get()); - } - mono = mono.otherwise(ex -> { - // Ignore errors from beforeCommit actions - return Mono.empty(); - }); - mono = mono.after(() -> { - this.state.set(State.COMITTED); - writeHeaders(); - writeCookies(); - return Mono.empty(); - }); + Mono mono = Mono.empty(); + if (this.state.compareAndSet(State.NEW, State.COMMITTING)) { + for (Supplier> action : this.beforeCommitActions) { + mono = mono.after(() -> action.get()); } - return mono; - }).after(); + mono = mono.otherwise(ex -> { + // Ignore errors from beforeCommit actions + return Mono.empty(); + }); + mono = mono.after(() -> { + this.state.set(State.COMITTED); + writeHeaders(); + writeCookies(); + return Mono.empty(); + }); + } + return mono; } /** diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java index 8b0f3bcd19..011b3d599a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java @@ -15,6 +15,8 @@ */ package org.springframework.http.server.reactive; +import java.time.Duration; + import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -73,55 +75,59 @@ public class ReactorServerHttpResponse extends AbstractServerHttpResponse { protected void writeCookies() { for (String name : getHeaders().getCookies().keySet()) { for (HttpCookie httpCookie : getHeaders().getCookies().get(name)) { - Cookie cookie = new ReactorCookie(name, httpCookie); - this.channel.addResponseCookie(name, cookie); + Cookie reactorCookie = new ReactorCookie(httpCookie); + this.channel.addResponseCookie(name, reactorCookie); } } } + + /** + * At present Reactor does not provide a {@link Cookie} implementation. + */ private final static class ReactorCookie extends Cookie { - final HttpCookie httpCookie; - final String name; + private final HttpCookie httpCookie; - public ReactorCookie(String name, HttpCookie httpCookie) { - this.name = name; + + public ReactorCookie(HttpCookie httpCookie) { this.httpCookie = httpCookie; } @Override public String name() { - return name; + return this.httpCookie.getName(); } @Override public String value() { - return httpCookie.getValue(); + return this.httpCookie.getValue(); } @Override public boolean httpOnly() { - return httpCookie.isHttpOnly(); + return this.httpCookie.isHttpOnly(); } @Override public long maxAge() { - return httpCookie.getMaxAge() > -1 ? httpCookie.getMaxAge() : -1; + Duration maxAge = this.httpCookie.getMaxAge(); + return (!maxAge.isNegative() ? maxAge.getSeconds() : -1); } @Override public String domain() { - return httpCookie.getDomain(); + return this.httpCookie.getDomain(); } @Override public String path() { - return httpCookie.getPath(); + return this.httpCookie.getPath(); } @Override public boolean secure() { - return httpCookie.isSecure(); + return this.httpCookie.isSecure(); } } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java index 417e6b0a50..e1ad9ad646 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java @@ -82,8 +82,8 @@ public class RxNettyServerHttpResponse extends AbstractServerHttpResponse { for (String name : getHeaders().getCookies().keySet()) { for (HttpCookie httpCookie : getHeaders().getCookies().get(name)) { Cookie cookie = new DefaultCookie(name, httpCookie.getValue()); - if (httpCookie.getMaxAge() > -1) { - cookie.setMaxAge(httpCookie.getMaxAge()); + if (!httpCookie.getMaxAge().isNegative()) { + cookie.setMaxAge(httpCookie.getMaxAge().getSeconds()); } cookie.setDomain(httpCookie.getDomain()); cookie.setPath(httpCookie.getPath()); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java index 5bc34daa71..cbfb6e7342 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java @@ -113,14 +113,17 @@ public class ServletServerHttpRequest extends AbstractServerHttpRequest { @Override protected void initCookies(Map> map) { - for (Cookie cookie : this.request.getCookies()) { - String name = cookie.getName(); - List list = map.get(name); - if (list == null) { - list = new ArrayList<>(); - map.put(name, list); + Cookie[] cookies = this.request.getCookies(); + if (cookies != null) { + for (Cookie cookie : cookies) { + String name = cookie.getName(); + List list = map.get(name); + if (list == null) { + list = new ArrayList<>(); + map.put(name, list); + } + list.add(HttpCookie.clientCookie(name, cookie.getValue())); } - list.add(HttpCookie.clientCookie(name, cookie.getValue())); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java index 887baa567a..eeb50d96c8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java @@ -91,8 +91,8 @@ public class ServletServerHttpResponse extends AbstractServerHttpResponse { for (String name : getHeaders().getCookies().keySet()) { for (HttpCookie httpCookie : getHeaders().getCookies().get(name)) { Cookie cookie = new Cookie(name, httpCookie.getValue()); - if (httpCookie.getMaxAge() > -1) { - cookie.setMaxAge(httpCookie.getMaxAge()); + if (!httpCookie.getMaxAge().isNegative()) { + cookie.setMaxAge((int) httpCookie.getMaxAge().getSeconds()); } if (httpCookie.getDomain() != null) { cookie.setDomain(httpCookie.getDomain()); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java index c4d8929b34..dcaa12d233 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java @@ -83,8 +83,8 @@ public class UndertowServerHttpResponse extends AbstractServerHttpResponse { for (String name : getHeaders().getCookies().keySet()) { for (HttpCookie httpCookie : getHeaders().getCookies().get(name)) { Cookie cookie = new CookieImpl(name, httpCookie.getValue()); - if (httpCookie.getMaxAge() > -1) { - cookie.setMaxAge(httpCookie.getMaxAge()); + if (!httpCookie.getMaxAge().isNegative()) { + cookie.setMaxAge((int) httpCookie.getMaxAge().getSeconds()); } cookie.setDomain(httpCookie.getDomain()); cookie.setPath(httpCookie.getPath()); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/DefaultWebServerExchange.java b/spring-web-reactive/src/main/java/org/springframework/web/server/DefaultWebServerExchange.java index fe5cb4f5db..49703cddb6 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/DefaultWebServerExchange.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/DefaultWebServerExchange.java @@ -18,9 +18,14 @@ package org.springframework.web.server; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import reactor.core.publisher.FluxProcessor; +import reactor.core.publisher.Mono; +import reactor.core.publisher.Processors; + import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.util.Assert; +import org.springframework.web.server.session.WebSessionManager; /** * Default implementation of {@link WebServerExchange}. @@ -33,14 +38,26 @@ public class DefaultWebServerExchange implements WebServerExchange { private final ServerHttpResponse response; + private final WebSessionManager sessionManager; + + private final Map attributes = new ConcurrentHashMap<>(); + private final Object createSessionLock = new Object(); + + private Mono sessionMono; + + + + public DefaultWebServerExchange(ServerHttpRequest request, ServerHttpResponse response, + WebSessionManager sessionManager) { - public DefaultWebServerExchange(ServerHttpRequest request, ServerHttpResponse response) { Assert.notNull(request, "'request' is required."); Assert.notNull(response, "'response' is required."); + Assert.notNull(response, "'sessionManager' is required."); this.request = request; this.response = response; + this.sessionManager = sessionManager; } @@ -59,4 +76,17 @@ public class DefaultWebServerExchange implements WebServerExchange { return this.attributes; } + @Override + public Mono getSession() { + if (this.sessionMono == null) { + synchronized (this.createSessionLock) { + if (this.sessionMono == null) { + FluxProcessor replay = Processors.replay(1); + this.sessionMono = this.sessionManager.getSession(this).subscribeWith(replay).next(); + } + } + } + return this.sessionMono; + } + } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/WebServerExchange.java b/spring-web-reactive/src/main/java/org/springframework/web/server/WebServerExchange.java index 87fbf7ac3c..53d1006e0a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/WebServerExchange.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/WebServerExchange.java @@ -17,6 +17,8 @@ package org.springframework.web.server; import java.util.Map; +import reactor.core.publisher.Mono; + import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; @@ -30,18 +32,23 @@ import org.springframework.http.server.reactive.ServerHttpResponse; public interface WebServerExchange { /** - * @return the current HTTP request + * Return the current HTTP request. */ ServerHttpRequest getRequest(); /** - * @return the current HTTP response + * Return the current HTTP response. */ ServerHttpResponse getResponse(); /** - * @return mutable map of request attributes for the current exchange + * Return a mutable map of request attributes for the current exchange. */ Map getAttributes(); + /** + * + */ + Mono getSession(); + } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/WebSession.java b/spring-web-reactive/src/main/java/org/springframework/web/server/WebSession.java new file mode 100644 index 0000000000..ca6554cbcf --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/WebSession.java @@ -0,0 +1,109 @@ +/* + * Copyright 2002-2015 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.web.server; + +import java.time.Duration; +import java.time.Instant; +import java.util.Map; + +import reactor.core.publisher.Mono; + +/** + * Main contract for using a server-side session that provides access to session + * attributes across HTTP requests. + * + *

    The creation of a {@code WebSession} instance does not automatically start + * a session thus causing the session id to be sent to the client (typically via + * a cookie). A session starts implicitly when session attributes are added. + * A session may also be created explicitly via {@link #start()}. + * + * @author Rossen Stoyanchev + */ +public interface WebSession { + + /** + * Return a unique session identifier. + */ + String getId(); + + /** + * Return a map that holds session attributes. + */ + Map getAttributes(); + + /** + * Force the creation of a session causing the session id to be sent when + * {@link #save()} is called. + */ + void start(); + + /** + * Whether a session with the client has been started explicitly via + * {@link #start()} or implicitly by adding session attributes. + * If "false" then the session id is not sent to the client and the + * {@link #save()} method is essentially a no-op. + */ + boolean isStarted(); + + /** + * Save the session persisting attributes (e.g. if stored remotely) and also + * sending the session id to the client if the session is new. + *

    Note that a session must be started explicitly via {@link #start()} or + * implicitly by adding attributes or otherwise this method has no effect. + * @return {@code Mono} to indicate completion with success or error + *

    Typically this method should be automatically invoked just before the + * response is committed so applications don't have to by default. + */ + Mono save(); + + /** + * Return {@code true} if the session expired after {@link #getMaxIdleTime() + * maxIdleTime} elapsed. + *

    Typically expiration checks should be automatically made when a session + * is accessed, a new {@code WebSession} instance created if necessary, at + * the start of request processing so that applications don't have to worry + * about expired session by default. + */ + boolean isExpired(); + + /** + * Return the time when the session was created. + */ + Instant getCreationTime(); + + /** + * Return the last time of session access as a result of user activity such + * as an HTTP request. Together with {@link #getMaxIdleTime() + * maxIdleTimeInSeconds} this helps to determine when a session is + * {@link #isExpired() expired}. + */ + Instant getLastAccessTime(); + + /** + * Configure the max amount of time that may elapse after the + * {@link #getLastAccessTime() lastAccessTime} before a session is considered + * expired. A negative value indicates the session should not expire. + */ + void setMaxIdleTime(Duration maxIdleTime); + + /** + * Return the maximum time after the {@link #getLastAccessTime() + * lastAccessTime} before a session expires. A negative time indicates the + * session doesn't expire. + */ + Duration getMaxIdleTime(); + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/WebToHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/server/WebToHttpHandlerAdapter.java index 6b6b203327..c85ba349b4 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/WebToHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/WebToHttpHandlerAdapter.java @@ -23,6 +23,9 @@ import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.HttpHandler; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.util.Assert; +import org.springframework.web.server.session.DefaultWebSessionManager; +import org.springframework.web.server.session.WebSessionManager; /** * Adapt {@link WebHandler} to {@link HttpHandler} also creating the @@ -35,11 +38,31 @@ public class WebToHttpHandlerAdapter extends WebHandlerDecorator implements Http private static Log logger = LogFactory.getLog(WebToHttpHandlerAdapter.class); + private WebSessionManager sessionManager = new DefaultWebSessionManager(); + + public WebToHttpHandlerAdapter(WebHandler delegate) { super(delegate); } + /** + * + * @param sessionManager + */ + public void setSessionManager(WebSessionManager sessionManager) { + Assert.notNull(sessionManager, "'sessionManager' must not be null."); + this.sessionManager = sessionManager; + } + + /** + * Return the configured {@link WebSessionManager}. + */ + public WebSessionManager getSessionManager() { + return this.sessionManager; + } + + @Override public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { WebServerExchange exchange = createWebServerExchange(request, response); @@ -55,7 +78,7 @@ public class WebToHttpHandlerAdapter extends WebHandlerDecorator implements Http } protected WebServerExchange createWebServerExchange(ServerHttpRequest request, ServerHttpResponse response) { - return new DefaultWebServerExchange(request, response); + return new DefaultWebServerExchange(request, response, this.sessionManager); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/WebToHttpHandlerBuilder.java b/spring-web-reactive/src/main/java/org/springframework/web/server/WebToHttpHandlerBuilder.java index 62a133a35e..55dc3960f5 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/WebToHttpHandlerBuilder.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/WebToHttpHandlerBuilder.java @@ -21,6 +21,7 @@ import java.util.List; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; +import org.springframework.web.server.session.WebSessionManager; /** * Assist with building an @@ -43,6 +44,8 @@ public class WebToHttpHandlerBuilder { private final List exceptionHandlers = new ArrayList<>(); + private WebSessionManager sessionManager; + private WebToHttpHandlerBuilder(WebHandler targetHandler) { Assert.notNull(targetHandler, "'targetHandler' must not be null"); @@ -68,17 +71,26 @@ public class WebToHttpHandlerBuilder { return this; } + public WebToHttpHandlerBuilder sessionManager(WebSessionManager sessionManager) { + this.sessionManager = sessionManager; + return this; + } + public WebToHttpHandlerAdapter build() { - WebHandler webHandler = this.targetHandler; + WebHandler handler = this.targetHandler; if (!this.exceptionHandlers.isEmpty()) { WebExceptionHandler[] array = new WebExceptionHandler[this.exceptionHandlers.size()]; - webHandler = new ExceptionHandlingWebHandler(webHandler, this.exceptionHandlers.toArray(array)); + handler = new ExceptionHandlingWebHandler(handler, this.exceptionHandlers.toArray(array)); } if (!this.filters.isEmpty()) { WebFilter[] array = new WebFilter[this.filters.size()]; - webHandler = new FilteringWebHandler(webHandler, this.filters.toArray(array)); + handler = new FilteringWebHandler(handler, this.filters.toArray(array)); } - return new WebToHttpHandlerAdapter(webHandler); + WebToHttpHandlerAdapter adapter = new WebToHttpHandlerAdapter(handler); + if (this.sessionManager != null) { + adapter.setSessionManager(this.sessionManager); + } + return adapter; } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/session/ConfigurableWebSession.java b/spring-web-reactive/src/main/java/org/springframework/web/server/session/ConfigurableWebSession.java new file mode 100644 index 0000000000..6a91b3f219 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/session/ConfigurableWebSession.java @@ -0,0 +1,45 @@ +/* + * 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.web.server.session; + +import java.time.Instant; +import java.util.function.Supplier; + +import reactor.core.publisher.Mono; + +import org.springframework.web.server.WebSession; + +/** + * Extend {@link WebSession} with management operations meant for internal use + * for example by implementations of {@link WebSessionManager}. + * + * @author Rossen Stoyanchev + */ +public interface ConfigurableWebSession extends WebSession { + + /** + * Update the last access time for user-related session activity. + * @param time the time of access + */ + void setLastAccessTime(Instant time); + + /** + * Set the operation to invoke when {@link WebSession#save()} is invoked. + * @param saveOperation the save operation + */ + void setSaveOperation(Supplier> saveOperation); + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/session/CookieWebSessionIdResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/server/session/CookieWebSessionIdResolver.java new file mode 100644 index 0000000000..2584487c2b --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/session/CookieWebSessionIdResolver.java @@ -0,0 +1,93 @@ +/* + * 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.web.server.session; + +import java.time.Duration; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import org.springframework.http.HttpCookie; +import org.springframework.http.HttpHeaders; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.server.WebServerExchange; + +/** + * Cookie-based {@link WebSessionIdResolver}. + * + * @author Rossen Stoyanchev + */ +public class CookieWebSessionIdResolver implements WebSessionIdResolver { + + private String cookieName = "SESSION"; + + private Duration cookieMaxAge = Duration.ofSeconds(-1); + + + /** + * Set the name of the cookie to use for the session id. + *

    By default set to "SESSION". + * @param cookieName the cookie name + */ + public void setCookieName(String cookieName) { + Assert.hasText(cookieName, "'cookieName' must not be empty."); + this.cookieName = cookieName; + } + + /** + * Return the configured cookie name. + */ + public String getCookieName() { + return this.cookieName; + } + + /** + * Set the value for the "Max-Age" attribute of the cookie that holds the + * session id. For the range of values see {@link HttpCookie#getMaxAge()}. + *

    By default set to -1. + * @param maxAge the maxAge duration value + */ + public void setCookieMaxAge(Duration maxAge) { + this.cookieMaxAge = maxAge; + } + + /** + * Return the configured "Max-Age" attribute value for the session cookie. + */ + public Duration getCookieMaxAge() { + return this.cookieMaxAge; + } + + + @Override + public Optional resolveSessionId(WebServerExchange exchange) { + HttpHeaders headers = exchange.getRequest().getHeaders(); + List cookies = headers.getCookies().get(getCookieName()); + return (CollectionUtils.isEmpty(cookies) ? + Optional.empty() : Optional.of(cookies.get(0).getValue())); + } + + @Override + public void setSessionId(WebServerExchange exchange, String id) { + Duration maxAge = (StringUtils.hasText(id) ? getCookieMaxAge() : Duration.ofSeconds(0)); + HttpCookie cookie = HttpCookie.serverCookie(getCookieName(), id).maxAge(maxAge).build(); + HttpHeaders headers = exchange.getResponse().getHeaders(); + headers.getCookies().put(getCookieName(), Collections.singletonList(cookie)); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/session/DefaultWebSession.java b/spring-web-reactive/src/main/java/org/springframework/web/server/session/DefaultWebSession.java new file mode 100644 index 0000000000..0301563994 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/session/DefaultWebSession.java @@ -0,0 +1,169 @@ +/* + * 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.web.server.session; + +import java.io.Serializable; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; + +import reactor.core.publisher.Mono; + +import org.springframework.util.Assert; + +/** + * @author Rossen Stoyanchev + */ +public class DefaultWebSession implements ConfigurableWebSession, Serializable { + + private final String id; + + private final Map attributes; + + private final Clock clock; + + private final Instant creationTime; + + private volatile Instant lastAccessTime; + + private volatile Duration maxIdleTime; + + private AtomicReference state = new AtomicReference<>(); + + private volatile transient Supplier> saveOperation; + + + /** + * Constructor to create a new session. + * @param id the session id + * @param clock for access to current time + */ + public DefaultWebSession(String id, Clock clock) { + Assert.notNull(id, "'id' is required."); + Assert.notNull(clock, "'clock' is required."); + this.id = id; + this.clock = clock; + this.attributes = new ConcurrentHashMap<>(); + this.creationTime = Instant.now(clock); + this.lastAccessTime = this.creationTime; + this.maxIdleTime = Duration.ofMinutes(30); + this.state.set(State.NEW); + } + + /** + * Constructor to load existing session. + * @param id the session id + * @param attributes the attributes of the session + * @param clock for access to current time + * @param creationTime the creation time + * @param lastAccessTime the last access time + * @param maxIdleTime the configured maximum session idle time + */ + public DefaultWebSession(String id, Map attributes, Clock clock, + Instant creationTime, Instant lastAccessTime, Duration maxIdleTime) { + + Assert.notNull(id, "'id' is required."); + Assert.notNull(clock, "'clock' is required."); + this.id = id; + this.attributes = new ConcurrentHashMap<>(attributes); + this.clock = clock; + this.creationTime = creationTime; + this.lastAccessTime = lastAccessTime; + this.maxIdleTime = maxIdleTime; + this.state.set(State.STARTED); + } + + + @Override + public String getId() { + return this.id; + } + + @Override + public Map getAttributes() { + return this.attributes; + } + + @Override + public Instant getCreationTime() { + return this.creationTime; + } + + @Override + public void setLastAccessTime(Instant lastAccessTime) { + this.lastAccessTime = lastAccessTime; + } + + @Override + public Instant getLastAccessTime() { + return this.lastAccessTime; + } + + /** + *

    By default this is set to 30 minutes. + * @param maxIdleTime the max idle time + */ + @Override + public void setMaxIdleTime(Duration maxIdleTime) { + this.maxIdleTime = maxIdleTime; + } + + @Override + public Duration getMaxIdleTime() { + return this.maxIdleTime; + } + + @Override + public void setSaveOperation(Supplier> saveOperation) { + Assert.notNull(saveOperation, "'saveOperation' is required."); + this.saveOperation = saveOperation; + } + + protected Supplier> getSaveOperation() { + return this.saveOperation; + } + + + @Override + public void start() { + this.state.compareAndSet(State.NEW, State.STARTED); + } + + @Override + public boolean isStarted() { + State value = this.state.get(); + return (State.STARTED.equals(value) || (State.NEW.equals(value) && !getAttributes().isEmpty())); + } + + @Override + public Mono save() { + return this.saveOperation.get(); + } + + @Override + public boolean isExpired() { + return (isStarted() && !this.maxIdleTime.isNegative() && + Instant.now(this.clock).minus(this.maxIdleTime).isAfter(this.lastAccessTime)); + } + + + private enum State { NEW, STARTED } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/session/DefaultWebSessionManager.java b/spring-web-reactive/src/main/java/org/springframework/web/server/session/DefaultWebSessionManager.java new file mode 100644 index 0000000000..f374449215 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/session/DefaultWebSessionManager.java @@ -0,0 +1,157 @@ +/* + * 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.web.server.session; + +import java.time.Clock; +import java.time.Instant; +import java.util.Optional; +import java.util.UUID; + +import reactor.core.publisher.Mono; + +import org.springframework.util.Assert; +import org.springframework.web.server.WebServerExchange; +import org.springframework.web.server.WebSession; + + +/** + * Default implementation of {@link WebSessionManager} with a cookie-based web + * session id resolution strategy and simple in-memory session persistence. + * + * @author Rossen Stoyanchev + */ +public class DefaultWebSessionManager implements WebSessionManager { + + private WebSessionIdResolver sessionIdResolver = new CookieWebSessionIdResolver(); + + private WebSessionStore sessionStore = new InMemoryWebSessionStore(); + + private Clock clock = Clock.systemDefaultZone(); + + + /** + * Configure the session id resolution strategy to use. + *

    By default {@link CookieWebSessionIdResolver} is used. + * @param sessionIdResolver the resolver + */ + public void setSessionIdResolver(WebSessionIdResolver sessionIdResolver) { + Assert.notNull(sessionIdResolver, "'sessionIdResolver' is required."); + this.sessionIdResolver = sessionIdResolver; + } + + /** + * Return the configured {@link WebSessionIdResolver}. + */ + public WebSessionIdResolver getSessionIdResolver() { + return this.sessionIdResolver; + } + + /** + * Configure the session persistence strategy to use. + *

    By default {@link InMemoryWebSessionStore} is used. + * @param sessionStore the persistence strategy + */ + public void setSessionStore(WebSessionStore sessionStore) { + Assert.notNull(sessionStore, "'sessionStore' is required."); + this.sessionStore = sessionStore; + } + + /** + * Return the configured {@link WebSessionStore}. + */ + public WebSessionStore getSessionStore() { + return this.sessionStore; + } + + /** + * Configure the {@link Clock} for access to current time. During tests you + * may use {code Clock.offset(clock, Duration.ofMinutes(-31))} to set the + * clock back for example to test changes after sessions expire. + *

    By default {@link Clock#systemDefaultZone()} is used. + * @param clock the clock to use + */ + public void setClock(Clock clock) { + Assert.notNull(clock, "'clock' is required."); + this.clock = clock; + } + + /** + * Return the configured clock for access to current time. + */ + public Clock getClock() { + return this.clock; + } + + + @Override + public Mono getSession(WebServerExchange exchange) { + return Mono.fromCallable(() -> getSessionIdResolver().resolveSessionId(exchange)) + .where(Optional::isPresent) + .map(Optional::get) + .then(this.sessionStore::retrieveSession) + .then(session -> validateSession(exchange, session)) + .otherwiseIfEmpty(createSession(exchange)) + .map(session -> extendSession(exchange, session)); + } + + protected Mono validateSession(WebServerExchange exchange, WebSession session) { + if (session.isExpired()) { + this.sessionIdResolver.setSessionId(exchange, ""); + return this.sessionStore.removeSession(session.getId()).after(Mono::empty); + } + else { + return Mono.just(session); + } + } + + protected Mono createSession(WebServerExchange exchange) { + String sessionId = UUID.randomUUID().toString(); + WebSession session = new DefaultWebSession(sessionId, getClock()); + return Mono.just(session); + } + + protected WebSession extendSession(WebServerExchange exchange, WebSession session) { + if (session instanceof ConfigurableWebSession) { + ConfigurableWebSession managed = (ConfigurableWebSession) session; + managed.setSaveOperation(() -> saveSession(exchange, session)); + managed.setLastAccessTime(Instant.now(getClock())); + } + exchange.getResponse().beforeCommit(session::save); + return session; + } + + protected Mono saveSession(WebServerExchange exchange, WebSession session) { + + Assert.isTrue(!session.isExpired(), "Sessions are checked for expiration and have their " + + "access time updated when first accessed during request processing. " + + "However this session is expired meaning that maxIdleTime elapsed " + + "since then and before the call to session.save()."); + + if (!session.isStarted()) { + return Mono.empty(); + } + + // Force explicit start + session.start(); + + Optional requestedId = getSessionIdResolver().resolveSessionId(exchange); + if (!requestedId.isPresent() || !session.getId().equals(requestedId.get())) { + this.sessionIdResolver.setSessionId(exchange, session.getId()); + } + return this.sessionStore.storeSession(session); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/session/InMemoryWebSessionStore.java b/spring-web-reactive/src/main/java/org/springframework/web/server/session/InMemoryWebSessionStore.java new file mode 100644 index 0000000000..7e1bb388a3 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/session/InMemoryWebSessionStore.java @@ -0,0 +1,52 @@ +/* + * 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.web.server.session; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import reactor.core.publisher.Mono; + +import org.springframework.web.server.WebSession; + +/** + * Simple Map-based storage for {@link WebSession} instances. + * + * @author Rossen Stoyanchev + */ +public class InMemoryWebSessionStore implements WebSessionStore { + + private final Map sessions = new ConcurrentHashMap<>(); + + + @Override + public Mono storeSession(WebSession session) { + this.sessions.put(session.getId(), session); + return Mono.empty(); + } + + @Override + public Mono retrieveSession(String id) { + return (this.sessions.containsKey(id) ? Mono.just(this.sessions.get(id)) : Mono.empty()); + } + + @Override + public Mono removeSession(String id) { + this.sessions.remove(id); + return Mono.empty(); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/session/WebSessionIdResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/server/session/WebSessionIdResolver.java new file mode 100644 index 0000000000..61c28260f2 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/session/WebSessionIdResolver.java @@ -0,0 +1,49 @@ +/* + * 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.web.server.session; + +import java.util.Optional; + +import org.springframework.web.server.WebServerExchange; +import org.springframework.web.server.WebSession; + + +/** + * Contract for session id resolution strategies. Allows for session id + * resolution through the request and for sending the session id to the + * client through the response. + * + * @author Rossen Stoyanchev + * @see CookieWebSessionIdResolver + */ +public interface WebSessionIdResolver { + + /** + * Resolve the session id associated with the request. + * @param exchange the current exchange + * @return the session id if present + */ + Optional resolveSessionId(WebServerExchange exchange); + + /** + * Send the given session id to the client or if the session id is "null" + * instruct the client to end the current session. + * @param exchange the current exchange + * @param sessionId the session id + */ + void setSessionId(WebServerExchange exchange, String sessionId); + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/session/WebSessionManager.java b/spring-web-reactive/src/main/java/org/springframework/web/server/session/WebSessionManager.java new file mode 100644 index 0000000000..63909b072e --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/session/WebSessionManager.java @@ -0,0 +1,48 @@ +/* + * 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.web.server.session; + +import reactor.core.publisher.Mono; + +import org.springframework.web.server.WebServerExchange; +import org.springframework.web.server.WebSession; + +/** + * Main contract abstracting support for access to {@link WebSession} instances + * associated with HTTP requests as well as the subsequent management such as + * persistence and others. + * + *

    The {@link DefaultWebSessionManager} implementation in turn delegates to + * {@link WebSessionIdResolver} and {@link WebSessionStore} which abstract + * underlying concerns related to the management of web sessions. + * + * @author Rossen Stoyanchev + * @see WebSessionIdResolver + * @see WebSessionStore + */ +public interface WebSessionManager { + + /** + * Return the {@link WebSession} for the given exchange. Always guaranteed + * to return an instance either matching to the session id requested by the + * client, or with a new session id either because the client did not + * specify one or because the underlying session had expired. + * @param exchange the current exchange + * @return {@code Mono} for async access to the session + */ + Mono getSession(WebServerExchange exchange); + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/session/WebSessionStore.java b/spring-web-reactive/src/main/java/org/springframework/web/server/session/WebSessionStore.java new file mode 100644 index 0000000000..998e298e69 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/session/WebSessionStore.java @@ -0,0 +1,51 @@ +/* + * 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.web.server.session; + +import reactor.core.publisher.Mono; + +import org.springframework.web.server.WebSession; + +/** + * Strategy for {@link WebSession} persistence. + * + * @author Rossen Stoyanchev + * @since 4.3 + */ +public interface WebSessionStore { + + /** + * Store the given session. + * @param session the session to store + * @return {@code Mono} for completion notification + */ + Mono storeSession(WebSession session); + + /** + * Load the session for the given session id. + * @param sessionId the session to load + * @return {@code Mono} for async access to the loaded session + */ + Mono retrieveSession(String sessionId); + + /** + * Remove the session with the given id. + * @param sessionId the session to remove + * @return {@code Mono} for completion notification + */ + Mono removeSession(String sessionId); + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/session/package-info.java b/spring-web-reactive/src/main/java/org/springframework/web/server/session/package-info.java new file mode 100644 index 0000000000..57b15561b0 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/session/package-info.java @@ -0,0 +1,4 @@ +/** + * Support for a user session. + */ +package org.springframework.web.server.session; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java index 033148a846..c609536cde 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java @@ -57,9 +57,11 @@ import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilterChain; import org.springframework.web.server.WebHandler; import org.springframework.web.server.WebServerExchange; +import org.springframework.web.server.session.WebSessionManager; import static org.hamcrest.CoreMatchers.startsWith; import static org.junit.Assert.*; +import static org.mockito.Mockito.mock; /** * Test the effect of exceptions at different stages of request processing by @@ -91,9 +93,11 @@ public class DispatcherHandlerErrorTests { this.dispatcherHandler = new DispatcherHandler(); this.dispatcherHandler.setApplicationContext(appContext); + WebSessionManager sessionManager = mock(WebSessionManager.class); + this.request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); this.response = new MockServerHttpResponse(); - this.exchange = new DefaultWebServerExchange(this.request, this.response); + this.exchange = new DefaultWebServerExchange(this.request, this.response, sessionManager); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java index 8a992da11b..6c48305a88 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java @@ -31,10 +31,12 @@ import org.springframework.http.server.reactive.MockServerHttpResponse; import org.springframework.web.ResponseStatusException; import org.springframework.web.server.DefaultWebServerExchange; import org.springframework.web.server.WebServerExchange; +import org.springframework.web.server.session.WebSessionManager; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; /** * @author Rossen Stoyanchev @@ -52,8 +54,9 @@ public class ResponseStatusExceptionHandlerTests { public void setUp() throws Exception { this.handler = new ResponseStatusExceptionHandler(); MockServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/path")); + WebSessionManager sessionManager = mock(WebSessionManager.class); this.response = new MockServerHttpResponse(); - this.exchange = new DefaultWebServerExchange(request, this.response); + this.exchange = new DefaultWebServerExchange(request, this.response, sessionManager); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java index c0385cde11..903de4bb19 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java @@ -37,6 +37,7 @@ import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.method.annotation.RequestParamArgumentResolver; import org.springframework.web.server.DefaultWebServerExchange; import org.springframework.web.server.WebServerExchange; +import org.springframework.web.server.session.WebSessionManager; import static org.junit.Assert.assertEquals; import static org.mockito.Matchers.any; @@ -56,8 +57,9 @@ public class InvocableHandlerMethodTests { @Before public void setUp() throws Exception { + WebSessionManager sessionManager = mock(WebSessionManager.class); this.request = mock(ServerHttpRequest.class); - this.exchange = new DefaultWebServerExchange(request, mock(ServerHttpResponse.class)); + this.exchange = new DefaultWebServerExchange(request, mock(ServerHttpResponse.class), sessionManager); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java index 8ce0f57628..c14153c954 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java @@ -36,10 +36,12 @@ import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.method.HandlerMethod; import org.springframework.web.server.DefaultWebServerExchange; import org.springframework.web.server.WebServerExchange; +import org.springframework.web.server.session.WebSessionManager; import static java.util.stream.Collectors.toList; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.mock; /** * @author Sebastien Deleuze @@ -62,7 +64,9 @@ public class RequestMappingHandlerMappingTests { @Test public void path() throws Exception { ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("boo")); - WebServerExchange exchange = new DefaultWebServerExchange(request, new MockServerHttpResponse()); + MockServerHttpResponse response = new MockServerHttpResponse(); + WebSessionManager sessionManager = mock(WebSessionManager.class); + WebServerExchange exchange = new DefaultWebServerExchange(request, response, sessionManager); Publisher handlerPublisher = this.mapping.getHandler(exchange); HandlerMethod handlerMethod = toHandlerMethod(handlerPublisher); assertEquals(TestController.class.getMethod("boo"), handlerMethod.getMethod()); @@ -71,13 +75,15 @@ public class RequestMappingHandlerMappingTests { @Test public void method() throws Exception { ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.POST, new URI("foo")); - WebServerExchange exchange = new DefaultWebServerExchange(request, new MockServerHttpResponse()); + MockServerHttpResponse response = new MockServerHttpResponse(); + WebSessionManager sessionManager = mock(WebSessionManager.class); + WebServerExchange exchange = new DefaultWebServerExchange(request, response, sessionManager); Publisher handlerPublisher = this.mapping.getHandler(exchange); HandlerMethod handlerMethod = toHandlerMethod(handlerPublisher); assertEquals(TestController.class.getMethod("postFoo"), handlerMethod.getMethod()); request = new MockServerHttpRequest(HttpMethod.GET, new URI("foo")); - exchange = new DefaultWebServerExchange(request, new MockServerHttpResponse()); + exchange = new DefaultWebServerExchange(request, new MockServerHttpResponse(), sessionManager); handlerPublisher = this.mapping.getHandler(exchange); handlerMethod = toHandlerMethod(handlerPublisher); assertEquals(TestController.class.getMethod("getFoo"), handlerMethod.getMethod()); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/server/ExceptionHandlingHttpHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/server/ExceptionHandlingHttpHandlerTests.java index 0360c17ace..e640ceb486 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/server/ExceptionHandlingHttpHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/server/ExceptionHandlingHttpHandlerTests.java @@ -26,8 +26,10 @@ import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.MockServerHttpRequest; import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.web.server.session.WebSessionManager; import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; /** * @author Rossen Stoyanchev @@ -45,9 +47,10 @@ public class ExceptionHandlingHttpHandlerTests { @Before public void setUp() throws Exception { URI uri = new URI("http://localhost:8080"); + WebSessionManager sessionManager = mock(WebSessionManager.class); MockServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, uri); this.response = new MockServerHttpResponse(); - this.exchange = new DefaultWebServerExchange(request, this.response); + this.exchange = new DefaultWebServerExchange(request, this.response, sessionManager); this.targetHandler = new StubWebHandler(new IllegalStateException("boo")); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/server/session/DefaultWebSessionManagerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/server/session/DefaultWebSessionManagerTests.java new file mode 100644 index 0000000000..37c676fc6e --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/server/session/DefaultWebSessionManagerTests.java @@ -0,0 +1,154 @@ +/* + * 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.web.server.session; + +import java.net.URI; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.util.Optional; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.http.HttpMethod; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.web.server.DefaultWebServerExchange; +import org.springframework.web.server.WebServerExchange; +import org.springframework.web.server.WebSession; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +/** + * @author Rossen Stoyanchev + */ +public class DefaultWebSessionManagerTests { + + private DefaultWebSessionManager manager; + + private TestWebSessionIdResolver idResolver; + + private DefaultWebServerExchange exchange; + + + @Before + public void setUp() throws Exception { + this.idResolver = new TestWebSessionIdResolver(); + this.manager = new DefaultWebSessionManager(); + this.manager.setSessionIdResolver(this.idResolver); + + MockServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/path")); + MockServerHttpResponse response = new MockServerHttpResponse(); + this.exchange = new DefaultWebServerExchange(request, response, this.manager); + } + + + @Test + public void getSessionPassive() throws Exception { + this.idResolver.setIdToResolve(Optional.empty()); + WebSession session = this.manager.getSession(this.exchange).get(); + + assertNotNull(session); + assertFalse(session.isStarted()); + assertFalse(session.isExpired()); + + session.save(); + + assertFalse(this.idResolver.getId().isPresent()); + assertNull(this.manager.getSessionStore().retrieveSession(session.getId()).get()); + } + + @Test + public void getSessionForceCreate() throws Exception { + this.idResolver.setIdToResolve(Optional.empty()); + WebSession session = this.manager.getSession(this.exchange).get(); + session.start(); + session.save(); + + String id = session.getId(); + assertTrue(this.idResolver.getId().isPresent()); + assertEquals(id, this.idResolver.getId().get()); + assertSame(session, this.manager.getSessionStore().retrieveSession(id).get()); + } + + @Test + public void getSessionAddAttribute() throws Exception { + this.idResolver.setIdToResolve(Optional.empty()); + WebSession session = this.manager.getSession(this.exchange).get(); + session.getAttributes().put("foo", "bar"); + session.save(); + + assertTrue(this.idResolver.getId().isPresent()); + } + + @Test + public void getSessionExisting() throws Exception { + DefaultWebSession existing = new DefaultWebSession("1", Clock.systemDefaultZone()); + this.manager.getSessionStore().storeSession(existing); + + this.idResolver.setIdToResolve(Optional.of("1")); + WebSession actual = this.manager.getSession(this.exchange).get(); + assertSame(existing, actual); + } + + @Test + public void getSessionExistingExpired() throws Exception { + Clock clock = Clock.systemDefaultZone(); + DefaultWebSession existing = new DefaultWebSession("1", clock); + existing.start(); + existing.setLastAccessTime(Instant.now(clock).minus(Duration.ofMinutes(31))); + this.manager.getSessionStore().storeSession(existing); + + this.idResolver.setIdToResolve(Optional.of("1")); + WebSession actual = this.manager.getSession(this.exchange).get(); + assertNotSame(existing, actual); + } + + + private static class TestWebSessionIdResolver implements WebSessionIdResolver { + + private Optional idToResolve = Optional.empty(); + + private Optional id = Optional.empty(); + + + public void setIdToResolve(Optional idToResolve) { + this.idToResolve = idToResolve; + } + + public Optional getId() { + return this.id; + } + + @Override + public Optional resolveSessionId(WebServerExchange exchange) { + return this.idToResolve; + } + + @Override + public void setSessionId(WebServerExchange exchange, Optional sessionId) { + this.id = sessionId; + } + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/server/session/WebSessionIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/server/session/WebSessionIntegrationTests.java new file mode 100644 index 0000000000..c4e80f0d14 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/server/session/WebSessionIntegrationTests.java @@ -0,0 +1,167 @@ +/* + * 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.web.server.session; + +import java.net.URI; +import java.net.URISyntaxException; +import java.time.Clock; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; +import reactor.core.publisher.Mono; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.http.server.reactive.AbstractHttpHandlerIntegrationTests; +import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.util.StringUtils; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.server.WebHandler; +import org.springframework.web.server.WebServerExchange; +import org.springframework.web.server.WebToHttpHandlerBuilder; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + + +/** + * @author Rossen Stoyanchev + */ +public class WebSessionIntegrationTests extends AbstractHttpHandlerIntegrationTests { + + private RestTemplate restTemplate; + + private DefaultWebSessionManager sessionManager; + + private TestWebHandler handler; + + + @Override + public void setup() throws Exception { + super.setup(); + this.restTemplate = new RestTemplate(); + } + + protected URI createUri(String pathAndQuery) throws URISyntaxException { + boolean prefix = !StringUtils.hasText(pathAndQuery) || !pathAndQuery.startsWith("/"); + pathAndQuery = (prefix ? "/" + pathAndQuery : pathAndQuery); + return new URI("http://localhost:" + port + pathAndQuery); + } + + @Override + protected HttpHandler createHttpHandler() { + this.sessionManager = new DefaultWebSessionManager(); + this.handler = new TestWebHandler(); + return WebToHttpHandlerBuilder.webHandler(this.handler).sessionManager(this.sessionManager).build(); + } + + @Test + public void createSession() throws Exception { + RequestEntity request = RequestEntity.get(createUri("/")).build(); + ResponseEntity response = this.restTemplate.exchange(request, Void.class); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + String id = extractSessionId(response.getHeaders()); + assertNotNull(id); + assertEquals(1, this.handler.getCount()); + + request = RequestEntity.get(createUri("/")).header("Cookie", "SESSION=" + id).build(); + response = this.restTemplate.exchange(request, Void.class); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertNull(response.getHeaders().get("Set-Cookie")); + assertEquals(2, this.handler.getCount()); + } + + @Test + public void expiredSession() throws Exception { + RequestEntity request = RequestEntity.get(createUri("/")).build(); + ResponseEntity response = this.restTemplate.exchange(request, Void.class); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + String id = extractSessionId(response.getHeaders()); + assertNotNull(id); + assertEquals(1, this.handler.getCount()); + + // Set clock back 31 minutes + Clock clock = this.sessionManager.getClock(); + this.sessionManager.setClock(Clock.offset(clock, Duration.ofMinutes(-31))); + + // Access again to update lastAccessTime + request = RequestEntity.get(createUri("/")).header("Cookie", "SESSION=" + id).build(); + response = this.restTemplate.exchange(request, Void.class); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertNull(response.getHeaders().get("Set-Cookie")); + assertEquals(2, this.handler.getCount()); + + // Now it should be expired + request = RequestEntity.get(createUri("/")).header("Cookie", "SESSION=" + id).build(); + response = this.restTemplate.exchange(request, Void.class); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + id = extractSessionId(response.getHeaders()); + assertNotNull("Expected new session id", id); + assertEquals("Expected new session attribute", 1, this.handler.getCount()); + } + + + // No client side HttpCookie support yet + + private String extractSessionId(HttpHeaders headers) { + List headerValues = headers.get("Set-Cookie"); + assertNotNull(headerValues); + assertEquals(1, headerValues.size()); + + List data = new ArrayList<>(); + for (String s : headerValues.get(0).split(";")){ + if (s.startsWith("SESSION=")) { + return s.substring("SESSION=".length()); + } + } + return null; + } + + private static class TestWebHandler implements WebHandler { + + private AtomicInteger currentValue = new AtomicInteger(); + + + public int getCount() { + return this.currentValue.get(); + } + + @Override + public Mono handle(WebServerExchange exchange) { + return exchange.getSession().map(session -> { + Map map = session.getAttributes(); + int value = (map.get("counter") != null ? (int) map.get("counter") : 0); + value++; + map.put("counter", value); + this.currentValue.set(value); + return session; + }).after(); + } + } + +} From e59b927fd108d6173c5d5fd8095d2fcb5ab9f135 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 27 Jan 2016 15:40:35 -0500 Subject: [PATCH 199/498] Refine web.server package structure Introduce adapter and handler sub-packages under web.server following a review prompted by the addition of the session package and the package cycle it brought in based on dependency on session.WebSessionManager. --- .../springframework/web/server/WebHandler.java | 3 +++ .../{ => adapter}/DefaultWebServerExchange.java | 4 +++- .../{ => adapter}/WebToHttpHandlerAdapter.java | 5 ++++- .../{ => adapter}/WebToHttpHandlerBuilder.java | 10 ++++++++-- .../web/server/adapter/package-info.java | 6 ++++++ .../ExceptionHandlingWebHandler.java | 5 ++++- .../{ => handler}/FilteringWebHandler.java | 7 ++++++- .../{ => handler}/WebHandlerDecorator.java | 4 +++- .../web/server/handler/package-info.java | 4 ++++ .../web/server/package-info.java | 2 +- .../web/server/session/package-info.java | 2 +- .../reactive/DispatcherHandlerErrorTests.java | 6 +++--- .../ResponseStatusExceptionHandlerTests.java | 2 +- ...SimpleUrlHandlerMappingIntegrationTests.java | 2 +- .../method/InvocableHandlerMethodTests.java | 2 +- .../RequestMappingHandlerMappingTests.java | 2 +- .../RequestMappingIntegrationTests.java | 2 +- .../ExceptionHandlingHttpHandlerTests.java | 7 ++++++- .../{ => handler}/FilteringWebHandlerTests.java | 10 +++++++--- .../session/DefaultWebSessionManagerTests.java | 17 ++++++++--------- .../session/WebSessionIntegrationTests.java | 2 +- 21 files changed, 73 insertions(+), 31 deletions(-) rename spring-web-reactive/src/main/java/org/springframework/web/server/{ => adapter}/DefaultWebServerExchange.java (94%) rename spring-web-reactive/src/main/java/org/springframework/web/server/{ => adapter}/WebToHttpHandlerAdapter.java (92%) rename spring-web-reactive/src/main/java/org/springframework/web/server/{ => adapter}/WebToHttpHandlerBuilder.java (87%) create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/server/adapter/package-info.java rename spring-web-reactive/src/main/java/org/springframework/web/server/{ => handler}/ExceptionHandlingWebHandler.java (92%) rename spring-web-reactive/src/main/java/org/springframework/web/server/{ => handler}/FilteringWebHandler.java (87%) rename spring-web-reactive/src/main/java/org/springframework/web/server/{ => handler}/WebHandlerDecorator.java (89%) create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/server/handler/package-info.java rename spring-web-reactive/src/test/java/org/springframework/web/server/{ => handler}/ExceptionHandlingHttpHandlerTests.java (92%) rename spring-web-reactive/src/test/java/org/springframework/web/server/{ => handler}/FilteringWebHandlerTests.java (91%) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/WebHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/server/WebHandler.java index e0566e51f0..c5cd2883f7 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/WebHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/WebHandler.java @@ -18,6 +18,9 @@ package org.springframework.web.server; import reactor.core.publisher.Mono; +import org.springframework.web.server.adapter.WebToHttpHandlerAdapter; +import org.springframework.web.server.adapter.WebToHttpHandlerBuilder; + /** * Contract to handle a web server exchange. * diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/DefaultWebServerExchange.java b/spring-web-reactive/src/main/java/org/springframework/web/server/adapter/DefaultWebServerExchange.java similarity index 94% rename from spring-web-reactive/src/main/java/org/springframework/web/server/DefaultWebServerExchange.java rename to spring-web-reactive/src/main/java/org/springframework/web/server/adapter/DefaultWebServerExchange.java index 49703cddb6..e0aa538652 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/DefaultWebServerExchange.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/adapter/DefaultWebServerExchange.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.web.server; +package org.springframework.web.server.adapter; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -25,6 +25,8 @@ import reactor.core.publisher.Processors; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.util.Assert; +import org.springframework.web.server.WebServerExchange; +import org.springframework.web.server.WebSession; import org.springframework.web.server.session.WebSessionManager; /** diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/WebToHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/server/adapter/WebToHttpHandlerAdapter.java similarity index 92% rename from spring-web-reactive/src/main/java/org/springframework/web/server/WebToHttpHandlerAdapter.java rename to spring-web-reactive/src/main/java/org/springframework/web/server/adapter/WebToHttpHandlerAdapter.java index c85ba349b4..bb67feab84 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/WebToHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/adapter/WebToHttpHandlerAdapter.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.web.server; +package org.springframework.web.server.adapter; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -24,6 +24,9 @@ import org.springframework.http.server.reactive.HttpHandler; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.util.Assert; +import org.springframework.web.server.WebHandler; +import org.springframework.web.server.handler.WebHandlerDecorator; +import org.springframework.web.server.WebServerExchange; import org.springframework.web.server.session.DefaultWebSessionManager; import org.springframework.web.server.session.WebSessionManager; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/WebToHttpHandlerBuilder.java b/spring-web-reactive/src/main/java/org/springframework/web/server/adapter/WebToHttpHandlerBuilder.java similarity index 87% rename from spring-web-reactive/src/main/java/org/springframework/web/server/WebToHttpHandlerBuilder.java rename to spring-web-reactive/src/main/java/org/springframework/web/server/adapter/WebToHttpHandlerBuilder.java index 55dc3960f5..97ca700f23 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/WebToHttpHandlerBuilder.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/adapter/WebToHttpHandlerBuilder.java @@ -13,14 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.web.server; +package org.springframework.web.server.adapter; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import org.springframework.http.server.reactive.HttpHandler; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; +import org.springframework.web.server.WebExceptionHandler; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebHandler; +import org.springframework.web.server.handler.ExceptionHandlingWebHandler; +import org.springframework.web.server.handler.FilteringWebHandler; import org.springframework.web.server.session.WebSessionManager; /** @@ -76,7 +82,7 @@ public class WebToHttpHandlerBuilder { return this; } - public WebToHttpHandlerAdapter build() { + public HttpHandler build() { WebHandler handler = this.targetHandler; if (!this.exceptionHandlers.isEmpty()) { WebExceptionHandler[] array = new WebExceptionHandler[this.exceptionHandlers.size()]; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/adapter/package-info.java b/spring-web-reactive/src/main/java/org/springframework/web/server/adapter/package-info.java new file mode 100644 index 0000000000..f3cb8231ed --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/adapter/package-info.java @@ -0,0 +1,6 @@ +/** + * Implementation support to adapt + * {@link org.springframework.web.server Spring web server} to the underlying + * {@link org.springframework.http.server.reactive HTTP server} layer. + */ +package org.springframework.web.server.adapter; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/ExceptionHandlingWebHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/server/handler/ExceptionHandlingWebHandler.java similarity index 92% rename from spring-web-reactive/src/main/java/org/springframework/web/server/ExceptionHandlingWebHandler.java rename to spring-web-reactive/src/main/java/org/springframework/web/server/handler/ExceptionHandlingWebHandler.java index 5c662f0411..5f75c3a8dc 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/ExceptionHandlingWebHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/handler/ExceptionHandlingWebHandler.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.web.server; +package org.springframework.web.server.handler; import java.util.Arrays; import java.util.Collections; @@ -24,6 +24,9 @@ import org.apache.commons.logging.LogFactory; import reactor.core.publisher.Mono; import org.springframework.http.HttpStatus; +import org.springframework.web.server.WebExceptionHandler; +import org.springframework.web.server.WebHandler; +import org.springframework.web.server.WebServerExchange; /** * {@code WebHandler} that decorates another with exception handling using one diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/FilteringWebHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/server/handler/FilteringWebHandler.java similarity index 87% rename from spring-web-reactive/src/main/java/org/springframework/web/server/FilteringWebHandler.java rename to spring-web-reactive/src/main/java/org/springframework/web/server/handler/FilteringWebHandler.java index 4228c8710a..9b55bf98ba 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/FilteringWebHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/handler/FilteringWebHandler.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.web.server; +package org.springframework.web.server.handler; import java.util.Arrays; import java.util.Collections; @@ -21,6 +21,11 @@ import java.util.List; import reactor.core.publisher.Mono; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; +import org.springframework.web.server.WebHandler; +import org.springframework.web.server.WebServerExchange; + /** * {@code WebHandler} that decorates another with a chain of {@link WebFilter}s. * diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/WebHandlerDecorator.java b/spring-web-reactive/src/main/java/org/springframework/web/server/handler/WebHandlerDecorator.java similarity index 89% rename from spring-web-reactive/src/main/java/org/springframework/web/server/WebHandlerDecorator.java rename to spring-web-reactive/src/main/java/org/springframework/web/server/handler/WebHandlerDecorator.java index 83cd7a8907..151ce5b115 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/WebHandlerDecorator.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/handler/WebHandlerDecorator.java @@ -13,11 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.web.server; +package org.springframework.web.server.handler; import reactor.core.publisher.Mono; import org.springframework.util.Assert; +import org.springframework.web.server.WebHandler; +import org.springframework.web.server.WebServerExchange; /** * Base class for a {@link WebHandler} that decorates and delegates to another. diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/handler/package-info.java b/spring-web-reactive/src/main/java/org/springframework/web/server/handler/package-info.java new file mode 100644 index 0000000000..95c56f6941 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/handler/package-info.java @@ -0,0 +1,4 @@ +/** + * Provides WebHandler implementations. + */ +package org.springframework.web.server.handler; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/package-info.java b/spring-web-reactive/src/main/java/org/springframework/web/server/package-info.java index 5d344fadcd..dc65db1eb4 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/package-info.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/package-info.java @@ -15,6 +15,6 @@ */ /** - * Web server infrastructure like filter, exchange (request + response), etc. + * Foundational Spring web server support. */ package org.springframework.web.server; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/session/package-info.java b/spring-web-reactive/src/main/java/org/springframework/web/server/session/package-info.java index 57b15561b0..1a62756b01 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/session/package-info.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/session/package-info.java @@ -1,4 +1,4 @@ /** - * Support for a user session. + * Web session support. */ package org.springframework.web.server.session; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java index c609536cde..b9d1dda3f7 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java @@ -49,9 +49,9 @@ import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.reactive.method.annotation.RequestMappingHandlerAdapter; import org.springframework.web.reactive.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.reactive.method.annotation.ResponseBodyResultHandler; -import org.springframework.web.server.DefaultWebServerExchange; -import org.springframework.web.server.ExceptionHandlingWebHandler; -import org.springframework.web.server.FilteringWebHandler; +import org.springframework.web.server.adapter.DefaultWebServerExchange; +import org.springframework.web.server.handler.ExceptionHandlingWebHandler; +import org.springframework.web.server.handler.FilteringWebHandler; import org.springframework.web.server.WebExceptionHandler; import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilterChain; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java index 6c48305a88..0bd90168d2 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java @@ -29,7 +29,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.MockServerHttpRequest; import org.springframework.http.server.reactive.MockServerHttpResponse; import org.springframework.web.ResponseStatusException; -import org.springframework.web.server.DefaultWebServerExchange; +import org.springframework.web.server.adapter.DefaultWebServerExchange; import org.springframework.web.server.WebServerExchange; import org.springframework.web.server.session.WebSessionManager; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java index 5546b4cb61..859201d754 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java @@ -40,7 +40,7 @@ import org.springframework.web.reactive.DispatcherHandler; import org.springframework.web.reactive.ResponseStatusExceptionHandler; import org.springframework.web.server.WebHandler; import org.springframework.web.server.WebServerExchange; -import org.springframework.web.server.WebToHttpHandlerBuilder; +import org.springframework.web.server.adapter.WebToHttpHandlerBuilder; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java index 903de4bb19..c9da12c17a 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java @@ -35,7 +35,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.method.annotation.RequestParamArgumentResolver; -import org.springframework.web.server.DefaultWebServerExchange; +import org.springframework.web.server.adapter.DefaultWebServerExchange; import org.springframework.web.server.WebServerExchange; import org.springframework.web.server.session.WebSessionManager; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java index c14153c954..bd2d143dca 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java @@ -34,7 +34,7 @@ import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.method.HandlerMethod; -import org.springframework.web.server.DefaultWebServerExchange; +import org.springframework.web.server.adapter.DefaultWebServerExchange; import org.springframework.web.server.WebServerExchange; import org.springframework.web.server.session.WebSessionManager; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java index 84d3f55c34..d0331c8c23 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java @@ -64,7 +64,7 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; import org.springframework.web.reactive.DispatcherHandler; import org.springframework.web.reactive.handler.SimpleHandlerResultHandler; -import org.springframework.web.server.WebToHttpHandlerBuilder; +import org.springframework.web.server.adapter.WebToHttpHandlerBuilder; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/server/ExceptionHandlingHttpHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/server/handler/ExceptionHandlingHttpHandlerTests.java similarity index 92% rename from spring-web-reactive/src/test/java/org/springframework/web/server/ExceptionHandlingHttpHandlerTests.java rename to spring-web-reactive/src/test/java/org/springframework/web/server/handler/ExceptionHandlingHttpHandlerTests.java index e640ceb486..ef9e3cafaa 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/server/ExceptionHandlingHttpHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/server/handler/ExceptionHandlingHttpHandlerTests.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.web.server; +package org.springframework.web.server.handler; import java.net.URI; @@ -26,6 +26,11 @@ import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.MockServerHttpRequest; import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.web.server.WebExceptionHandler; +import org.springframework.web.server.WebHandler; +import org.springframework.web.server.WebServerExchange; +import org.springframework.web.server.handler.ExceptionHandlingWebHandler; +import org.springframework.web.server.adapter.DefaultWebServerExchange; import org.springframework.web.server.session.WebSessionManager; import static org.junit.Assert.assertEquals; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/server/FilteringWebHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/server/handler/FilteringWebHandlerTests.java similarity index 91% rename from spring-web-reactive/src/test/java/org/springframework/web/server/FilteringWebHandlerTests.java rename to spring-web-reactive/src/test/java/org/springframework/web/server/handler/FilteringWebHandlerTests.java index aaddff9233..0deeb1f7d3 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/server/FilteringWebHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/server/handler/FilteringWebHandlerTests.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.web.server; +package org.springframework.web.server.handler; import java.net.URI; @@ -30,10 +30,14 @@ import org.springframework.http.server.reactive.MockServerHttpRequest; import org.springframework.http.server.reactive.MockServerHttpResponse; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; +import org.springframework.web.server.WebHandler; +import org.springframework.web.server.WebServerExchange; +import org.springframework.web.server.adapter.WebToHttpHandlerBuilder; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; /** * @author Rossen Stoyanchev @@ -104,7 +108,7 @@ public class FilteringWebHandlerTests { assertTrue(webHandler.invoked()); } - private WebToHttpHandlerAdapter createHttpHandler(StubWebHandler webHandler, WebFilter... filters) { + private HttpHandler createHttpHandler(StubWebHandler webHandler, WebFilter... filters) { return WebToHttpHandlerBuilder.webHandler(webHandler).filters(filters).build(); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/server/session/DefaultWebSessionManagerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/server/session/DefaultWebSessionManagerTests.java index 37c676fc6e..8bae6455d3 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/server/session/DefaultWebSessionManagerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/server/session/DefaultWebSessionManagerTests.java @@ -27,7 +27,7 @@ import org.junit.Test; import org.springframework.http.HttpMethod; import org.springframework.http.server.reactive.MockServerHttpRequest; import org.springframework.http.server.reactive.MockServerHttpResponse; -import org.springframework.web.server.DefaultWebServerExchange; +import org.springframework.web.server.adapter.DefaultWebServerExchange; import org.springframework.web.server.WebServerExchange; import org.springframework.web.server.WebSession; @@ -37,7 +37,6 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; /** * @author Rossen Stoyanchev @@ -74,7 +73,7 @@ public class DefaultWebSessionManagerTests { session.save(); - assertFalse(this.idResolver.getId().isPresent()); + assertNull(this.idResolver.getId()); assertNull(this.manager.getSessionStore().retrieveSession(session.getId()).get()); } @@ -86,8 +85,8 @@ public class DefaultWebSessionManagerTests { session.save(); String id = session.getId(); - assertTrue(this.idResolver.getId().isPresent()); - assertEquals(id, this.idResolver.getId().get()); + assertNotNull(this.idResolver.getId()); + assertEquals(id, this.idResolver.getId()); assertSame(session, this.manager.getSessionStore().retrieveSession(id).get()); } @@ -98,7 +97,7 @@ public class DefaultWebSessionManagerTests { session.getAttributes().put("foo", "bar"); session.save(); - assertTrue(this.idResolver.getId().isPresent()); + assertNotNull(this.idResolver.getId()); } @Test @@ -129,14 +128,14 @@ public class DefaultWebSessionManagerTests { private Optional idToResolve = Optional.empty(); - private Optional id = Optional.empty(); + private String id = null; public void setIdToResolve(Optional idToResolve) { this.idToResolve = idToResolve; } - public Optional getId() { + public String getId() { return this.id; } @@ -146,7 +145,7 @@ public class DefaultWebSessionManagerTests { } @Override - public void setSessionId(WebServerExchange exchange, Optional sessionId) { + public void setSessionId(WebServerExchange exchange, String sessionId) { this.id = sessionId; } } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/server/session/WebSessionIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/server/session/WebSessionIntegrationTests.java index c4e80f0d14..9c25fd9619 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/server/session/WebSessionIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/server/session/WebSessionIntegrationTests.java @@ -37,7 +37,7 @@ import org.springframework.util.StringUtils; import org.springframework.web.client.RestTemplate; import org.springframework.web.server.WebHandler; import org.springframework.web.server.WebServerExchange; -import org.springframework.web.server.WebToHttpHandlerBuilder; +import org.springframework.web.server.adapter.WebToHttpHandlerBuilder; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; From 381855aaf315adfd44e22797bbf14f07d08155d9 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 27 Jan 2016 17:05:07 -0500 Subject: [PATCH 200/498] Refine names in web.server and polish Javadoc WebServerExchange -> ServerWebExchange Follows the same convention as in the http package also better allowing the possibility for a client equivalent in the future. WebToHttpHandlerBuilder -> WebHttpHandlerBuilder WebToHttpHandlerAdapter -> WebHttpHandlerAdapter More consistent with Spring conventions. --- .../web/reactive/DispatcherHandler.java | 10 +- .../web/reactive/HandlerAdapter.java | 4 +- .../web/reactive/HandlerMapping.java | 4 +- .../web/reactive/HandlerResultHandler.java | 4 +- .../ResponseStatusExceptionHandler.java | 4 +- .../handler/HttpHandlerHandlerAdapter.java | 4 +- .../handler/SimpleHandlerResultHandler.java | 4 +- .../handler/SimpleUrlHandlerMapping.java | 4 +- .../method/HandlerMethodArgumentResolver.java | 4 +- .../method/InvocableHandlerMethod.java | 6 +- .../RequestBodyArgumentResolver.java | 4 +- .../RequestMappingHandlerAdapter.java | 6 +- .../RequestMappingHandlerMapping.java | 4 +- .../RequestParamArgumentResolver.java | 4 +- .../annotation/ResponseBodyResultHandler.java | 4 +- ...erExchange.java => ServerWebExchange.java} | 9 +- .../web/server/WebExceptionHandler.java | 2 +- .../springframework/web/server/WebFilter.java | 2 +- .../web/server/WebFilterChain.java | 2 +- .../web/server/WebHandler.java | 12 +- ...nge.java => DefaultServerWebExchange.java} | 8 +- ...dapter.java => WebHttpHandlerAdapter.java} | 27 +-- .../server/adapter/WebHttpHandlerBuilder.java | 158 ++++++++++++++++++ .../adapter/WebToHttpHandlerBuilder.java | 102 ----------- .../handler/ExceptionHandlingWebHandler.java | 12 +- .../server/handler/FilteringWebHandler.java | 11 +- .../server/handler/WebHandlerDecorator.java | 6 +- .../session/CookieWebSessionIdResolver.java | 6 +- .../session/DefaultWebSessionManager.java | 12 +- .../server/session/WebSessionIdResolver.java | 7 +- .../web/server/session/WebSessionManager.java | 4 +- .../reactive/DispatcherHandlerErrorTests.java | 12 +- .../ResponseStatusExceptionHandlerTests.java | 8 +- ...mpleUrlHandlerMappingIntegrationTests.java | 12 +- .../method/InvocableHandlerMethodTests.java | 8 +- .../RequestMappingHandlerMappingTests.java | 10 +- .../RequestMappingIntegrationTests.java | 4 +- .../ExceptionHandlingHttpHandlerTests.java | 15 +- .../handler/FilteringWebHandlerTests.java | 16 +- .../DefaultWebSessionManagerTests.java | 12 +- .../session/WebSessionIntegrationTests.java | 8 +- 41 files changed, 310 insertions(+), 245 deletions(-) rename spring-web-reactive/src/main/java/org/springframework/web/server/{WebServerExchange.java => ServerWebExchange.java} (77%) rename spring-web-reactive/src/main/java/org/springframework/web/server/adapter/{DefaultWebServerExchange.java => DefaultServerWebExchange.java} (91%) rename spring-web-reactive/src/main/java/org/springframework/web/server/adapter/{WebToHttpHandlerAdapter.java => WebHttpHandlerAdapter.java} (68%) create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/server/adapter/WebHttpHandlerBuilder.java delete mode 100644 spring-web-reactive/src/main/java/org/springframework/web/server/adapter/WebToHttpHandlerBuilder.java diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java index cc143cbfcd..7b37bf23a7 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java @@ -33,7 +33,7 @@ import org.springframework.context.ApplicationContextAware; import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.web.server.WebHandler; -import org.springframework.web.server.WebServerExchange; +import org.springframework.web.server.ServerWebExchange; /** * Central dispatcher for HTTP request handlers/controllers. Dispatches to registered @@ -112,7 +112,7 @@ public class DispatcherHandler implements WebHandler, ApplicationContextAware { @Override - public Mono handle(WebServerExchange exchange) { + public Mono handle(ServerWebExchange exchange) { if (logger.isDebugEnabled()) { ServerHttpRequest request = exchange.getRequest(); logger.debug("Processing " + request.getMethod() + " request for [" + request.getURI() + "]"); @@ -125,7 +125,7 @@ public class DispatcherHandler implements WebHandler, ApplicationContextAware { .otherwise(ex -> Mono.error(this.errorMapper.apply(ex))); } - private Mono invokeHandler(WebServerExchange exchange, Object handler) { + private Mono invokeHandler(ServerWebExchange exchange, Object handler) { for (HandlerAdapter handlerAdapter : this.handlerAdapters) { if (handlerAdapter.supports(handler)) { return handlerAdapter.handle(exchange, handler); @@ -134,7 +134,7 @@ public class DispatcherHandler implements WebHandler, ApplicationContextAware { return Mono.error(new IllegalStateException("No HandlerAdapter: " + handler)); } - private Mono handleResult(WebServerExchange exchange, HandlerResult result) { + private Mono handleResult(ServerWebExchange exchange, HandlerResult result) { return getResultHandler(result).handleResult(exchange, result) .otherwise(ex -> result.applyExceptionHandler(ex).then(exceptionResult -> getResultHandler(result).handleResult(exchange, exceptionResult))); @@ -157,7 +157,7 @@ public class DispatcherHandler implements WebHandler, ApplicationContextAware { @Override - public Mono getHandler(WebServerExchange exchange) { + public Mono getHandler(ServerWebExchange exchange) { return Mono.error(HANDLER_NOT_FOUND_EXCEPTION); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerAdapter.java index a8056ee3d7..18672faac0 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerAdapter.java @@ -20,7 +20,7 @@ import java.util.function.Function; import reactor.core.publisher.Mono; -import org.springframework.web.server.WebServerExchange; +import org.springframework.web.server.ServerWebExchange; /** * Contract that decouples the {@link DispatcherHandler} from the details of @@ -58,6 +58,6 @@ public interface HandlerAdapter { * @return {@link Mono} that emits a single {@code HandlerResult} or none if * the request has been fully handled and doesn't require further handling. */ - Mono handle(WebServerExchange exchange, Object handler); + Mono handle(ServerWebExchange exchange, Object handler); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerMapping.java index bc7f1bc06f..9dd0ccb072 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerMapping.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerMapping.java @@ -18,7 +18,7 @@ package org.springframework.web.reactive; import reactor.core.publisher.Mono; -import org.springframework.web.server.WebServerExchange; +import org.springframework.web.server.ServerWebExchange; /** * Interface to be implemented by objects that define a mapping between @@ -35,6 +35,6 @@ public interface HandlerMapping { * @return A {@link Mono} that emits one value or none in case the request * cannot be resolved to a handler */ - Mono getHandler(WebServerExchange exchange); + Mono getHandler(ServerWebExchange exchange); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResultHandler.java index cbbfae5c76..c2038b3e3c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResultHandler.java @@ -18,7 +18,7 @@ package org.springframework.web.reactive; import reactor.core.publisher.Mono; -import org.springframework.web.server.WebServerExchange; +import org.springframework.web.server.ServerWebExchange; /** * Process the {@link HandlerResult}, usually returned by an {@link HandlerAdapter}. @@ -44,6 +44,6 @@ public interface HandlerResultHandler { * @param result the result from the handling * @return {@code Mono} to indicate when request handling is complete. */ - Mono handleResult(WebServerExchange exchange, HandlerResult result); + Mono handleResult(ServerWebExchange exchange, HandlerResult result); } \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/ResponseStatusExceptionHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/ResponseStatusExceptionHandler.java index f60ebbd464..add1da9fdc 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/ResponseStatusExceptionHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/ResponseStatusExceptionHandler.java @@ -19,7 +19,7 @@ import reactor.core.publisher.Mono; import org.springframework.web.ResponseStatusException; import org.springframework.web.server.WebExceptionHandler; -import org.springframework.web.server.WebServerExchange; +import org.springframework.web.server.ServerWebExchange; /** * Handle {@link ResponseStatusException} by setting the response status. @@ -30,7 +30,7 @@ public class ResponseStatusExceptionHandler implements WebExceptionHandler { @Override - public Mono handle(WebServerExchange exchange, Throwable ex) { + public Mono handle(ServerWebExchange exchange, Throwable ex) { if (ex instanceof ResponseStatusException) { exchange.getResponse().setStatusCode(((ResponseStatusException) ex).getHttpStatus()); return Mono.empty(); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/HttpHandlerHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/HttpHandlerHandlerAdapter.java index 51212539b1..bfe32fae98 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/HttpHandlerHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/HttpHandlerHandlerAdapter.java @@ -24,7 +24,7 @@ import org.springframework.web.reactive.DispatcherHandler; import org.springframework.web.reactive.HandlerAdapter; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.server.WebHandler; -import org.springframework.web.server.WebServerExchange; +import org.springframework.web.server.ServerWebExchange; /** * Support use of {@link org.springframework.web.server.WebHandler} through the @@ -45,7 +45,7 @@ public class HttpHandlerHandlerAdapter implements HandlerAdapter { } @Override - public Mono handle(WebServerExchange exchange, Object handler) { + public Mono handle(ServerWebExchange exchange, Object handler) { WebHandler webHandler = (WebHandler) handler; Mono completion = webHandler.handle(exchange); return Mono.just(new HandlerResult(webHandler, completion, PUBLISHER_VOID)); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java index cfaa7066fa..ed3c513f8a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java @@ -25,7 +25,7 @@ import org.springframework.core.convert.ConversionService; import org.springframework.util.Assert; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.HandlerResultHandler; -import org.springframework.web.server.WebServerExchange; +import org.springframework.web.server.ServerWebExchange; /** * Supports {@link HandlerResult} with a {@code void} or {@code Publisher} value. @@ -74,7 +74,7 @@ public class SimpleHandlerResultHandler implements Ordered, HandlerResultHandler @SuppressWarnings("unchecked") @Override - public Mono handleResult(WebServerExchange exchange, HandlerResult result) { + public Mono handleResult(ServerWebExchange exchange, HandlerResult result) { Object value = result.getResult(); if (Void.TYPE.equals(result.getResultType().getRawClass())) { return Mono.empty(); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMapping.java index 1bca0d78ff..0f1709ad85 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMapping.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMapping.java @@ -23,7 +23,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.web.reactive.HandlerMapping; -import org.springframework.web.server.WebServerExchange; +import org.springframework.web.server.ServerWebExchange; /** * @author Rossen Stoyanchev @@ -42,7 +42,7 @@ public class SimpleUrlHandlerMapping implements HandlerMapping { @Override - public Mono getHandler(WebServerExchange exchange) { + public Mono getHandler(ServerWebExchange exchange) { return Flux.create(subscriber -> { String path = exchange.getRequest().getURI().getPath(); Object handler = this.handlerMap.get(path); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/HandlerMethodArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/HandlerMethodArgumentResolver.java index 628d4380f1..defc46ea64 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/HandlerMethodArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/HandlerMethodArgumentResolver.java @@ -19,7 +19,7 @@ package org.springframework.web.reactive.method; import reactor.core.publisher.Mono; import org.springframework.core.MethodParameter; -import org.springframework.web.server.WebServerExchange; +import org.springframework.web.server.ServerWebExchange; /** @@ -35,6 +35,6 @@ public interface HandlerMethodArgumentResolver { * does not resolve to any value, which will result in {@code null} passed * as the argument value. */ - Mono resolveArgument(MethodParameter parameter, WebServerExchange exchange); + Mono resolveArgument(MethodParameter parameter, ServerWebExchange exchange); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java index 855d6dc5ca..47d84dfc6c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java @@ -36,7 +36,7 @@ import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerResult; -import org.springframework.web.server.WebServerExchange; +import org.springframework.web.server.ServerWebExchange; /** @@ -82,7 +82,7 @@ public class InvocableHandlerMethod extends HandlerMethod { * @return Publisher that produces a single HandlerResult or an error signal; * never throws an exception */ - public Mono invokeForRequest(WebServerExchange exchange, Object... providedArgs) { + public Mono invokeForRequest(ServerWebExchange exchange, Object... providedArgs) { return resolveArguments(exchange, providedArgs).then(args -> { try { Object value = doInvoke(args); @@ -100,7 +100,7 @@ public class InvocableHandlerMethod extends HandlerMethod { }); } - private Mono resolveArguments(WebServerExchange exchange, Object... providedArgs) { + private Mono resolveArguments(ServerWebExchange exchange, Object... providedArgs) { if (ObjectUtils.isEmpty(getMethodParameters())) { return NO_ARGS; } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestBodyArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestBodyArgumentResolver.java index 3f0bff5c98..d7bbf50149 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestBodyArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestBodyArgumentResolver.java @@ -31,7 +31,7 @@ import org.springframework.http.MediaType; import org.springframework.util.Assert; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.reactive.method.HandlerMethodArgumentResolver; -import org.springframework.web.server.WebServerExchange; +import org.springframework.web.server.ServerWebExchange; /** * @author Sebastien Deleuze @@ -58,7 +58,7 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve } @Override - public Mono resolveArgument(MethodParameter parameter, WebServerExchange exchange) { + public Mono resolveArgument(MethodParameter parameter, ServerWebExchange exchange) { MediaType mediaType = exchange.getRequest().getHeaders().getContentType(); if (mediaType == null) { mediaType = MediaType.APPLICATION_OCTET_STREAM; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java index acc907ce7d..9457a395fb 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java @@ -45,7 +45,7 @@ import org.springframework.web.reactive.HandlerAdapter; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.method.HandlerMethodArgumentResolver; import org.springframework.web.reactive.method.InvocableHandlerMethod; -import org.springframework.web.server.WebServerExchange; +import org.springframework.web.server.ServerWebExchange; /** @@ -114,7 +114,7 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Initializin } @Override - public Mono handle(WebServerExchange exchange, Object handler) { + public Mono handle(ServerWebExchange exchange, Object handler) { HandlerMethod handlerMethod = (HandlerMethod) handler; InvocableHandlerMethod invocable = new InvocableHandlerMethod(handlerMethod); invocable.setHandlerMethodArgumentResolvers(this.argumentResolvers); @@ -125,7 +125,7 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Initializin } private Mono handleException(Throwable ex, HandlerMethod handlerMethod, - WebServerExchange exchange) { + ServerWebExchange exchange) { if (ex instanceof Exception) { InvocableHandlerMethod invocable = findExceptionHandler(handlerMethod, (Exception) ex); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMapping.java index 3d5761f4b8..d2d86dc76c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMapping.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMapping.java @@ -41,7 +41,7 @@ import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.method.HandlerMethod; import org.springframework.web.method.HandlerMethodSelector; import org.springframework.web.reactive.HandlerMapping; -import org.springframework.web.server.WebServerExchange; +import org.springframework.web.server.ServerWebExchange; /** @@ -93,7 +93,7 @@ public class RequestMappingHandlerMapping implements HandlerMapping, } @Override - public Mono getHandler(WebServerExchange exchange) { + public Mono getHandler(ServerWebExchange exchange) { return Flux.create(subscriber -> { for (Map.Entry entry : this.methodMap.entrySet()) { RequestMappingInfo info = entry.getKey(); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestParamArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestParamArgumentResolver.java index 3b09b36330..0bbc40906f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestParamArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestParamArgumentResolver.java @@ -21,7 +21,7 @@ import reactor.core.publisher.Mono; import org.springframework.core.MethodParameter; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.reactive.method.HandlerMethodArgumentResolver; -import org.springframework.web.server.WebServerExchange; +import org.springframework.web.server.ServerWebExchange; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; @@ -41,7 +41,7 @@ public class RequestParamArgumentResolver implements HandlerMethodArgumentResolv @Override - public Mono resolveArgument(MethodParameter param, WebServerExchange exchange) { + public Mono resolveArgument(MethodParameter param, ServerWebExchange exchange) { RequestParam annotation = param.getParameterAnnotation(RequestParam.class); String name = (annotation.value().length() != 0 ? annotation.value() : param.getParameterName()); UriComponents uriComponents = UriComponentsBuilder.fromUri(exchange.getRequest().getURI()).build(); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java index 1d6a5dd201..da7d4a01cc 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java @@ -45,7 +45,7 @@ import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.HandlerResultHandler; -import org.springframework.web.server.WebServerExchange; +import org.springframework.web.server.ServerWebExchange; /** @@ -130,7 +130,7 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered @Override @SuppressWarnings("unchecked") - public Mono handleResult(WebServerExchange exchange, HandlerResult result) { + public Mono handleResult(ServerWebExchange exchange, HandlerResult result) { Object value = result.getResult(); if (value == null) { diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/WebServerExchange.java b/spring-web-reactive/src/main/java/org/springframework/web/server/ServerWebExchange.java similarity index 77% rename from spring-web-reactive/src/main/java/org/springframework/web/server/WebServerExchange.java rename to spring-web-reactive/src/main/java/org/springframework/web/server/ServerWebExchange.java index 53d1006e0a..144cc81953 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/WebServerExchange.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/ServerWebExchange.java @@ -29,7 +29,7 @@ import org.springframework.http.server.reactive.ServerHttpResponse; * * @author Rossen Stoyanchev */ -public interface WebServerExchange { +public interface ServerWebExchange { /** * Return the current HTTP request. @@ -47,7 +47,12 @@ public interface WebServerExchange { Map getAttributes(); /** - * + * Return the web session for the current request. Always guaranteed to + * return an instance either matching to the session id requested by the + * client, or with a new session id either because the client did not + * specify one or because the underlying session had expired. Use of this + * method does not automatically create a session. See {@link WebSession} + * for more details. */ Mono getSession(); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/WebExceptionHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/server/WebExceptionHandler.java index 438c9df82e..e189498d02 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/WebExceptionHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/WebExceptionHandler.java @@ -33,6 +33,6 @@ public interface WebExceptionHandler { * @param ex the exception to handle * @return {@code Mono} to indicate when exception handling is complete */ - Mono handle(WebServerExchange exchange, Throwable ex); + Mono handle(ServerWebExchange exchange, Throwable ex); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/WebFilter.java b/spring-web-reactive/src/main/java/org/springframework/web/server/WebFilter.java index 2c003db3a6..6b65e7c6b0 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/WebFilter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/WebFilter.java @@ -35,6 +35,6 @@ public interface WebFilter { * @param chain provides a way to delegate to the next filter * @return {@code Mono} to indicate when request processing is complete */ - Mono filter(WebServerExchange exchange, WebFilterChain chain); + Mono filter(ServerWebExchange exchange, WebFilterChain chain); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/WebFilterChain.java b/spring-web-reactive/src/main/java/org/springframework/web/server/WebFilterChain.java index e7ea8668b6..54863f668e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/WebFilterChain.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/WebFilterChain.java @@ -30,6 +30,6 @@ public interface WebFilterChain { * @param exchange the current server exchange * @return {@code Mono} to indicate when request handling is complete */ - Mono filter(WebServerExchange exchange); + Mono filter(ServerWebExchange exchange); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/WebHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/server/WebHandler.java index c5cd2883f7..b2c8045b19 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/WebHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/WebHandler.java @@ -18,19 +18,19 @@ package org.springframework.web.server; import reactor.core.publisher.Mono; -import org.springframework.web.server.adapter.WebToHttpHandlerAdapter; -import org.springframework.web.server.adapter.WebToHttpHandlerBuilder; +import org.springframework.web.server.adapter.WebHttpHandlerAdapter; +import org.springframework.web.server.adapter.WebHttpHandlerBuilder; /** * Contract to handle a web server exchange. * - *

    Use {@link WebToHttpHandlerAdapter} to adapt a {@code WebHandler} to an + *

    Use {@link WebHttpHandlerAdapter} to adapt a {@code WebHandler} to an * {@link org.springframework.http.server.reactive.HttpHandler HttpHandler}. - * The {@link WebToHttpHandlerBuilder} provides a convenient way to do that while + * The {@link WebHttpHandlerBuilder} provides a convenient way to do that while * also optionally configuring one or more filters and/or exception handlers. * * @author Rossen Stoyanchev - * @see WebToHttpHandlerBuilder + * @see WebHttpHandlerBuilder */ public interface WebHandler { @@ -40,6 +40,6 @@ public interface WebHandler { * @param exchange the current server exchange * @return {@code Mono} to indicate when request handling is complete */ - Mono handle(WebServerExchange exchange); + Mono handle(ServerWebExchange exchange); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/adapter/DefaultWebServerExchange.java b/spring-web-reactive/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java similarity index 91% rename from spring-web-reactive/src/main/java/org/springframework/web/server/adapter/DefaultWebServerExchange.java rename to spring-web-reactive/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java index e0aa538652..99e1f641dd 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/adapter/DefaultWebServerExchange.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java @@ -25,16 +25,16 @@ import reactor.core.publisher.Processors; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.util.Assert; -import org.springframework.web.server.WebServerExchange; +import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebSession; import org.springframework.web.server.session.WebSessionManager; /** - * Default implementation of {@link WebServerExchange}. + * Default implementation of {@link ServerWebExchange}. * * @author Rossen Stoyanchev */ -public class DefaultWebServerExchange implements WebServerExchange { +public class DefaultServerWebExchange implements ServerWebExchange { private final ServerHttpRequest request; @@ -51,7 +51,7 @@ public class DefaultWebServerExchange implements WebServerExchange { - public DefaultWebServerExchange(ServerHttpRequest request, ServerHttpResponse response, + public DefaultServerWebExchange(ServerHttpRequest request, ServerHttpResponse response, WebSessionManager sessionManager) { Assert.notNull(request, "'request' is required."); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/adapter/WebToHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/server/adapter/WebHttpHandlerAdapter.java similarity index 68% rename from spring-web-reactive/src/main/java/org/springframework/web/server/adapter/WebToHttpHandlerAdapter.java rename to spring-web-reactive/src/main/java/org/springframework/web/server/adapter/WebHttpHandlerAdapter.java index bb67feab84..fab3ffcdcb 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/adapter/WebToHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/adapter/WebHttpHandlerAdapter.java @@ -26,32 +26,37 @@ import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.util.Assert; import org.springframework.web.server.WebHandler; import org.springframework.web.server.handler.WebHandlerDecorator; -import org.springframework.web.server.WebServerExchange; +import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.session.DefaultWebSessionManager; import org.springframework.web.server.session.WebSessionManager; /** - * Adapt {@link WebHandler} to {@link HttpHandler} also creating the - * {@link WebServerExchange} before invoking the target {@code WebHandler}. + * Default adapter of {@link WebHandler} to the {@link HttpHandler} contract. + * + *

    By default creates and configures a {@link DefaultServerWebExchange} and + * then invokes the target {@code WebHandler}. * * @author Rossen Stoyanchev */ -public class WebToHttpHandlerAdapter extends WebHandlerDecorator implements HttpHandler { +public class WebHttpHandlerAdapter extends WebHandlerDecorator implements HttpHandler { - private static Log logger = LogFactory.getLog(WebToHttpHandlerAdapter.class); + private static Log logger = LogFactory.getLog(WebHttpHandlerAdapter.class); private WebSessionManager sessionManager = new DefaultWebSessionManager(); - public WebToHttpHandlerAdapter(WebHandler delegate) { + public WebHttpHandlerAdapter(WebHandler delegate) { super(delegate); } /** - * - * @param sessionManager + * Configure a custom {@link WebSessionManager} to use for managing web + * sessions. The provided instance is set on each created + * {@link DefaultServerWebExchange}. + *

    By default this is set to {@link DefaultWebSessionManager}. + * @param sessionManager the session manager to use */ public void setSessionManager(WebSessionManager sessionManager) { Assert.notNull(sessionManager, "'sessionManager' must not be null."); @@ -68,7 +73,7 @@ public class WebToHttpHandlerAdapter extends WebHandlerDecorator implements Http @Override public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { - WebServerExchange exchange = createWebServerExchange(request, response); + ServerWebExchange exchange = createExchange(request, response); return getDelegate().handle(exchange) .otherwise(ex -> { if (logger.isDebugEnabled()) { @@ -80,8 +85,8 @@ public class WebToHttpHandlerAdapter extends WebHandlerDecorator implements Http .after(response::setComplete); } - protected WebServerExchange createWebServerExchange(ServerHttpRequest request, ServerHttpResponse response) { - return new DefaultWebServerExchange(request, response, this.sessionManager); + protected ServerWebExchange createExchange(ServerHttpRequest request, ServerHttpResponse response) { + return new DefaultServerWebExchange(request, response, this.sessionManager); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/adapter/WebHttpHandlerBuilder.java b/spring-web-reactive/src/main/java/org/springframework/web/server/adapter/WebHttpHandlerBuilder.java new file mode 100644 index 0000000000..38dfda1b54 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/adapter/WebHttpHandlerBuilder.java @@ -0,0 +1,158 @@ +/* + * Copyright 2002-2015 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.web.server.adapter; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebExceptionHandler; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebHandler; +import org.springframework.web.server.handler.ExceptionHandlingWebHandler; +import org.springframework.web.server.handler.FilteringWebHandler; +import org.springframework.web.server.session.WebSessionManager; + +/** + * Build an {@link org.springframework.http.server.reactive.HttpHandler HttpHandler} + * to handle requests with a chain of {@link #filters(WebFilter...) web filters}, + * a target {@link #webHandler(WebHandler) web handler}, and apply one or more + * {@link #exceptionHandlers(WebExceptionHandler...) exception handlers}. + * + *

    Effective this sets up the following {@code WebHandler} delegation:
    + * {@link WebHttpHandlerAdapter} {@code -->} + * {@link ExceptionHandlingWebHandler} {@code -->} + * {@link FilteringWebHandler} {@code -->} + * {@link WebHandler} + * + *

    Example usage: + *

    + * WebFilter myFilter = ... ;
    + * WebHandler myHandler = ... ;
    + *
    + * HttpHandler httpHandler = WebToHttpHandlerBuilder.webHandler(myHandler)
    + *         .filters(myFilter)
    + *         .exceptionHandlers(new ResponseStatusExceptionHandler())
    + *         .build();
    + *
    + * // Configure the HttpServer with the created httpHandler
    + * 
    + * + * @author Rossen Stoyanchev + */ +public class WebHttpHandlerBuilder { + + private final WebHandler targetHandler; + + private final List filters = new ArrayList<>(); + + private final List exceptionHandlers = new ArrayList<>(); + + private WebSessionManager sessionManager; + + + /** + * Private constructor. + * See static factory method {@link #webHandler(WebHandler)}. + */ + private WebHttpHandlerBuilder(WebHandler targetHandler) { + Assert.notNull(targetHandler, "'targetHandler' must not be null"); + this.targetHandler = targetHandler; + } + + + /** + * Factory method to create a new builder instance. + * @param targetHandler the target handler to process requests with + */ + public static WebHttpHandlerBuilder webHandler(WebHandler targetHandler) { + return new WebHttpHandlerBuilder(targetHandler); + } + + + /** + * Add the given filters to use for processing requests. + * @param filters the filters to add + */ + public WebHttpHandlerBuilder filters(WebFilter... filters) { + if (!ObjectUtils.isEmpty(filters)) { + this.filters.addAll(Arrays.asList(filters)); + } + return this; + } + + /** + * Add the given exception handler to apply at the end of request processing. + * @param exceptionHandlers the exception handlers + */ + public WebHttpHandlerBuilder exceptionHandlers(WebExceptionHandler... exceptionHandlers) { + if (!ObjectUtils.isEmpty(exceptionHandlers)) { + this.exceptionHandlers.addAll(Arrays.asList(exceptionHandlers)); + } + return this; + } + + /** + * Configure the {@link WebSessionManager} to set on the + * {@link ServerWebExchange WebServerExchange} + * created for each HTTP request. + * @param sessionManager the session manager + */ + public WebHttpHandlerBuilder sessionManager(WebSessionManager sessionManager) { + this.sessionManager = sessionManager; + return this; + } + + /** + * Build the {@link HttpHandler}. + */ + public HttpHandler build() { + WebHandler handler = createWebHandler(); + return adaptWebHandler(handler); + } + + /** + * Create the final (decorated) {@link WebHandler} to use. + */ + protected WebHandler createWebHandler() { + WebHandler webHandler = this.targetHandler; + if (!this.exceptionHandlers.isEmpty()) { + WebExceptionHandler[] array = new WebExceptionHandler[this.exceptionHandlers.size()]; + webHandler = new ExceptionHandlingWebHandler(webHandler, this.exceptionHandlers.toArray(array)); + } + if (!this.filters.isEmpty()) { + WebFilter[] array = new WebFilter[this.filters.size()]; + webHandler = new FilteringWebHandler(webHandler, this.filters.toArray(array)); + } + return webHandler; + } + + /** + * Adapt the {@link WebHandler} to {@link HttpHandler}. + */ + protected WebHttpHandlerAdapter adaptWebHandler(WebHandler handler) { + WebHttpHandlerAdapter adapter = new WebHttpHandlerAdapter(handler); + if (this.sessionManager != null) { + adapter.setSessionManager(this.sessionManager); + } + return adapter; + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/adapter/WebToHttpHandlerBuilder.java b/spring-web-reactive/src/main/java/org/springframework/web/server/adapter/WebToHttpHandlerBuilder.java deleted file mode 100644 index 97ca700f23..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/adapter/WebToHttpHandlerBuilder.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright 2002-2015 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.web.server.adapter; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import org.springframework.http.server.reactive.HttpHandler; -import org.springframework.util.Assert; -import org.springframework.util.ObjectUtils; -import org.springframework.web.server.WebExceptionHandler; -import org.springframework.web.server.WebFilter; -import org.springframework.web.server.WebHandler; -import org.springframework.web.server.handler.ExceptionHandlingWebHandler; -import org.springframework.web.server.handler.FilteringWebHandler; -import org.springframework.web.server.session.WebSessionManager; - -/** - * Assist with building an - * {@link org.springframework.http.server.reactive.HttpHandler HttpHandler} to - * invoke a target {@link WebHandler} with an optional chain of - * {@link WebFilter}s and one or more {@link WebExceptionHandler}s. - * - *

    Effective this sets up the following {@code WebHandler} delegation:
    - * {@link WebToHttpHandlerAdapter} {@code -->} - * {@link ExceptionHandlingWebHandler} {@code -->} - * {@link FilteringWebHandler} - * - * @author Rossen Stoyanchev - */ -public class WebToHttpHandlerBuilder { - - private final WebHandler targetHandler; - - private final List filters = new ArrayList<>(); - - private final List exceptionHandlers = new ArrayList<>(); - - private WebSessionManager sessionManager; - - - private WebToHttpHandlerBuilder(WebHandler targetHandler) { - Assert.notNull(targetHandler, "'targetHandler' must not be null"); - this.targetHandler = targetHandler; - } - - - public static WebToHttpHandlerBuilder webHandler(WebHandler webHandler) { - return new WebToHttpHandlerBuilder(webHandler); - } - - public WebToHttpHandlerBuilder filters(WebFilter... filters) { - if (!ObjectUtils.isEmpty(filters)) { - this.filters.addAll(Arrays.asList(filters)); - } - return this; - } - - public WebToHttpHandlerBuilder exceptionHandlers(WebExceptionHandler... exceptionHandlers) { - if (!ObjectUtils.isEmpty(exceptionHandlers)) { - this.exceptionHandlers.addAll(Arrays.asList(exceptionHandlers)); - } - return this; - } - - public WebToHttpHandlerBuilder sessionManager(WebSessionManager sessionManager) { - this.sessionManager = sessionManager; - return this; - } - - public HttpHandler build() { - WebHandler handler = this.targetHandler; - if (!this.exceptionHandlers.isEmpty()) { - WebExceptionHandler[] array = new WebExceptionHandler[this.exceptionHandlers.size()]; - handler = new ExceptionHandlingWebHandler(handler, this.exceptionHandlers.toArray(array)); - } - if (!this.filters.isEmpty()) { - WebFilter[] array = new WebFilter[this.filters.size()]; - handler = new FilteringWebHandler(handler, this.filters.toArray(array)); - } - WebToHttpHandlerAdapter adapter = new WebToHttpHandlerAdapter(handler); - if (this.sessionManager != null) { - adapter.setSessionManager(this.sessionManager); - } - return adapter; - } - -} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/handler/ExceptionHandlingWebHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/server/handler/ExceptionHandlingWebHandler.java index 5f75c3a8dc..a21433f33d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/handler/ExceptionHandlingWebHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/handler/ExceptionHandlingWebHandler.java @@ -26,11 +26,11 @@ import reactor.core.publisher.Mono; import org.springframework.http.HttpStatus; import org.springframework.web.server.WebExceptionHandler; import org.springframework.web.server.WebHandler; -import org.springframework.web.server.WebServerExchange; +import org.springframework.web.server.ServerWebExchange; /** - * {@code WebHandler} that decorates another with exception handling using one - * or more instances of {@link WebExceptionHandler}. + * WebHandler that can invoke a target {@link WebHandler} and then apply + * exception handling with one or more {@link WebExceptionHandler} instances. * * @author Rossen Stoyanchev */ @@ -53,7 +53,7 @@ public class ExceptionHandlingWebHandler extends WebHandlerDecorator { /** - * @return a read-only list of the configured exception handlers. + * Return a read-only list of the configured exception handlers. */ public List getExceptionHandlers() { return this.exceptionHandlers; @@ -61,7 +61,7 @@ public class ExceptionHandlingWebHandler extends WebHandlerDecorator { @Override - public Mono handle(WebServerExchange exchange) { + public Mono handle(ServerWebExchange exchange) { Mono mono; try { mono = getDelegate().handle(exchange); @@ -75,7 +75,7 @@ public class ExceptionHandlingWebHandler extends WebHandlerDecorator { return mono.otherwise(ex -> handleUnresolvedException(exchange, ex)); } - private Mono handleUnresolvedException(WebServerExchange exchange, Throwable ex) { + private Mono handleUnresolvedException(ServerWebExchange exchange, Throwable ex) { if (logger.isDebugEnabled()) { logger.debug("Could not complete request", ex); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/handler/FilteringWebHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/server/handler/FilteringWebHandler.java index 9b55bf98ba..8dc1d7396a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/handler/FilteringWebHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/handler/FilteringWebHandler.java @@ -24,10 +24,11 @@ import reactor.core.publisher.Mono; import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilterChain; import org.springframework.web.server.WebHandler; -import org.springframework.web.server.WebServerExchange; +import org.springframework.web.server.ServerWebExchange; /** - * {@code WebHandler} that decorates another with a chain of {@link WebFilter}s. + * WebHandler that delegates to a chain of {@link WebFilter} instances followed + * by a target {@link WebHandler}. * * @author Rossen Stoyanchev */ @@ -47,14 +48,14 @@ public class FilteringWebHandler extends WebHandlerDecorator { /** - * @return a read-only list of the configured filters. + * Return a read-only list of the configured filters. */ public List getFilters() { return this.filters; } @Override - public Mono handle(WebServerExchange exchange) { + public Mono handle(ServerWebExchange exchange) { return new DefaultWebFilterChain().filter(exchange); } @@ -65,7 +66,7 @@ public class FilteringWebHandler extends WebHandlerDecorator { @Override - public Mono filter(WebServerExchange exchange) { + public Mono filter(ServerWebExchange exchange) { if (this.index < filters.size()) { WebFilter filter = filters.get(this.index++); return filter.filter(exchange, this); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/handler/WebHandlerDecorator.java b/spring-web-reactive/src/main/java/org/springframework/web/server/handler/WebHandlerDecorator.java index 151ce5b115..5fa614af59 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/handler/WebHandlerDecorator.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/handler/WebHandlerDecorator.java @@ -19,10 +19,10 @@ import reactor.core.publisher.Mono; import org.springframework.util.Assert; import org.springframework.web.server.WebHandler; -import org.springframework.web.server.WebServerExchange; +import org.springframework.web.server.ServerWebExchange; /** - * Base class for a {@link WebHandler} that decorates and delegates to another. + * {@link WebHandler} that decorates and delegates to another. * * @author Rossen Stoyanchev */ @@ -43,7 +43,7 @@ public class WebHandlerDecorator implements WebHandler { @Override - public Mono handle(WebServerExchange exchange) { + public Mono handle(ServerWebExchange exchange) { return this.delegate.handle(exchange); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/session/CookieWebSessionIdResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/server/session/CookieWebSessionIdResolver.java index 2584487c2b..f325be9e67 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/session/CookieWebSessionIdResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/session/CookieWebSessionIdResolver.java @@ -25,7 +25,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; -import org.springframework.web.server.WebServerExchange; +import org.springframework.web.server.ServerWebExchange; /** * Cookie-based {@link WebSessionIdResolver}. @@ -75,7 +75,7 @@ public class CookieWebSessionIdResolver implements WebSessionIdResolver { @Override - public Optional resolveSessionId(WebServerExchange exchange) { + public Optional resolveSessionId(ServerWebExchange exchange) { HttpHeaders headers = exchange.getRequest().getHeaders(); List cookies = headers.getCookies().get(getCookieName()); return (CollectionUtils.isEmpty(cookies) ? @@ -83,7 +83,7 @@ public class CookieWebSessionIdResolver implements WebSessionIdResolver { } @Override - public void setSessionId(WebServerExchange exchange, String id) { + public void setSessionId(ServerWebExchange exchange, String id) { Duration maxAge = (StringUtils.hasText(id) ? getCookieMaxAge() : Duration.ofSeconds(0)); HttpCookie cookie = HttpCookie.serverCookie(getCookieName(), id).maxAge(maxAge).build(); HttpHeaders headers = exchange.getResponse().getHeaders(); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/session/DefaultWebSessionManager.java b/spring-web-reactive/src/main/java/org/springframework/web/server/session/DefaultWebSessionManager.java index f374449215..8c1756c313 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/session/DefaultWebSessionManager.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/session/DefaultWebSessionManager.java @@ -23,7 +23,7 @@ import java.util.UUID; import reactor.core.publisher.Mono; import org.springframework.util.Assert; -import org.springframework.web.server.WebServerExchange; +import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebSession; @@ -97,7 +97,7 @@ public class DefaultWebSessionManager implements WebSessionManager { @Override - public Mono getSession(WebServerExchange exchange) { + public Mono getSession(ServerWebExchange exchange) { return Mono.fromCallable(() -> getSessionIdResolver().resolveSessionId(exchange)) .where(Optional::isPresent) .map(Optional::get) @@ -107,7 +107,7 @@ public class DefaultWebSessionManager implements WebSessionManager { .map(session -> extendSession(exchange, session)); } - protected Mono validateSession(WebServerExchange exchange, WebSession session) { + protected Mono validateSession(ServerWebExchange exchange, WebSession session) { if (session.isExpired()) { this.sessionIdResolver.setSessionId(exchange, ""); return this.sessionStore.removeSession(session.getId()).after(Mono::empty); @@ -117,13 +117,13 @@ public class DefaultWebSessionManager implements WebSessionManager { } } - protected Mono createSession(WebServerExchange exchange) { + protected Mono createSession(ServerWebExchange exchange) { String sessionId = UUID.randomUUID().toString(); WebSession session = new DefaultWebSession(sessionId, getClock()); return Mono.just(session); } - protected WebSession extendSession(WebServerExchange exchange, WebSession session) { + protected WebSession extendSession(ServerWebExchange exchange, WebSession session) { if (session instanceof ConfigurableWebSession) { ConfigurableWebSession managed = (ConfigurableWebSession) session; managed.setSaveOperation(() -> saveSession(exchange, session)); @@ -133,7 +133,7 @@ public class DefaultWebSessionManager implements WebSessionManager { return session; } - protected Mono saveSession(WebServerExchange exchange, WebSession session) { + protected Mono saveSession(ServerWebExchange exchange, WebSession session) { Assert.isTrue(!session.isExpired(), "Sessions are checked for expiration and have their " + "access time updated when first accessed during request processing. " + diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/session/WebSessionIdResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/server/session/WebSessionIdResolver.java index 61c28260f2..151952716b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/session/WebSessionIdResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/session/WebSessionIdResolver.java @@ -17,8 +17,7 @@ package org.springframework.web.server.session; import java.util.Optional; -import org.springframework.web.server.WebServerExchange; -import org.springframework.web.server.WebSession; +import org.springframework.web.server.ServerWebExchange; /** @@ -36,7 +35,7 @@ public interface WebSessionIdResolver { * @param exchange the current exchange * @return the session id if present */ - Optional resolveSessionId(WebServerExchange exchange); + Optional resolveSessionId(ServerWebExchange exchange); /** * Send the given session id to the client or if the session id is "null" @@ -44,6 +43,6 @@ public interface WebSessionIdResolver { * @param exchange the current exchange * @param sessionId the session id */ - void setSessionId(WebServerExchange exchange, String sessionId); + void setSessionId(ServerWebExchange exchange, String sessionId); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/session/WebSessionManager.java b/spring-web-reactive/src/main/java/org/springframework/web/server/session/WebSessionManager.java index 63909b072e..f0ad012150 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/session/WebSessionManager.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/session/WebSessionManager.java @@ -17,7 +17,7 @@ package org.springframework.web.server.session; import reactor.core.publisher.Mono; -import org.springframework.web.server.WebServerExchange; +import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebSession; /** @@ -43,6 +43,6 @@ public interface WebSessionManager { * @param exchange the current exchange * @return {@code Mono} for async access to the session */ - Mono getSession(WebServerExchange exchange); + Mono getSession(ServerWebExchange exchange); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java index b9d1dda3f7..776277fbb6 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java @@ -49,14 +49,14 @@ import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.reactive.method.annotation.RequestMappingHandlerAdapter; import org.springframework.web.reactive.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.reactive.method.annotation.ResponseBodyResultHandler; -import org.springframework.web.server.adapter.DefaultWebServerExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; import org.springframework.web.server.handler.ExceptionHandlingWebHandler; import org.springframework.web.server.handler.FilteringWebHandler; import org.springframework.web.server.WebExceptionHandler; import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilterChain; import org.springframework.web.server.WebHandler; -import org.springframework.web.server.WebServerExchange; +import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.session.WebSessionManager; import static org.hamcrest.CoreMatchers.startsWith; @@ -81,7 +81,7 @@ public class DispatcherHandlerErrorTests { private MockServerHttpResponse response; - private WebServerExchange exchange; + private ServerWebExchange exchange; @Before @@ -97,7 +97,7 @@ public class DispatcherHandlerErrorTests { this.request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); this.response = new MockServerHttpResponse(); - this.exchange = new DefaultWebServerExchange(this.request, this.response, sessionManager); + this.exchange = new DefaultServerWebExchange(this.request, this.response, sessionManager); } @@ -280,7 +280,7 @@ public class DispatcherHandlerErrorTests { private static class ServerError500ExceptionHandler implements WebExceptionHandler { @Override - public Mono handle(WebServerExchange exchange, Throwable ex) { + public Mono handle(ServerWebExchange exchange, Throwable ex) { exchange.getResponse().setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR); return Mono.empty(); } @@ -289,7 +289,7 @@ public class DispatcherHandlerErrorTests { private static class TestWebFilter implements WebFilter { @Override - public Mono filter(WebServerExchange exchange, WebFilterChain chain) { + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { return chain.filter(exchange); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java index 0bd90168d2..4257499747 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java @@ -29,8 +29,8 @@ import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.MockServerHttpRequest; import org.springframework.http.server.reactive.MockServerHttpResponse; import org.springframework.web.ResponseStatusException; -import org.springframework.web.server.adapter.DefaultWebServerExchange; -import org.springframework.web.server.WebServerExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.session.WebSessionManager; import static org.junit.Assert.assertEquals; @@ -47,7 +47,7 @@ public class ResponseStatusExceptionHandlerTests { private MockServerHttpResponse response; - private WebServerExchange exchange; + private ServerWebExchange exchange; @Before @@ -56,7 +56,7 @@ public class ResponseStatusExceptionHandlerTests { MockServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/path")); WebSessionManager sessionManager = mock(WebSessionManager.class); this.response = new MockServerHttpResponse(); - this.exchange = new DefaultWebServerExchange(request, this.response, sessionManager); + this.exchange = new DefaultServerWebExchange(request, this.response, sessionManager); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java index 859201d754..a8fdd8b0aa 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java @@ -39,8 +39,8 @@ import org.springframework.web.client.RestTemplate; import org.springframework.web.reactive.DispatcherHandler; import org.springframework.web.reactive.ResponseStatusExceptionHandler; import org.springframework.web.server.WebHandler; -import org.springframework.web.server.WebServerExchange; -import org.springframework.web.server.adapter.WebToHttpHandlerBuilder; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.WebHttpHandlerBuilder; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; @@ -66,7 +66,7 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler DispatcherHandler webHandler = new DispatcherHandler(); webHandler.setApplicationContext(wac); - return WebToHttpHandlerBuilder.webHandler(webHandler) + return WebHttpHandlerBuilder.webHandler(webHandler) .exceptionHandlers(new ResponseStatusExceptionHandler()) .build(); } @@ -140,7 +140,7 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler private static class FooHandler implements WebHandler { @Override - public Mono handle(WebServerExchange exchange) { + public Mono handle(ServerWebExchange exchange) { DataBuffer buffer = new DefaultDataBufferAllocator().allocateBuffer() .write("foo".getBytes(StandardCharsets.UTF_8)); return exchange.getResponse().setBody(Flux.just(buffer)); @@ -150,7 +150,7 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler private static class BarHandler implements WebHandler { @Override - public Mono handle(WebServerExchange exchange) { + public Mono handle(ServerWebExchange exchange) { DataBuffer buffer = new DefaultDataBufferAllocator().allocateBuffer() .write("bar".getBytes(StandardCharsets.UTF_8)); return exchange.getResponse().setBody(Flux.just(buffer)); @@ -160,7 +160,7 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler private static class HeaderSettingHandler implements WebHandler { @Override - public Mono handle(WebServerExchange exchange) { + public Mono handle(ServerWebExchange exchange) { exchange.getResponse().getHeaders().add("foo", "bar"); return Mono.empty(); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java index c9da12c17a..892d554e2a 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java @@ -35,8 +35,8 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.method.annotation.RequestParamArgumentResolver; -import org.springframework.web.server.adapter.DefaultWebServerExchange; -import org.springframework.web.server.WebServerExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.session.WebSessionManager; import static org.junit.Assert.assertEquals; @@ -52,14 +52,14 @@ public class InvocableHandlerMethodTests { private ServerHttpRequest request; - private WebServerExchange exchange; + private ServerWebExchange exchange; @Before public void setUp() throws Exception { WebSessionManager sessionManager = mock(WebSessionManager.class); this.request = mock(ServerHttpRequest.class); - this.exchange = new DefaultWebServerExchange(request, mock(ServerHttpResponse.class), sessionManager); + this.exchange = new DefaultServerWebExchange(request, mock(ServerHttpResponse.class), sessionManager); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java index bd2d143dca..d29d0d7f2e 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java @@ -34,8 +34,8 @@ import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.method.HandlerMethod; -import org.springframework.web.server.adapter.DefaultWebServerExchange; -import org.springframework.web.server.WebServerExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.session.WebSessionManager; import static java.util.stream.Collectors.toList; @@ -66,7 +66,7 @@ public class RequestMappingHandlerMappingTests { ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("boo")); MockServerHttpResponse response = new MockServerHttpResponse(); WebSessionManager sessionManager = mock(WebSessionManager.class); - WebServerExchange exchange = new DefaultWebServerExchange(request, response, sessionManager); + ServerWebExchange exchange = new DefaultServerWebExchange(request, response, sessionManager); Publisher handlerPublisher = this.mapping.getHandler(exchange); HandlerMethod handlerMethod = toHandlerMethod(handlerPublisher); assertEquals(TestController.class.getMethod("boo"), handlerMethod.getMethod()); @@ -77,13 +77,13 @@ public class RequestMappingHandlerMappingTests { ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.POST, new URI("foo")); MockServerHttpResponse response = new MockServerHttpResponse(); WebSessionManager sessionManager = mock(WebSessionManager.class); - WebServerExchange exchange = new DefaultWebServerExchange(request, response, sessionManager); + ServerWebExchange exchange = new DefaultServerWebExchange(request, response, sessionManager); Publisher handlerPublisher = this.mapping.getHandler(exchange); HandlerMethod handlerMethod = toHandlerMethod(handlerPublisher); assertEquals(TestController.class.getMethod("postFoo"), handlerMethod.getMethod()); request = new MockServerHttpRequest(HttpMethod.GET, new URI("foo")); - exchange = new DefaultWebServerExchange(request, new MockServerHttpResponse(), sessionManager); + exchange = new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); handlerPublisher = this.mapping.getHandler(exchange); handlerMethod = toHandlerMethod(handlerPublisher); assertEquals(TestController.class.getMethod("getFoo"), handlerMethod.getMethod()); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java index d0331c8c23..2ec5b58d94 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java @@ -64,7 +64,7 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; import org.springframework.web.reactive.DispatcherHandler; import org.springframework.web.reactive.handler.SimpleHandlerResultHandler; -import org.springframework.web.server.adapter.WebToHttpHandlerBuilder; +import org.springframework.web.server.adapter.WebHttpHandlerBuilder; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; @@ -88,7 +88,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati DispatcherHandler webHandler = new DispatcherHandler(); webHandler.setApplicationContext(this.wac); - return WebToHttpHandlerBuilder.webHandler(webHandler).build(); + return WebHttpHandlerBuilder.webHandler(webHandler).build(); } @Test diff --git a/spring-web-reactive/src/test/java/org/springframework/web/server/handler/ExceptionHandlingHttpHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/server/handler/ExceptionHandlingHttpHandlerTests.java index ef9e3cafaa..0e1c0240bd 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/server/handler/ExceptionHandlingHttpHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/server/handler/ExceptionHandlingHttpHandlerTests.java @@ -28,9 +28,8 @@ import org.springframework.http.server.reactive.MockServerHttpRequest; import org.springframework.http.server.reactive.MockServerHttpResponse; import org.springframework.web.server.WebExceptionHandler; import org.springframework.web.server.WebHandler; -import org.springframework.web.server.WebServerExchange; -import org.springframework.web.server.handler.ExceptionHandlingWebHandler; -import org.springframework.web.server.adapter.DefaultWebServerExchange; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; import org.springframework.web.server.session.WebSessionManager; import static org.junit.Assert.assertEquals; @@ -44,7 +43,7 @@ public class ExceptionHandlingHttpHandlerTests { private MockServerHttpResponse response; - private WebServerExchange exchange; + private ServerWebExchange exchange; private WebHandler targetHandler; @@ -55,7 +54,7 @@ public class ExceptionHandlingHttpHandlerTests { WebSessionManager sessionManager = mock(WebSessionManager.class); MockServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, uri); this.response = new MockServerHttpResponse(); - this.exchange = new DefaultWebServerExchange(request, this.response, sessionManager); + this.exchange = new DefaultServerWebExchange(request, this.response, sessionManager); this.targetHandler = new StubWebHandler(new IllegalStateException("boo")); } @@ -119,7 +118,7 @@ public class ExceptionHandlingHttpHandlerTests { } @Override - public Mono handle(WebServerExchange exchange) { + public Mono handle(ServerWebExchange exchange) { if (this.raise) { throw this.exception; } @@ -130,7 +129,7 @@ public class ExceptionHandlingHttpHandlerTests { private static class BadRequestExceptionHandler implements WebExceptionHandler { @Override - public Mono handle(WebServerExchange exchange, Throwable ex) { + public Mono handle(ServerWebExchange exchange, Throwable ex) { exchange.getResponse().setStatusCode(HttpStatus.BAD_REQUEST); return Mono.empty(); } @@ -140,7 +139,7 @@ public class ExceptionHandlingHttpHandlerTests { private static class UnresolvedExceptionHandler implements WebExceptionHandler { @Override - public Mono handle(WebServerExchange exchange, Throwable ex) { + public Mono handle(ServerWebExchange exchange, Throwable ex) { return Mono.error(ex); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/server/handler/FilteringWebHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/server/handler/FilteringWebHandlerTests.java index 0deeb1f7d3..3ba4e6966d 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/server/handler/FilteringWebHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/server/handler/FilteringWebHandlerTests.java @@ -33,8 +33,8 @@ import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilterChain; import org.springframework.web.server.WebHandler; -import org.springframework.web.server.WebServerExchange; -import org.springframework.web.server.adapter.WebToHttpHandlerBuilder; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.WebHttpHandlerBuilder; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -109,7 +109,7 @@ public class FilteringWebHandlerTests { } private HttpHandler createHttpHandler(StubWebHandler webHandler, WebFilter... filters) { - return WebToHttpHandlerBuilder.webHandler(webHandler).filters(filters).build(); + return WebHttpHandlerBuilder.webHandler(webHandler).filters(filters).build(); } @@ -123,12 +123,12 @@ public class FilteringWebHandlerTests { } @Override - public Mono filter(WebServerExchange exchange, WebFilterChain chain) { + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { this.invoked = true; return doFilter(exchange, chain); } - public Mono doFilter(WebServerExchange exchange, WebFilterChain chain) { + public Mono doFilter(ServerWebExchange exchange, WebFilterChain chain) { return chain.filter(exchange); } } @@ -136,7 +136,7 @@ public class FilteringWebHandlerTests { private static class ShortcircuitingFilter extends TestFilter { @Override - public Mono doFilter(WebServerExchange exchange, WebFilterChain chain) { + public Mono doFilter(ServerWebExchange exchange, WebFilterChain chain) { return Mono.empty(); } } @@ -144,7 +144,7 @@ public class FilteringWebHandlerTests { private static class AsyncFilter extends TestFilter { @Override - public Mono doFilter(WebServerExchange exchange, WebFilterChain chain) { + public Mono doFilter(ServerWebExchange exchange, WebFilterChain chain) { return doAsyncWork().then(asyncResult -> { logger.debug("Async result: " + asyncResult); return chain.filter(exchange); @@ -166,7 +166,7 @@ public class FilteringWebHandlerTests { } @Override - public Mono handle(WebServerExchange exchange) { + public Mono handle(ServerWebExchange exchange) { logger.trace("StubHandler invoked."); this.invoked = true; return Mono.empty(); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/server/session/DefaultWebSessionManagerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/server/session/DefaultWebSessionManagerTests.java index 8bae6455d3..5342f97f1a 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/server/session/DefaultWebSessionManagerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/server/session/DefaultWebSessionManagerTests.java @@ -27,8 +27,8 @@ import org.junit.Test; import org.springframework.http.HttpMethod; import org.springframework.http.server.reactive.MockServerHttpRequest; import org.springframework.http.server.reactive.MockServerHttpResponse; -import org.springframework.web.server.adapter.DefaultWebServerExchange; -import org.springframework.web.server.WebServerExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebSession; import static org.junit.Assert.assertEquals; @@ -47,7 +47,7 @@ public class DefaultWebSessionManagerTests { private TestWebSessionIdResolver idResolver; - private DefaultWebServerExchange exchange; + private DefaultServerWebExchange exchange; @Before @@ -58,7 +58,7 @@ public class DefaultWebSessionManagerTests { MockServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/path")); MockServerHttpResponse response = new MockServerHttpResponse(); - this.exchange = new DefaultWebServerExchange(request, response, this.manager); + this.exchange = new DefaultServerWebExchange(request, response, this.manager); } @@ -140,12 +140,12 @@ public class DefaultWebSessionManagerTests { } @Override - public Optional resolveSessionId(WebServerExchange exchange) { + public Optional resolveSessionId(ServerWebExchange exchange) { return this.idToResolve; } @Override - public void setSessionId(WebServerExchange exchange, String sessionId) { + public void setSessionId(ServerWebExchange exchange, String sessionId) { this.id = sessionId; } } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/server/session/WebSessionIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/server/session/WebSessionIntegrationTests.java index 9c25fd9619..cc5cfdf437 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/server/session/WebSessionIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/server/session/WebSessionIntegrationTests.java @@ -36,8 +36,8 @@ import org.springframework.http.server.reactive.HttpHandler; import org.springframework.util.StringUtils; import org.springframework.web.client.RestTemplate; import org.springframework.web.server.WebHandler; -import org.springframework.web.server.WebServerExchange; -import org.springframework.web.server.adapter.WebToHttpHandlerBuilder; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.WebHttpHandlerBuilder; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -72,7 +72,7 @@ public class WebSessionIntegrationTests extends AbstractHttpHandlerIntegrationTe protected HttpHandler createHttpHandler() { this.sessionManager = new DefaultWebSessionManager(); this.handler = new TestWebHandler(); - return WebToHttpHandlerBuilder.webHandler(this.handler).sessionManager(this.sessionManager).build(); + return WebHttpHandlerBuilder.webHandler(this.handler).sessionManager(this.sessionManager).build(); } @Test @@ -152,7 +152,7 @@ public class WebSessionIntegrationTests extends AbstractHttpHandlerIntegrationTe } @Override - public Mono handle(WebServerExchange exchange) { + public Mono handle(ServerWebExchange exchange) { return exchange.getSession().map(session -> { Map map = session.getAttributes(); int value = (map.get("counter") != null ? (int) map.get("counter") : 0); From b9f4698b695122e32cbbc7f9247de19d303ba741 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 27 Jan 2016 17:45:54 -0500 Subject: [PATCH 201/498] Remove license from package infos --- .../springframework/core/codec/package-info.java | 16 ---------------- .../core/codec/support/package-info.java | 16 ---------------- .../core/convert/support/package-info.java | 16 ---------------- .../http/client/package-info.java | 16 ---------------- .../org/springframework/http/package-info.java | 16 ---------------- .../http/server/reactive/package-info.java | 16 ---------------- .../org/springframework/web/package-info.java | 16 ---------------- .../springframework/web/server/package-info.java | 16 ---------------- 8 files changed, 128 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/package-info.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/package-info.java index c71c068caa..9509c9b2f2 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/package-info.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/package-info.java @@ -1,19 +1,3 @@ -/* - * 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. - */ - /** * Root-package for codecs used to transform streams of bytes. * Used for example for serialization/deserialization. diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/package-info.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/package-info.java index 634952baf0..e915f2a1a2 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/package-info.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/package-info.java @@ -1,19 +1,3 @@ -/* - * 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. - */ - /** * This package contains codec implementations. */ diff --git a/spring-web-reactive/src/main/java/org/springframework/core/convert/support/package-info.java b/spring-web-reactive/src/main/java/org/springframework/core/convert/support/package-info.java index f81a625d7c..2a23abd9f0 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/convert/support/package-info.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/convert/support/package-info.java @@ -1,19 +1,3 @@ -/* - * 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. - */ - /** * Default implementation of the type conversion system. */ diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/package-info.java b/spring-web-reactive/src/main/java/org/springframework/http/client/package-info.java index f3c20a57a7..418d5045f8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/package-info.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/package-info.java @@ -1,19 +1,3 @@ -/* - * 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. - */ - /** * Core package of the client-side web support. * Provides a {@code RestTemplate} class and various callback interfaces. diff --git a/spring-web-reactive/src/main/java/org/springframework/http/package-info.java b/spring-web-reactive/src/main/java/org/springframework/http/package-info.java index 798fc50e6e..0f7cce528b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/package-info.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/package-info.java @@ -1,19 +1,3 @@ -/* - * 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. - */ - /** * Contains a basic abstraction over client/server-side HTTP. This package contains * the {@code HttpInputMessage} and {@code HttpOutputMessage} interfaces. diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/package-info.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/package-info.java index d9c4967c6a..9568060c17 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/package-info.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/package-info.java @@ -1,19 +1,3 @@ -/* - * 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. - */ - /** * Core package of the reactive server-side HTTP support. */ diff --git a/spring-web-reactive/src/main/java/org/springframework/web/package-info.java b/spring-web-reactive/src/main/java/org/springframework/web/package-info.java index c3e2e97b72..142f096ec7 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/package-info.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/package-info.java @@ -1,19 +1,3 @@ -/* - * 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. - */ - /** * Common, generic interfaces that define minimal boundary points * between Spring's web infrastructure and other framework modules. diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/package-info.java b/spring-web-reactive/src/main/java/org/springframework/web/server/package-info.java index dc65db1eb4..fcfe7bdaa9 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/package-info.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/package-info.java @@ -1,19 +1,3 @@ -/* - * 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. - */ - /** * Foundational Spring web server support. */ From ee9c5833f51d2c70a2f36e3d6252ce8af2b2ae88 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Thu, 28 Jan 2016 10:23:46 +0100 Subject: [PATCH 202/498] Update our tests to use TestSubscriber --- .../codec/support/ByteBufferDecoderTests.java | 14 ++---- .../codec/support/ByteBufferEncoderTests.java | 32 +++++-------- .../support/JacksonJsonDecoderTests.java | 12 ++--- .../support/JacksonJsonEncoderTests.java | 14 +++--- .../core/codec/support/Jaxb2DecoderTests.java | 13 ++--- .../core/codec/support/Jaxb2EncoderTests.java | 14 +++--- .../codec/support/JsonObjectDecoderTests.java | 36 +++++++------- .../codec/support/JsonObjectEncoderTests.java | 47 +++++++++++-------- .../codec/support/StringDecoderTests.java | 20 +++----- .../codec/support/StringEncoderTests.java | 9 ++-- 10 files changed, 92 insertions(+), 119 deletions(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ByteBufferDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ByteBufferDecoderTests.java index bf7ae3c86c..7ef1437343 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ByteBufferDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ByteBufferDecoderTests.java @@ -17,8 +17,6 @@ package org.springframework.core.codec.support; import java.nio.ByteBuffer; -import java.util.List; -import java.util.stream.StreamSupport; import org.junit.Test; import org.reactivestreams.Publisher; @@ -28,8 +26,8 @@ import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.MediaType; -import static java.util.stream.Collectors.toList; import static org.junit.Assert.*; +import reactor.core.test.TestSubscriber; /** * @author Sebastien Deleuze @@ -46,16 +44,14 @@ public class ByteBufferDecoderTests extends AbstractAllocatingTestCase { } @Test - public void decode() throws InterruptedException { + public void decode() { DataBuffer fooBuffer = stringBuffer("foo"); DataBuffer barBuffer = stringBuffer("bar"); Flux source = Flux.just(fooBuffer, barBuffer); Flux output = decoder.decode(source, ResolvableType.forClassWithGenerics(Publisher.class, ByteBuffer.class), null); - List results = StreamSupport.stream(output.toIterable().spliterator(), false).collect(toList()); - assertEquals(2, results.size()); - - assertBufferEquals(fooBuffer, results.get(0)); - assertBufferEquals(barBuffer, results.get(1)); + TestSubscriber testSubscriber = new TestSubscriber<>(); + testSubscriber.bindTo(output) + .assertValuesWith(b -> assertBufferEquals(fooBuffer, b), b -> assertBufferEquals(barBuffer, b)); } public void assertBufferEquals(DataBuffer expected, ByteBuffer actual) { diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ByteBufferEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ByteBufferEncoderTests.java index ae4e817d64..3e95dcef1f 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ByteBufferEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ByteBufferEncoderTests.java @@ -18,8 +18,6 @@ package org.springframework.core.codec.support; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; -import java.util.List; -import java.util.stream.StreamSupport; import org.junit.Before; import org.junit.Test; @@ -30,8 +28,8 @@ import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.MediaType; -import static java.util.stream.Collectors.toList; import static org.junit.Assert.*; +import reactor.core.test.TestSubscriber; /** * @author Sebastien Deleuze @@ -53,7 +51,7 @@ public class ByteBufferEncoderTests extends AbstractAllocatingTestCase { } @Test - public void encode() throws Exception { + public void encode() { byte[] fooBytes = "foo".getBytes(StandardCharsets.UTF_8); byte[] barBytes = "bar".getBytes(StandardCharsets.UTF_8); Flux source = @@ -62,21 +60,17 @@ public class ByteBufferEncoderTests extends AbstractAllocatingTestCase { Flux output = encoder.encode(source, ResolvableType.forClassWithGenerics(Publisher.class, ByteBuffer.class), null); - List results = - StreamSupport.stream(output.toIterable().spliterator(), false) - .collect(toList()); - - assertEquals(2, results.size()); - assertEquals(3, results.get(0).readableByteCount()); - assertEquals(3, results.get(1).readableByteCount()); - - byte[] buf = new byte[3]; - results.get(0).read(buf); - assertArrayEquals(fooBytes, buf); - - results.get(1).read(buf); - assertArrayEquals(barBytes, buf); - + TestSubscriber testSubscriber = new TestSubscriber<>(); + testSubscriber.bindTo(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/test/java/org/springframework/core/codec/support/JacksonJsonDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonDecoderTests.java index ca01aa945f..8f54bfee31 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonDecoderTests.java @@ -16,9 +16,6 @@ package org.springframework.core.codec.support; -import java.util.List; -import java.util.stream.StreamSupport; - import org.junit.Test; import reactor.core.publisher.Flux; @@ -26,8 +23,8 @@ import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.MediaType; -import static java.util.stream.Collectors.toList; import static org.junit.Assert.*; +import reactor.core.test.TestSubscriber; /** * @author Sebastien Deleuze @@ -43,13 +40,12 @@ public class JacksonJsonDecoderTests extends AbstractAllocatingTestCase { } @Test - public void decode() throws InterruptedException { + public void decode() { Flux source = Flux.just(stringBuffer("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}")); Flux output = decoder.decode(source, ResolvableType.forClass(Pojo.class), null); - List results = StreamSupport.stream(output.toIterable().spliterator(), false).collect(toList()); - assertEquals(1, results.size()); - assertEquals("foofoo", ((Pojo) results.get(0)).getFoo()); + TestSubscriber testSubscriber = new TestSubscriber<>(); + testSubscriber.bindTo(output).assertValues(new Pojo("foofoo", "barbar")); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonEncoderTests.java index d31ad368c2..e61dc616dc 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonEncoderTests.java @@ -17,8 +17,6 @@ package org.springframework.core.codec.support; import java.nio.charset.StandardCharsets; -import java.util.List; -import java.util.stream.StreamSupport; import org.junit.Before; import org.junit.Test; @@ -26,8 +24,8 @@ import reactor.core.publisher.Flux; import org.springframework.http.MediaType; -import static java.util.stream.Collectors.toList; import static org.junit.Assert.*; +import reactor.core.test.TestSubscriber; /** * @author Sebastien Deleuze @@ -48,17 +46,17 @@ public class JacksonJsonEncoderTests extends AbstractAllocatingTestCase { } @Test - public void write() throws InterruptedException { + public void write() { Flux source = Flux.just(new Pojo("foofoo", "barbar"), new Pojo("foofoofoo", "barbarbar")); Flux output = encoder.encode(source, null, null).map(chunk -> { byte[] b = new byte[chunk.readableByteCount()]; chunk.read(b); return new String(b, StandardCharsets.UTF_8); }); - List results = StreamSupport.stream(output.toIterable().spliterator(), false).collect(toList()); - assertEquals(2, results.size()); - assertEquals("{\"foo\":\"foofoo\",\"bar\":\"barbar\"}", results.get(0)); - assertEquals("{\"foo\":\"foofoofoo\",\"bar\":\"barbarbar\"}", results.get(1)); + TestSubscriber testSubscriber = new TestSubscriber<>(); + testSubscriber.bindTo(output) + .assertValues("{\"foo\":\"foofoo\",\"bar\":\"barbar\"}", + "{\"foo\":\"foofoofoo\",\"bar\":\"barbarbar\"}"); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2DecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2DecoderTests.java index 5422869d81..3a5fe365ee 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2DecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2DecoderTests.java @@ -16,9 +16,6 @@ package org.springframework.core.codec.support; -import java.util.List; -import java.util.stream.StreamSupport; - import org.junit.Test; import reactor.core.publisher.Flux; @@ -26,8 +23,8 @@ import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.MediaType; -import static java.util.stream.Collectors.toList; import static org.junit.Assert.*; +import reactor.core.test.TestSubscriber; /** * @author Sebastien Deleuze @@ -44,13 +41,13 @@ public class Jaxb2DecoderTests extends AbstractAllocatingTestCase { } @Test - public void decode() throws InterruptedException { + public void decode() { Flux source = Flux.just(stringBuffer( "barbarfoofoo")); Flux output = decoder.decode(source, ResolvableType.forClass(Pojo.class), null); - List results = StreamSupport.stream(output.toIterable().spliterator(), false).collect(toList()); - assertEquals(1, results.size()); - assertEquals("foofoo", ((Pojo) results.get(0)).getFoo()); + TestSubscriber testSubscriber = new TestSubscriber<>(); + testSubscriber.bindTo(output) + .assertValues(new Pojo("foofoo", "barbar")); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2EncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2EncoderTests.java index 1d9dec12a4..a9ef2ea615 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2EncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2EncoderTests.java @@ -17,8 +17,6 @@ package org.springframework.core.codec.support; import java.nio.charset.StandardCharsets; -import java.util.List; -import java.util.stream.StreamSupport; import org.junit.Before; import org.junit.Test; @@ -26,8 +24,8 @@ import reactor.core.publisher.Flux; import org.springframework.http.MediaType; -import static java.util.stream.Collectors.toList; import static org.junit.Assert.*; +import reactor.core.test.TestSubscriber; /** * @author Sebastien Deleuze @@ -49,17 +47,17 @@ public class Jaxb2EncoderTests extends AbstractAllocatingTestCase { } @Test - public void encode() throws InterruptedException { + public void encode() { Flux source = Flux.just(new Pojo("foofoo", "barbar"), new Pojo("foofoofoo", "barbarbar")); Flux output = encoder.encode(source, null, null).map(chunk -> { byte[] b = new byte[chunk.readableByteCount()]; chunk.read(b); return new String(b, StandardCharsets.UTF_8); }); - List results = StreamSupport.stream(output.toIterable().spliterator(), false).collect(toList()); - assertEquals(2, results.size()); - assertEquals("barbarfoofoo", results.get(0)); - assertEquals("barbarbarfoofoofoo", results.get(1)); + TestSubscriber testSubscriber = new TestSubscriber<>(); + testSubscriber.bindTo(output) + .assertValues("barbarfoofoo", + "barbarbarfoofoofoo"); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JsonObjectDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JsonObjectDecoderTests.java index 3bb4f66f63..e3026f7d1d 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JsonObjectDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JsonObjectDecoderTests.java @@ -17,16 +17,13 @@ package org.springframework.core.codec.support; import java.nio.charset.StandardCharsets; -import java.util.List; -import java.util.stream.StreamSupport; import org.junit.Test; import reactor.core.publisher.Flux; import org.springframework.core.io.buffer.DataBuffer; -import static java.util.stream.Collectors.toList; -import static org.junit.Assert.assertEquals; +import reactor.core.test.TestSubscriber; /** * @author Sebastien Deleuze @@ -35,15 +32,15 @@ public class JsonObjectDecoderTests extends AbstractAllocatingTestCase { @Test - public void decodeSingleChunkToJsonObject() throws InterruptedException { + public void decodeSingleChunkToJsonObject() { JsonObjectDecoder decoder = new JsonObjectDecoder(allocator); Flux source = Flux.just(stringBuffer("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}")); Flux output = decoder.decode(source, null, null).map(JsonObjectDecoderTests::toString); - List results = StreamSupport.stream(output.toIterable().spliterator(), false).collect(toList()); - assertEquals(1, results.size()); - assertEquals("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}", results.get(0)); + TestSubscriber testSubscriber = new TestSubscriber<>(); + testSubscriber.bindTo(output) + .assertValues("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}"); } @Test @@ -53,9 +50,9 @@ public class JsonObjectDecoderTests extends AbstractAllocatingTestCase { stringBuffer(", \"bar\": \"barbar\"}")); Flux output = decoder.decode(source, null, null).map(JsonObjectDecoderTests::toString); - List results = StreamSupport.stream(output.toIterable().spliterator(), false).collect(toList()); - assertEquals(1, results.size()); - assertEquals("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}", results.get(0)); + TestSubscriber testSubscriber = new TestSubscriber<>(); + testSubscriber.bindTo(output) + .assertValues("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}"); } @Test @@ -65,11 +62,10 @@ public class JsonObjectDecoderTests extends AbstractAllocatingTestCase { "[{\"foo\": \"foofoo\", \"bar\": \"barbar\"},{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}]")); Flux output = decoder.decode(source, null, null).map(JsonObjectDecoderTests::toString); - - List results = StreamSupport.stream(output.toIterable().spliterator(), false).collect(toList()); - assertEquals(2, results.size()); - assertEquals("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}", results.get(0)); - assertEquals("{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}", results.get(1)); + TestSubscriber testSubscriber = new TestSubscriber<>(); + testSubscriber.bindTo(output) + .assertValues("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}", + "{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}"); } @Test @@ -80,10 +76,10 @@ public class JsonObjectDecoderTests extends AbstractAllocatingTestCase { ": \"barbar\"},{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}]")); Flux output = decoder.decode(source, null, null).map(JsonObjectDecoderTests::toString); - List results = StreamSupport.stream(output.toIterable().spliterator(), false).collect(toList()); - assertEquals(2, results.size()); - assertEquals("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}", results.get(0)); - assertEquals("{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}", results.get(1)); + TestSubscriber testSubscriber = new TestSubscriber<>(); + testSubscriber.bindTo(output) + .assertValues("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}", + "{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}"); } private static String toString(DataBuffer buffer) { diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JsonObjectEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JsonObjectEncoderTests.java index 04034ea0b1..64116126e9 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JsonObjectEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JsonObjectEncoderTests.java @@ -25,7 +25,7 @@ import reactor.core.publisher.Mono; import org.springframework.core.io.buffer.DataBuffer; -import static org.junit.Assert.assertEquals; +import reactor.core.test.TestSubscriber; /** * @author Sebastien Deleuze @@ -43,27 +43,28 @@ public class JsonObjectEncoderTests extends AbstractAllocatingTestCase { public void encodeSingleElementFlux() throws InterruptedException { Flux source = Flux.just(stringBuffer("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}")); - Iterable results = Flux.from(encoder.encode(source, null, null)).map(chunk -> { + Flux output = Flux.from(encoder.encode(source, null, null)).map(chunk -> { byte[] b = new byte[chunk.readableByteCount()]; chunk.read(b); return new String(b, StandardCharsets.UTF_8); - }).toIterable(); - String result = String.join("", results); - assertEquals("[{\"foo\": \"foofoo\", \"bar\": \"barbar\"}]", result); + }); + TestSubscriber testSubscriber = new TestSubscriber<>(); + testSubscriber.bindTo(output) + .assertValues("[", "{\"foo\": \"foofoo\", \"bar\": \"barbar\"}]"); } - @Test public void encodeSingleElementMono() throws InterruptedException { Mono source = Mono.just(stringBuffer("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}")); - Iterable results = Flux.from(encoder.encode(source, null, null)).map(chunk -> { + Flux output = Flux.from(encoder.encode(source, null, null)).map(chunk -> { byte[] b = new byte[chunk.readableByteCount()]; chunk.read(b); return new String(b, StandardCharsets.UTF_8); - }).toIterable(); - String result = String.join("", results); - assertEquals("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}", result); + }); + TestSubscriber testSubscriber = new TestSubscriber<>(); + testSubscriber.bindTo(output) + .assertValues("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}"); } @Test @@ -71,13 +72,16 @@ public class JsonObjectEncoderTests extends AbstractAllocatingTestCase { Flux source = Flux.just(stringBuffer("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}"), stringBuffer("{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}")); - Iterable results = Flux.from(encoder.encode(source, null, null)).map(chunk -> { + Flux output = Flux.from(encoder.encode(source, null, null)).map(chunk -> { byte[] b = new byte[chunk.readableByteCount()]; chunk.read(b); return new String(b, StandardCharsets.UTF_8); - }).toIterable(); - String result = String.join("", results); - assertEquals("[{\"foo\": \"foofoo\", \"bar\": \"barbar\"},{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}]", result); + }); + TestSubscriber testSubscriber = new TestSubscriber<>(); + testSubscriber.bindTo(output) + .assertValues("[", + "{\"foo\": \"foofoo\", \"bar\": \"barbar\"},", + "{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}]"); } @Test @@ -85,16 +89,19 @@ public class JsonObjectEncoderTests extends AbstractAllocatingTestCase { Flux source = Flux.just(stringBuffer("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}"), stringBuffer("{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}"), - stringBuffer( - "{\"foo\": \"foofoofoofoo\", \"bar\": \"barbarbarbar\"}") + stringBuffer("{\"foo\": \"foofoofoofoo\", \"bar\": \"barbarbarbar\"}") ); - Iterable results = Flux.from(encoder.encode(source, null, null)).map(chunk -> { + Flux output = Flux.from(encoder.encode(source, null, null)).map(chunk -> { byte[] b = new byte[chunk.readableByteCount()]; chunk.read(b); return new String(b, StandardCharsets.UTF_8); - }).toIterable(); - String result = String.join("", results); - assertEquals("[{\"foo\": \"foofoo\", \"bar\": \"barbar\"},{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"},{\"foo\": \"foofoofoofoo\", \"bar\": \"barbarbarbar\"}]", result); + }); + TestSubscriber testSubscriber = new TestSubscriber<>(); + testSubscriber.bindTo(output) + .assertValues("[", + "{\"foo\": \"foofoo\", \"bar\": \"barbar\"},", + "{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"},", + "{\"foo\": \"foofoofoofoo\", \"bar\": \"barbarbarbar\"}]"); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringDecoderTests.java index 937bdc8290..2a87fe86be 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringDecoderTests.java @@ -16,21 +16,18 @@ package org.springframework.core.codec.support; -import java.util.List; -import java.util.stream.StreamSupport; - import org.junit.Before; import org.junit.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.converter.RxJava1SingleConverter; +import reactor.core.test.TestSubscriber; import rx.Single; import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.MediaType; -import static java.util.stream.Collectors.toList; import static org.junit.Assert.*; /** @@ -58,9 +55,8 @@ public class StringDecoderTests extends AbstractAllocatingTestCase { public void decode() throws InterruptedException { Flux source = Flux.just(stringBuffer("foo"), stringBuffer("bar")); Flux output = this.decoder.decode(source, ResolvableType.forClassWithGenerics(Flux.class, String.class), null); - List results = StreamSupport.stream(output.toIterable().spliterator(), false).collect(toList()); - assertEquals(1, results.size()); - assertEquals("foobar", results.get(0)); + TestSubscriber testSubscriber = new TestSubscriber<>(); + testSubscriber.bindTo(output).assertValues("foobar"); } @Test @@ -68,10 +64,8 @@ public class StringDecoderTests extends AbstractAllocatingTestCase { StringDecoder decoder = new StringDecoder(allocator, false); Flux source = Flux.just(stringBuffer("foo"), stringBuffer("bar")); Flux output = decoder.decode(source, ResolvableType.forClassWithGenerics(Flux.class, String.class), null); - List results = StreamSupport.stream(output.toIterable().spliterator(), false).collect(toList()); - assertEquals(2, results.size()); - assertEquals("foo", results.get(0)); - assertEquals("bar", results.get(1)); + TestSubscriber testSubscriber = new TestSubscriber<>(); + testSubscriber.bindTo(output).assertValues("foo", "bar"); } @Test @@ -80,8 +74,8 @@ public class StringDecoderTests extends AbstractAllocatingTestCase { Mono mono = Mono.from(this.decoder.decode(source, ResolvableType.forClassWithGenerics(Mono.class, String.class), MediaType.TEXT_PLAIN)); - String result = mono.get(); - assertEquals("foobar", result); + TestSubscriber testSubscriber = new TestSubscriber<>(); + testSubscriber.bindTo(mono).assertValues("foobar"); } @Test diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringEncoderTests.java index e1a509743e..415f2883d2 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringEncoderTests.java @@ -17,8 +17,6 @@ package org.springframework.core.codec.support; import java.nio.charset.StandardCharsets; -import java.util.List; -import java.util.stream.StreamSupport; import org.junit.Before; import org.junit.Test; @@ -29,8 +27,8 @@ import reactor.core.publisher.Flux; import org.springframework.core.ResolvableType; import org.springframework.http.MediaType; -import static java.util.stream.Collectors.toList; import static org.junit.Assert.*; +import reactor.core.test.TestSubscriber; /** * @author Sebastien Deleuze @@ -59,9 +57,8 @@ public class StringEncoderTests extends AbstractAllocatingTestCase { chunk.read(b); return new String(b, StandardCharsets.UTF_8); }); - List results = StreamSupport.stream(output.toIterable().spliterator(), false).collect(toList()); - assertEquals(1, results.size()); - assertEquals("foo", results.get(0)); + TestSubscriber testSubscriber = new TestSubscriber<>(); + testSubscriber.bindTo(output).assertValues("foo"); } } From 28dcf2e78f9381674ec521c2028360bc8cd645f6 Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Thu, 28 Jan 2016 18:29:54 +0000 Subject: [PATCH 203/498] fix compile test issue --- .../http/server/reactive/AsyncIntegrationTests.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java index 2205511a7e..786fabaa08 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java @@ -27,7 +27,6 @@ import org.junit.runners.Parameterized; import reactor.core.publisher.Mono; import reactor.core.publisher.ProcessorGroup; import reactor.core.publisher.Processors; -import reactor.core.timer.Timers; import reactor.rx.Stream; import org.springframework.core.io.buffer.DataBufferAllocator; @@ -107,7 +106,7 @@ public class AsyncIntegrationTests { @Override public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { return response.setBody(Stream.just("h", "e", "l", "l", "o") - .timer(Timers.global()) + .timer(Timer.global()) .throttleRequest(100) .dispatchOn(asyncGroup) .collect(allocator::allocateBuffer, From 9204fdc371df0f4efc2e3bd17a5fc2839cc845e9 Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Thu, 28 Jan 2016 18:57:28 +0000 Subject: [PATCH 204/498] fix timer issues sync reactor-core --- .../http/server/reactive/UndertowHttpHandlerAdapter.java | 2 +- .../http/server/reactive/AsyncIntegrationTests.java | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java index 03e109575d..c01e34d0d1 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java @@ -131,7 +131,7 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle @Override public void subscribe(Subscriber subscriber) { if (subscriber == null) { - throw Exceptions.spec_2_13_exception(); + throw Exceptions.argumentIsNullException(); } if (this.subscriber != null) { subscriber.onError(new IllegalStateException("Only one subscriber allowed")); diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java index 786fabaa08..99c8c0523e 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java @@ -27,6 +27,7 @@ import org.junit.runners.Parameterized; import reactor.core.publisher.Mono; import reactor.core.publisher.ProcessorGroup; import reactor.core.publisher.Processors; +import reactor.core.timer.Timer; import reactor.rx.Stream; import org.springframework.core.io.buffer.DataBufferAllocator; From 44f4930e296a390105a2de19a89e6b4d4e1ecbec Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Thu, 28 Jan 2016 20:36:29 +0000 Subject: [PATCH 205/498] sync --- .../web/server/adapter/DefaultServerWebExchange.java | 1 - .../http/server/reactive/AsyncIntegrationTests.java | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java b/spring-web-reactive/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java index 99e1f641dd..a7d7ed550c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java @@ -20,7 +20,6 @@ import java.util.concurrent.ConcurrentHashMap; import reactor.core.publisher.FluxProcessor; import reactor.core.publisher.Mono; -import reactor.core.publisher.Processors; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java index 99c8c0523e..8ac52a6114 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java @@ -26,7 +26,6 @@ import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import reactor.core.publisher.Mono; import reactor.core.publisher.ProcessorGroup; -import reactor.core.publisher.Processors; import reactor.core.timer.Timer; import reactor.rx.Stream; @@ -51,7 +50,7 @@ import static org.junit.Assert.assertThat; @RunWith(Parameterized.class) public class AsyncIntegrationTests { - private final ProcessorGroup asyncGroup = Processors.asyncGroup(); + private final ProcessorGroup asyncGroup = ProcessorGroup.async(); private final DataBufferAllocator allocator = new DefaultDataBufferAllocator(); From 199252cda64987475bb362ebc31113cb51d72bae Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Thu, 28 Jan 2016 20:57:24 +0000 Subject: [PATCH 206/498] sync --- .../web/server/adapter/DefaultServerWebExchange.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java b/spring-web-reactive/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java index a7d7ed550c..ee6decb2eb 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java @@ -18,6 +18,7 @@ package org.springframework.web.server.adapter; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import reactor.core.publisher.EmitterProcessor; import reactor.core.publisher.FluxProcessor; import reactor.core.publisher.Mono; @@ -82,7 +83,7 @@ public class DefaultServerWebExchange implements ServerWebExchange { if (this.sessionMono == null) { synchronized (this.createSessionLock) { if (this.sessionMono == null) { - FluxProcessor replay = Processors.replay(1); + FluxProcessor replay = EmitterProcessor.replay(1); this.sessionMono = this.sessionManager.getSession(this).subscribeWith(replay).next(); } } From a4be950e372eae9acac64c0baacb3a9274752df7 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Tue, 2 Feb 2016 13:55:07 +0100 Subject: [PATCH 207/498] Polishing --- .../core/io/buffer/DataBuffer.java | 2 +- .../io/buffer/DefaultDataBufferAllocator.java | 35 ++++++++++++++++--- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DataBuffer.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DataBuffer.java index 49f5f557e0..591b6c3cc9 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DataBuffer.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DataBuffer.java @@ -92,7 +92,7 @@ public interface DataBuffer { DataBuffer write(byte[] source, int offset, int length); /** - * Writes one or more {@link DataBuffer} to this buffer, starting at the current + * Writes one or more {@code DataBuffer}s to this buffer, starting at the current * writing position. * @param buffers the byte buffers to write into this buffer * @return this buffer diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBufferAllocator.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBufferAllocator.java index 2c414e0de3..a9b2b56729 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBufferAllocator.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBufferAllocator.java @@ -18,18 +18,29 @@ package org.springframework.core.io.buffer; import java.nio.ByteBuffer; +import org.springframework.util.Assert; + /** - * Default implementation of the {@code DataBufferAllocator} interface. + * Default implementation of the {@code DataBufferAllocator} interface. Allows for + * specification of the default initial capacity at construction time, as well as whether + * heap-based or direct buffers are to be preferred. * * @author Arjen Poutsma */ public class DefaultDataBufferAllocator implements DataBufferAllocator { + /** + * The default capacity when none is specified. + * @see #DefaultDataBufferAllocator() + * @see #DefaultDataBufferAllocator(boolean) + */ public static final int DEFAULT_INITIAL_CAPACITY = 256; private final boolean preferDirect; + private final int defaultInitialCapacity; + /** * Creates a new {@code DefaultDataBufferAllocator} with default settings. */ @@ -39,17 +50,31 @@ public class DefaultDataBufferAllocator implements DataBufferAllocator { /** * Creates a new {@code DefaultDataBufferAllocator}, indicating whether direct buffers - * should be created by {@link #allocateBuffer(int)}. + * should be created by {@link #allocateBuffer()} and {@link #allocateBuffer(int)}. * @param preferDirect {@code true} if direct buffers are to be preferred; {@code * false} otherwise */ public DefaultDataBufferAllocator(boolean preferDirect) { + this(preferDirect, DEFAULT_INITIAL_CAPACITY); + } + + /** + * Creates a new {@code DefaultDataBufferAllocator}, indicating whether direct buffers + * should be created by {@link #allocateBuffer()} and {@link #allocateBuffer(int)}, + * and what the capacity is to be used for {@link #allocateBuffer()}. + * @param preferDirect {@code true} if direct buffers are to be preferred; {@code + * false} otherwise + */ + public DefaultDataBufferAllocator(boolean preferDirect, int defaultInitialCapacity) { + Assert.isTrue(defaultInitialCapacity > 0, + "'defaultInitialCapacity' should be larger than 0"); this.preferDirect = preferDirect; + this.defaultInitialCapacity = defaultInitialCapacity; } @Override - public DataBuffer allocateBuffer() { - return allocateBuffer(DEFAULT_INITIAL_CAPACITY); + public DefaultDataBuffer allocateBuffer() { + return allocateBuffer(this.defaultInitialCapacity); } @Override @@ -60,7 +85,7 @@ public class DefaultDataBufferAllocator implements DataBufferAllocator { } @Override - public DataBuffer wrap(ByteBuffer byteBuffer) { + public DefaultDataBuffer wrap(ByteBuffer byteBuffer) { ByteBuffer sliced = byteBuffer.slice(); return new DefaultDataBuffer(sliced, 0, byteBuffer.remaining()); } From 0983d302c7f85daa067586449dc861f20e01f89b Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Mon, 1 Feb 2016 15:37:52 +0100 Subject: [PATCH 208/498] Add base contracts for the Web client This commit adds the base contracts for the Web client. The "Reactive" prefixes of the previously commited contracts has been removed to match the server ones. Both the `ClientHttpRequest` and the `ServerHttpResponse` extend `ReactiveHttpOutputMessage`, which now has a `beforeCommit` method, necessary in both client and server implementations. `HttpRequestBuilder` will be used by the developers to create requests with a nice builder API. `ClientHttpRequestFactory` will provide support for many HTTP client libraries in this new client. --- .../http/ReactiveHttpOutputMessage.java | 8 +++ .../http/client/package-info.java | 5 -- .../client/reactive/ClientHttpRequest.java | 54 +++++++++++++++++++ .../reactive/ClientHttpRequestFactory.java | 39 ++++++++++++++ .../ClientHttpResponse.java} | 21 ++------ .../package-info.java} | 18 +++---- .../server/reactive/ServerHttpRequest.java | 4 +- .../server/reactive/ServerHttpResponse.java | 13 +---- .../web/client/HttpRequestBuilder.java | 34 ++++++++++++ 9 files changed, 149 insertions(+), 47 deletions(-) delete mode 100644 spring-web-reactive/src/main/java/org/springframework/http/client/package-info.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ClientHttpRequest.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ClientHttpRequestFactory.java rename spring-web-reactive/src/main/java/org/springframework/http/client/{ReactiveClientHttpResponse.java => reactive/ClientHttpResponse.java} (60%) rename spring-web-reactive/src/main/java/org/springframework/http/client/{ReactiveClientHttpRequest.java => reactive/package-info.java} (60%) create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/client/HttpRequestBuilder.java diff --git a/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java b/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java index ec4e63c192..fb6b06150b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java @@ -16,6 +16,8 @@ package org.springframework.http; +import java.util.function.Supplier; + import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; @@ -31,6 +33,12 @@ import org.springframework.core.io.buffer.DataBuffer; */ public interface ReactiveHttpOutputMessage extends HttpMessage { + /** + * Register an action to be applied just before the message is committed. + * @param action the action + */ + void beforeCommit(Supplier> action); + /** * Set the body of the message to the given {@link Publisher} which will be * used to write to the underlying HTTP layer. diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/package-info.java b/spring-web-reactive/src/main/java/org/springframework/http/client/package-info.java deleted file mode 100644 index 418d5045f8..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/package-info.java +++ /dev/null @@ -1,5 +0,0 @@ -/** - * Core package of the client-side web support. - * Provides a {@code RestTemplate} class and various callback interfaces. - */ -package org.springframework.http.client; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ClientHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ClientHttpRequest.java new file mode 100644 index 0000000000..32c38940ae --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ClientHttpRequest.java @@ -0,0 +1,54 @@ +/* + * 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.http.client.reactive; + +import java.net.URI; + +import reactor.core.publisher.Mono; + +import org.springframework.http.HttpMethod; +import org.springframework.http.ReactiveHttpOutputMessage; + +/** + * Represents a reactive client-side HTTP request. + * + * @author Arjen Poutsma + * @author Brian Clozel + */ +public interface ClientHttpRequest extends ReactiveHttpOutputMessage { + + /** + * Return the HTTP method of the request. + */ + HttpMethod getMethod(); + + /** + * Return the URI of the request. + */ + URI getURI(); + + /** + * Execute this request, resulting in a reactive stream of a single + * {@link org.springframework.http.client.ClientHttpResponse}. + * + * @return a {@code Mono} that signals when the the response + * status and headers have been received. The response body is made available with + * a separate Publisher within the {@code ClientHttpResponse}. + */ + Mono execute(); + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ClientHttpRequestFactory.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ClientHttpRequestFactory.java new file mode 100644 index 0000000000..4eea06aeec --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ClientHttpRequestFactory.java @@ -0,0 +1,39 @@ +/* + * 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.http.client.reactive; + +import java.net.URI; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; + +/** + * Factory for {@link ClientHttpRequest} objects. + * + * @author Brian Clozel + */ +public interface ClientHttpRequestFactory { + + /** + * Create a new {@link ClientHttpRequest} for the specified HTTP method, URI and headers + * + * @param httpMethod the HTTP method to execute + * @param uri the URI to create a request for + * @param headers the HTTP request headers + */ + ClientHttpRequest createRequest(HttpMethod httpMethod, URI uri, HttpHeaders headers); + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/ReactiveClientHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ClientHttpResponse.java similarity index 60% rename from spring-web-reactive/src/main/java/org/springframework/http/client/ReactiveClientHttpResponse.java rename to spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ClientHttpResponse.java index ae4b7fa0de..34cad69c10 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/ReactiveClientHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ClientHttpResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -14,34 +14,21 @@ * limitations under the License. */ -package org.springframework.http.client; +package org.springframework.http.client.reactive; import org.springframework.http.HttpStatus; import org.springframework.http.ReactiveHttpInputMessage; /** - * Represents a "reactive" client-side HTTP response. + * Represents a reactive client-side HTTP response. * * @author Arjen Poutsma */ -public interface ReactiveClientHttpResponse extends ReactiveHttpInputMessage { +public interface ClientHttpResponse extends ReactiveHttpInputMessage { /** - * Return the HTTP status code of the response. * @return the HTTP status as an {@link HttpStatus} enum value */ HttpStatus getStatusCode(); - /** - * Return the HTTP status code of the response as integer - * @return the HTTP status as an integer - */ - int getRawStatusCode(); - - /** - * Return the HTTP status text of the response. - * @return the HTTP status text - */ - String getStatusText(); - } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/ReactiveClientHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/package-info.java similarity index 60% rename from spring-web-reactive/src/main/java/org/springframework/http/client/ReactiveClientHttpRequest.java rename to spring-web-reactive/src/main/java/org/springframework/http/client/reactive/package-info.java index 591a017727..64e63d30de 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/ReactiveClientHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -14,16 +14,10 @@ * limitations under the License. */ -package org.springframework.http.client; - -import org.springframework.http.HttpRequest; -import org.springframework.http.ReactiveHttpOutputMessage; - /** - * Represents a "reactive" client-side HTTP request. - * - * @author Arjen Poutsma + * Core package of the reactive client HTTP support. + * Provides {@link org.springframework.http.client.reactive.ClientHttpRequest} + * and {@link org.springframework.http.client.reactive.ClientHttpResponse} + * interfaces and their implementations */ -public interface ReactiveClientHttpRequest extends HttpRequest, ReactiveHttpOutputMessage { - -} +package org.springframework.http.client.reactive; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java index 1cbbfbfb00..90a52dd13c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -20,7 +20,7 @@ import org.springframework.http.HttpRequest; import org.springframework.http.ReactiveHttpInputMessage; /** - * Represents a "reactive" server-side HTTP request + * Represents a reactive server-side HTTP request * * @author Arjen Poutsma */ diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java index d6c830641a..c7449eca34 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -16,16 +16,13 @@ package org.springframework.http.server.reactive; -import java.util.function.Supplier; - -import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; import org.springframework.http.HttpStatus; import org.springframework.http.ReactiveHttpOutputMessage; /** - * Represents a "reactive" server-side HTTP response. + * Represents a reactive server-side HTTP response. * * @author Arjen Poutsma */ @@ -37,12 +34,6 @@ public interface ServerHttpResponse extends ReactiveHttpOutputMessage { */ void setStatusCode(HttpStatus status); - /** - * Register an action to be applied just before the response is committed. - * @param action the action - */ - void beforeCommit(Supplier> action); - /** * Indicate that request handling is complete, allowing for any cleanup or * end-of-processing tasks to be performed such as applying header changes diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/HttpRequestBuilder.java b/spring-web-reactive/src/main/java/org/springframework/web/client/HttpRequestBuilder.java new file mode 100644 index 0000000000..a9717445b9 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/client/HttpRequestBuilder.java @@ -0,0 +1,34 @@ +/* + * 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.web.client; + +import org.springframework.http.client.reactive.ClientHttpRequest; +import org.springframework.http.client.reactive.ClientHttpRequestFactory; + +/** + * Build {@link ClientHttpRequest} using a {@link ClientHttpRequestFactory} + * which wraps an HTTP client implementation. + * + * @author Brian Clozel + */ +public interface HttpRequestBuilder { + + /** + * Build a {@link ClientHttpRequest} + */ + ClientHttpRequest build(ClientHttpRequestFactory factory); +} From a6469baa4f84de1b9da0131db981e2f42b09969d Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Mon, 1 Feb 2016 18:08:03 +0100 Subject: [PATCH 209/498] Add HttpRequestBuilder default implementation This commit adds a `DefaultHttpRequestBuilder` and its companion static builders in `HttpRequestBuilders`. This allows to build client requests with a friendly builder API, inspired by Spring's MockMvc API. --- .../reactive/DefaultHttpRequestBuilder.java | 167 ++++++++++++++++++ .../{ => reactive}/HttpRequestBuilder.java | 2 +- .../client/reactive/HttpRequestBuilders.java | 110 ++++++++++++ 3 files changed, 278 insertions(+), 1 deletion(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultHttpRequestBuilder.java rename spring-web-reactive/src/main/java/org/springframework/web/client/{ => reactive}/HttpRequestBuilder.java (95%) create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/client/reactive/HttpRequestBuilders.java diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultHttpRequestBuilder.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultHttpRequestBuilder.java new file mode 100644 index 0000000000..5ee4731a12 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultHttpRequestBuilder.java @@ -0,0 +1,167 @@ +/* + * 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.web.client.reactive; + + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; + +import org.springframework.core.ResolvableType; +import org.springframework.core.codec.Encoder; +import org.springframework.http.HttpCookie; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.client.reactive.ClientHttpRequest; +import org.springframework.http.client.reactive.ClientHttpRequestFactory; +import org.springframework.web.client.RestClientException; + +/** + * Builds a {@link ClientHttpRequest} + * + *

    See static factory methods in {@link HttpRequestBuilders} + * + * @author Brian Clozel + * @see HttpRequestBuilders + */ +public class DefaultHttpRequestBuilder implements HttpRequestBuilder { + + protected HttpMethod httpMethod; + + protected HttpHeaders httpHeaders; + + protected URI url; + + protected Flux contentPublisher; + + protected List> messageEncoders; + + protected final List cookies = new ArrayList(); + + protected DefaultHttpRequestBuilder() { + } + + public DefaultHttpRequestBuilder(HttpMethod httpMethod, String urlTemplate, Object... urlVariables) throws RestClientException { + this.httpMethod = httpMethod; + this.httpHeaders = new HttpHeaders(); + this.url = parseURI(urlTemplate); + } + + public DefaultHttpRequestBuilder(HttpMethod httpMethod, URI url) { + this.httpMethod = httpMethod; + this.httpHeaders = new HttpHeaders(); + this.url = url; + } + + protected DefaultHttpRequestBuilder setMessageEncoders(List> messageEncoders) { + this.messageEncoders = messageEncoders; + return this; + } + + private URI parseURI(String uri) throws RestClientException { + try { + return new URI(uri); + } + catch (URISyntaxException e) { + throw new RestClientException("could not parse URL template", e); + } + } + + public DefaultHttpRequestBuilder param(String name, String... values) { + return this; + } + + public DefaultHttpRequestBuilder header(String name, String... values) { + Arrays.stream(values).forEach(value -> this.httpHeaders.add(name, value)); + return this; + } + + public DefaultHttpRequestBuilder headers(HttpHeaders httpHeaders) { + this.httpHeaders = httpHeaders; + return this; + } + + public DefaultHttpRequestBuilder contentType(MediaType contentType) { + this.httpHeaders.setContentType(contentType); + return this; + } + + public DefaultHttpRequestBuilder contentType(String contentType) { + this.httpHeaders.setContentType(MediaType.parseMediaType(contentType)); + return this; + } + + public DefaultHttpRequestBuilder accept(MediaType... mediaTypes) { + this.httpHeaders.setAccept(Arrays.asList(mediaTypes)); + return this; + } + + public DefaultHttpRequestBuilder accept(String... mediaTypes) { + this.httpHeaders.setAccept(Arrays.stream(mediaTypes) + .map(type -> MediaType.parseMediaType(type)) + .collect(Collectors.toList())); + return this; + } + + public DefaultHttpRequestBuilder content(Object content) { + this.contentPublisher = Flux.just(content); + return this; + } + + public DefaultHttpRequestBuilder contentStream(Publisher content) { + this.contentPublisher = Flux.from(content); + return this; + } + + public ClientHttpRequest build(ClientHttpRequestFactory factory) { + ClientHttpRequest request = factory.createRequest(this.httpMethod, this.url, this.httpHeaders); + request.getHeaders().putAll(this.httpHeaders); + + if (this.contentPublisher != null) { + ResolvableType requestBodyType = ResolvableType.forInstance(this.contentPublisher); + MediaType mediaType = request.getHeaders().getContentType(); + + Optional> messageEncoder = resolveEncoder(requestBodyType, mediaType); + + if (messageEncoder.isPresent()) { + request.setBody(messageEncoder.get().encode(this.contentPublisher, requestBodyType, mediaType)); + } + else { + // TODO: wrap with client exception? + request.setBody(Flux.error(new IllegalStateException("Can't write request body" + + "of type '" + requestBodyType.toString() + + "' for content-type '" + mediaType.toString() + "'"))); + } + } + + return request; + } + + protected Optional> resolveEncoder(ResolvableType type, MediaType mediaType) { + return this.messageEncoders.stream() + .filter(e -> e.canEncode(type, mediaType)).findFirst(); + } + +} \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/HttpRequestBuilder.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/HttpRequestBuilder.java similarity index 95% rename from spring-web-reactive/src/main/java/org/springframework/web/client/HttpRequestBuilder.java rename to spring-web-reactive/src/main/java/org/springframework/web/client/reactive/HttpRequestBuilder.java index a9717445b9..55d5e18229 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/client/HttpRequestBuilder.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/HttpRequestBuilder.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.web.client; +package org.springframework.web.client.reactive; import org.springframework.http.client.reactive.ClientHttpRequest; import org.springframework.http.client.reactive.ClientHttpRequestFactory; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/HttpRequestBuilders.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/HttpRequestBuilders.java new file mode 100644 index 0000000000..13009f3651 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/HttpRequestBuilders.java @@ -0,0 +1,110 @@ +/* + * 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.web.client.reactive; + +import org.springframework.http.HttpMethod; + +/** + * Static factory methods for {@link DefaultHttpRequestBuilder RequestBuilders}. + * + * @author Brian Clozel + */ +public abstract class HttpRequestBuilders { + + /** + * Create a {@link DefaultHttpRequestBuilder} for a GET request. + * + * @param urlTemplate a URL template; the resulting URL will be encoded + * @param urlVariables zero or more URL variables + */ + public static DefaultHttpRequestBuilder get(String urlTemplate, Object... urlVariables) { + return new DefaultHttpRequestBuilder(HttpMethod.GET, urlTemplate, urlVariables); + } + + /** + * Create a {@link DefaultHttpRequestBuilder} for a POST request. + * + * @param urlTemplate a URL template; the resulting URL will be encoded + * @param urlVariables zero or more URL variables + */ + public static DefaultHttpRequestBuilder post(String urlTemplate, Object... urlVariables) { + return new DefaultHttpRequestBuilder(HttpMethod.POST, urlTemplate, urlVariables); + } + + + /** + * Create a {@link DefaultHttpRequestBuilder} for a PUT request. + * + * @param urlTemplate a URL template; the resulting URL will be encoded + * @param urlVariables zero or more URL variables + */ + public static DefaultHttpRequestBuilder put(String urlTemplate, Object... urlVariables) { + return new DefaultHttpRequestBuilder(HttpMethod.PUT, urlTemplate, urlVariables); + } + + /** + * Create a {@link DefaultHttpRequestBuilder} for a PATCH request. + * + * @param urlTemplate a URL template; the resulting URL will be encoded + * @param urlVariables zero or more URL variables + */ + public static DefaultHttpRequestBuilder patch(String urlTemplate, Object... urlVariables) { + return new DefaultHttpRequestBuilder(HttpMethod.PATCH, urlTemplate, urlVariables); + } + + /** + * Create a {@link DefaultHttpRequestBuilder} for a DELETE request. + * + * @param urlTemplate a URL template; the resulting URL will be encoded + * @param urlVariables zero or more URL variables + */ + public static DefaultHttpRequestBuilder delete(String urlTemplate, Object... urlVariables) { + return new DefaultHttpRequestBuilder(HttpMethod.DELETE, urlTemplate, urlVariables); + } + + /** + * Create a {@link DefaultHttpRequestBuilder} for an OPTIONS request. + * + * @param urlTemplate a URL template; the resulting URL will be encoded + * @param urlVariables zero or more URL variables + */ + public static DefaultHttpRequestBuilder options(String urlTemplate, Object... urlVariables) { + return new DefaultHttpRequestBuilder(HttpMethod.OPTIONS, urlTemplate, urlVariables); + } + + /** + * Create a {@link DefaultHttpRequestBuilder} for a HEAD request. + * + * @param urlTemplate a URL template; the resulting URL will be encoded + * @param urlVariables zero or more URL variables + */ + public static DefaultHttpRequestBuilder head(String urlTemplate, Object... urlVariables) { + return new DefaultHttpRequestBuilder(HttpMethod.HEAD, urlTemplate, urlVariables); + } + + /** + * Create a {@link DefaultHttpRequestBuilder} for a request with the given HTTP method. + * + * @param httpMethod the HTTP method + * @param urlTemplate a URL template; the resulting URL will be encoded + * @param urlVariables zero or more URL variables + */ + public static DefaultHttpRequestBuilder request(HttpMethod httpMethod, String urlTemplate, Object... urlVariables) { + return new DefaultHttpRequestBuilder(httpMethod, urlTemplate, urlVariables); + } + +} \ No newline at end of file From 255d2de5536169bd47b6bcb89b563104ee39094b Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Mon, 1 Feb 2016 18:29:13 +0100 Subject: [PATCH 210/498] Add client Request/Response impl. for Reactor-Net This commit introduces the `ClientHttpRequest` and `ClientHttpResponse` implementations for the Reactor-Net HTTP client. This client is already based on the `Flux` and `Mono` contracts. This commit also adds a `AbstractClientHttpRequest` to support the `ClientHttpRequest` implementations; it mirrors the `AbstractServerHttpResponse` contract with a `beforeCommit` to register `Supplier`s that should be notified before the request is committed. --- .../reactive/AbstractClientHttpRequest.java | 88 +++++++++++++ .../reactive/ReactorClientHttpRequest.java | 119 ++++++++++++++++++ .../reactive/ReactorClientHttpResponse.java | 72 +++++++++++ .../ReactorHttpClientRequestFactory.java | 63 ++++++++++ 4 files changed, 342 insertions(+) create mode 100644 spring-web-reactive/src/main/java/org/springframework/http/client/reactive/AbstractClientHttpRequest.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpResponse.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorHttpClientRequestFactory.java diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/AbstractClientHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/AbstractClientHttpRequest.java new file mode 100644 index 0000000000..850eea9e36 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/AbstractClientHttpRequest.java @@ -0,0 +1,88 @@ +/* + * 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.http.client.reactive; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; + +import reactor.core.publisher.Mono; + +import org.springframework.http.HttpHeaders; +import org.springframework.util.Assert; + +/** + * Base class for {@link ClientHttpRequest} implementations. + * + * @author Rossen Stoyanchev + * @author Brian Clozel + */ +public abstract class AbstractClientHttpRequest implements ClientHttpRequest { + + private final HttpHeaders headers; + + private AtomicReference state = new AtomicReference<>(State.NEW); + + private final List>> beforeCommitActions = new ArrayList<>(4); + + public AbstractClientHttpRequest(HttpHeaders httpHeaders) { + if (httpHeaders == null) { + this.headers = new HttpHeaders(); + } + else { + this.headers = httpHeaders; + } + } + + @Override + public HttpHeaders getHeaders() { + if (State.COMITTED.equals(this.state.get())) { + return HttpHeaders.readOnlyHttpHeaders(this.headers); + } + return this.headers; + } + + protected Mono applyBeforeCommit() { + Mono mono = Mono.empty(); + if (this.state.compareAndSet(State.NEW, State.COMMITTING)) { + for (Supplier> action : this.beforeCommitActions) { + mono = mono.after(() -> action.get()); + } + return mono + .otherwise(ex -> { + // Ignore errors from beforeCommit actions + return Mono.empty(); + }) + .after(() -> { + this.state.set(State.COMITTED); + //writeHeaders(); + //writeCookies(); + return Mono.empty(); + }); + } + return mono; + } + + @Override + public void beforeCommit(Supplier> action) { + Assert.notNull(action); + this.beforeCommitActions.add(action); + } + + private enum State {NEW, COMMITTING, COMITTED} +} diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java new file mode 100644 index 0000000000..e80a56ae5c --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java @@ -0,0 +1,119 @@ +/* + * 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.http.client.reactive; + +import java.net.URI; + +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.io.buffer.Buffer; +import reactor.io.net.http.HttpClient; +import reactor.io.net.http.model.Method; + +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferAllocator; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; + +/** + * {@link ClientHttpRequest} implementation for the Reactor Net HTTP client + * + * @author Brian Clozel + * @see HttpClient + */ +public class ReactorClientHttpRequest extends AbstractClientHttpRequest { + + private final DataBufferAllocator allocator; + + private final HttpMethod httpMethod; + + private final URI uri; + + private final HttpClient httpClient; + + private Flux body; + + + public ReactorClientHttpRequest(HttpMethod httpMethod, URI uri, HttpClient httpClient, HttpHeaders headers, + DataBufferAllocator allocator) { + super(headers); + this.allocator = allocator; + this.httpMethod = httpMethod; + this.uri = uri; + this.httpClient = httpClient; + } + + @Override + public HttpMethod getMethod() { + return this.httpMethod; + } + + @Override + public URI getURI() { + return this.uri; + } + + /** + * Set the body of the message to the given {@link Publisher}. + * + *

    Since the HTTP channel is not yet created when this method + * is called, the {@code Mono} return value completes immediately. + * For an event that signals that we're done writing the request, check the + * {@link #execute()} method. + * + * @return a publisher that completes immediately. + * @see #execute() + */ + @Override + public Mono setBody(Publisher body) { + + this.body = Flux.from(body).map(b -> new Buffer(b.asByteBuffer())); + return Mono.empty(); + } + + @Override + public Mono execute() { + + return this.httpClient.request(new Method(httpMethod.toString()), uri.toString(), + channel -> { + // see https://github.com/reactor/reactor-io/pull/8 + if (body == null) { + channel.headers().removeTransferEncodingChunked(); + } + return applyBeforeCommit() + .after(() -> + { + getHeaders().entrySet().stream() + .forEach(e -> channel.headers().set(e.getKey(), e.getValue())); + return Mono.empty(); + } + ) + .after(() -> { + if (body != null) { + return channel.writeBufferWith(body); + } + else { + return channel.writeHeaders(); + } + }); + }) + .map(httpChannel -> new ReactorClientHttpResponse(httpChannel, allocator)); + } + +} + diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpResponse.java new file mode 100644 index 0000000000..4eaac99918 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpResponse.java @@ -0,0 +1,72 @@ +/* + * 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.http.client.reactive; + +import java.nio.ByteBuffer; + +import reactor.core.publisher.Flux; +import reactor.io.buffer.Buffer; +import reactor.io.net.http.HttpChannel; + +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferAllocator; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; + +/** + * {@link ClientHttpResponse} implementation for the Reactor Net HTTP client + * + * @author Brian Clozel + * @see reactor.io.net.http.HttpClient + */ +public class ReactorClientHttpResponse implements ClientHttpResponse { + + private final DataBufferAllocator allocator; + + private final HttpChannel channel; + + + public ReactorClientHttpResponse(HttpChannel channel, DataBufferAllocator allocator) { + this.allocator = allocator; + this.channel = channel; + } + + @Override + public Flux getBody() { + return Flux.from(channel.input()).map(b -> allocator.wrap(b.byteBuffer())); + } + + @Override + public HttpHeaders getHeaders() { + HttpHeaders headers = new HttpHeaders(); + this.channel.responseHeaders().entries().stream().forEach(e -> headers.add(e.getKey(), e.getValue())); + return headers; + } + + @Override + public HttpStatus getStatusCode() { + return HttpStatus.valueOf(this.channel.responseStatus().getCode()); + } + + @Override + public String toString() { + return "ReactorClientHttpResponse{" + + "request=" + this.channel.method() + " " + this.channel.uri().toString() + "," + + "status=" + getStatusCode() + + '}'; + } +} diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorHttpClientRequestFactory.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorHttpClientRequestFactory.java new file mode 100644 index 0000000000..f3f83b95ae --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorHttpClientRequestFactory.java @@ -0,0 +1,63 @@ +/* + * 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.http.client.reactive; + +import java.net.URI; + +import reactor.io.net.ReactiveNet; +import reactor.io.net.http.HttpClient; + +import org.springframework.core.io.buffer.DataBufferAllocator; +import org.springframework.core.io.buffer.DefaultDataBufferAllocator; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.util.Assert; + +/** + * Create a {@link ClientHttpRequest} for the Reactor Net HTTP client + * + * @author Brian Clozel + */ +public class ReactorHttpClientRequestFactory implements ClientHttpRequestFactory { + + private final DataBufferAllocator allocator; + + private final HttpClient httpClient; + + public ReactorHttpClientRequestFactory() { + this(new DefaultDataBufferAllocator()); + } + + public ReactorHttpClientRequestFactory(DataBufferAllocator allocator) { + this(allocator, ReactiveNet.httpClient()); + } + + protected ReactorHttpClientRequestFactory(DataBufferAllocator allocator, HttpClient httpClient) { + this.allocator = allocator; + this.httpClient = httpClient; + } + + @Override + public ClientHttpRequest createRequest(HttpMethod httpMethod, URI uri, HttpHeaders headers) { + Assert.notNull(httpMethod, "HTTP method is required"); + Assert.notNull(uri, "request URI is required"); + Assert.notNull(headers, "request headers are required"); + + return new ReactorClientHttpRequest(httpMethod, uri, this.httpClient, headers, this.allocator); + } + +} From f63960af0a89eae21de18540e9ec4ba9be1bbe3d Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Mon, 1 Feb 2016 18:44:37 +0100 Subject: [PATCH 211/498] Add client Request/Response impl. for RxNetty This commit adds the `ClientHttpRequest` and `ClientHttpResponse` implementations for the RxNetty HTTP client. This client library is based on the `Single` and `Observable` composition API, so this has to be converted to the `Flux`/`Mono` variants. --- .../reactive/RxNettyClientHttpRequest.java | 124 ++++++++++++++++++ .../reactive/RxNettyClientHttpResponse.java | 67 ++++++++++ .../RxNettyHttpClientRequestFactory.java | 47 +++++++ 3 files changed, 238 insertions(+) create mode 100644 spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpRequest.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpResponse.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyHttpClientRequestFactory.java diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpRequest.java new file mode 100644 index 0000000000..74c894e7fa --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpRequest.java @@ -0,0 +1,124 @@ +/* + * 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.http.client.reactive; + +import java.net.URI; +import java.util.List; +import java.util.Map; + +import io.netty.buffer.ByteBuf; +import io.reactivex.netty.protocol.http.client.HttpClient; +import io.reactivex.netty.protocol.http.client.HttpClientRequest; +import org.reactivestreams.Publisher; +import reactor.core.converter.RxJava1ObservableConverter; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import rx.Observable; + +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.NettyDataBufferAllocator; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; + +/** + * {@link ClientHttpRequest} implementation for the RxNetty HTTP client + * + * @author Brian Clozel + */ +public class RxNettyClientHttpRequest extends AbstractClientHttpRequest { + + private final NettyDataBufferAllocator allocator; + + private final HttpMethod httpMethod; + + private final URI uri; + + private Observable body; + + + public RxNettyClientHttpRequest(HttpMethod httpMethod, URI uri, HttpHeaders headers, NettyDataBufferAllocator allocator) { + super(headers); + this.httpMethod = httpMethod; + this.uri = uri; + this.allocator = allocator; + } + + /** + * Set the body of the message to the given {@link Publisher}. + * + *

    Since the HTTP channel is not yet created when this method + * is called, the {@code Mono} return value completes immediately. + * For an event that signals that we're done writing the request, check the + * {@link #execute()} method. + * + * @return a publisher that completes immediately. + * @see #execute() + */ + @Override + public Mono setBody(Publisher body) { + + this.body = RxJava1ObservableConverter.from(Flux.from(body) + .map(b -> allocator.wrap(b.asByteBuffer()).getNativeBuffer())); + + return Mono.empty(); + } + + @Override + public HttpMethod getMethod() { + return this.httpMethod; + } + + @Override + public URI getURI() { + return this.uri; + } + + @Override + public Mono execute() { + try { + HttpClientRequest request = HttpClient + .newClient(this.uri.getHost(), this.uri.getPort()) + .createRequest(io.netty.handler.codec.http.HttpMethod.valueOf(this.httpMethod.name()), uri.getRawPath()); + + return applyBeforeCommit() + .after(() -> Mono.just(request)) + .map(req -> { + for (Map.Entry> entry : getHeaders().entrySet()) { + for (String value : entry.getValue()) { + req = req.addHeader(entry.getKey(), value); + } + } + return req; + }) + .map(req -> { + if (this.body != null) { + return RxJava1ObservableConverter.from(req.writeContent(this.body)); + } + else { + return RxJava1ObservableConverter.from(req); + } + }) + .flatMap(resp -> resp) + .next() + .map(response -> new RxNettyClientHttpResponse(response, this.allocator)); + } + catch (IllegalArgumentException exc) { + return Mono.error(exc); + } + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpResponse.java new file mode 100644 index 0000000000..a6a2efe3b2 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpResponse.java @@ -0,0 +1,67 @@ +/* + * 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.http.client.reactive; + +import io.netty.buffer.ByteBuf; +import io.reactivex.netty.protocol.http.client.HttpClientResponse; +import reactor.core.converter.RxJava1ObservableConverter; +import reactor.core.publisher.Flux; + +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.NettyDataBufferAllocator; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.util.Assert; + +/** + * {@link ClientHttpResponse} implementation for the RxNetty HTTP client + * + * @author Brian Clozel + */ +public class RxNettyClientHttpResponse implements ClientHttpResponse { + + private final HttpClientResponse response; + + private final HttpHeaders headers; + + private final NettyDataBufferAllocator allocator; + + public RxNettyClientHttpResponse(HttpClientResponse response, + NettyDataBufferAllocator allocator) { + Assert.notNull("'request', request must not be null"); + Assert.notNull(allocator, "'allocator' must not be null"); + this.allocator = allocator; + this.response = response; + this.headers = new HttpHeaders(); + this.response.headerIterator().forEachRemaining(e -> this.headers.set(e.getKey(), e.getValue())); + } + + @Override + public HttpStatus getStatusCode() { + return HttpStatus.valueOf(this.response.getStatus().code()); + } + + @Override + public Flux getBody() { + return RxJava1ObservableConverter.from(this.response.getContent().map(allocator::wrap)); + } + + @Override + public HttpHeaders getHeaders() { + return this.headers; + } +} diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyHttpClientRequestFactory.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyHttpClientRequestFactory.java new file mode 100644 index 0000000000..f96e97a0c2 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyHttpClientRequestFactory.java @@ -0,0 +1,47 @@ +/* + * 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.http.client.reactive; + +import java.net.URI; + +import org.springframework.core.io.buffer.NettyDataBufferAllocator; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.util.Assert; + +/** + * Create a {@link ClientHttpRequestFactory} for the RxNetty HTTP client + * + * @author Brian Clozel + */ +public class RxNettyHttpClientRequestFactory implements ClientHttpRequestFactory { + + private final NettyDataBufferAllocator allocator; + + public RxNettyHttpClientRequestFactory(NettyDataBufferAllocator allocator) { + this.allocator = allocator; + } + + @Override + public ClientHttpRequest createRequest(HttpMethod httpMethod, URI uri, HttpHeaders headers) { + Assert.notNull(httpMethod, "HTTP method is required"); + Assert.notNull(uri, "request URI is required"); + Assert.notNull(headers, "request headers are required"); + + return new RxNettyClientHttpRequest(httpMethod, uri, headers, this.allocator); + } +} From 69ce33e1eef6e03bbd647b8c930b3b0138a5e4e0 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Mon, 1 Feb 2016 18:46:30 +0100 Subject: [PATCH 212/498] Add WebClient and its WebResponseExtractor API This commit adds the `WebClient`, which relies on several parts of our infrastructure: * a `ClientHttpRequestFactory` to drive the underlying client library * a `HttpRequestBuilder` builder API to create the client request * a `WebResponseExtractor` to extract the "low-level" `ClientHttpResponse` into a higher level representation such as a decoded body The `WebResponseExtractors` helper class contains many extractor implementations all based on the `Flux`/`Mono` composition API. --- spring-web-reactive/build.gradle | 7 +- .../client/reactive/DefaultWebResponse.java | 52 ++++ .../web/client/reactive/WebClient.java | 139 ++++++++++ .../web/client/reactive/WebResponse.java | 44 ++++ .../client/reactive/WebResponseActions.java | 49 ++++ .../client/reactive/WebResponseExtractor.java | 31 +++ .../reactive/WebResponseExtractors.java | 123 +++++++++ .../reactive/WebClientIntegrationTests.java | 249 ++++++++++++++++++ 8 files changed, 691 insertions(+), 3 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultWebResponse.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebClient.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebResponse.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebResponseActions.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebResponseExtractor.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebResponseExtractors.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/client/reactive/WebClientIntegrationTests.java diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index a35115c7e0..2d359c0d66 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -102,14 +102,15 @@ dependencies { testCompile "org.springframework:spring-test:${springVersion}" testCompile "org.slf4j:slf4j-jcl:1.7.12" testCompile "org.slf4j:jul-to-slf4j:1.7.12" - testCompile("log4j:log4j:1.2.16") + testCompile "log4j:log4j:1.2.16" testCompile("org.mockito:mockito-core:1.10.19") { exclude group: 'org.hamcrest', module: 'hamcrest-core' } - testCompile("org.hamcrest:hamcrest-all:1.3") + testCompile "org.hamcrest:hamcrest-all:1.3" + testCompile "com.squareup.okhttp3:mockwebserver:3.0.1" // Needed to run Javadoc without error - optional("org.apache.httpcomponents:httpclient:4.5.1") + optional "org.apache.httpcomponents:httpclient:4.5.1" } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultWebResponse.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultWebResponse.java new file mode 100644 index 0000000000..328d1198e2 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultWebResponse.java @@ -0,0 +1,52 @@ +/* + * 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.web.client.reactive; + +import java.util.List; + +import reactor.core.publisher.Mono; + +import org.springframework.core.codec.Decoder; +import org.springframework.http.client.reactive.ClientHttpResponse; + +/** + * Default implementation of the {@link WebResponse} interface + * + * @author Brian Clozel + */ +public class DefaultWebResponse implements WebResponse { + + private final Mono clientResponse; + + private final List> messageDecoders; + + + public DefaultWebResponse(Mono clientResponse, List> messageDecoders) { + this.clientResponse = clientResponse; + this.messageDecoders = messageDecoders; + } + + @Override + public Mono getClientResponse() { + return this.clientResponse; + } + + @Override + public List> getMessageDecoders() { + return this.messageDecoders; + } +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebClient.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebClient.java new file mode 100644 index 0000000000..d733acc803 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebClient.java @@ -0,0 +1,139 @@ +/* + * 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.web.client.reactive; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; + +import reactor.core.publisher.Mono; + +import org.springframework.core.codec.Decoder; +import org.springframework.core.codec.Encoder; +import org.springframework.core.codec.support.ByteBufferDecoder; +import org.springframework.core.codec.support.ByteBufferEncoder; +import org.springframework.core.codec.support.JacksonJsonDecoder; +import org.springframework.core.codec.support.JacksonJsonEncoder; +import org.springframework.core.codec.support.JsonObjectDecoder; +import org.springframework.core.codec.support.StringDecoder; +import org.springframework.core.codec.support.StringEncoder; +import org.springframework.core.io.buffer.DataBufferAllocator; +import org.springframework.core.io.buffer.DefaultDataBufferAllocator; +import org.springframework.http.HttpStatus; +import org.springframework.http.client.reactive.ClientHttpRequest; +import org.springframework.http.client.reactive.ClientHttpRequestFactory; +import org.springframework.http.client.reactive.ClientHttpResponse; + +/** + * Reactive Web client supporting the HTTP/1.1 protocol + * + *

    Here is a simple example of a GET request: + *

    + * WebClient client = new WebClient(new ReactorHttpClientRequestFactory());
    + * Mono<String> result = client
    + * 		.perform(HttpRequestBuilders.get("http://example.org/resource")
    + * 			.accept(MediaType.TEXT_PLAIN))
    + * 		.extract(WebResponseExtractors.body(String.class));
    + * 
    + * + *

    This Web client relies on + *

      + *
    • a {@link ClientHttpRequestFactory} that drives the underlying library (e.g. Reactor-Net, RxNetty...)
    • + *
    • an {@link HttpRequestBuilder} which create a Web request with a builder API (see {@link HttpRequestBuilders})
    • + *
    • an {@link WebResponseExtractor} which extracts the relevant part of the server response + * with the composition API of choice (see {@link WebResponseExtractors}
    • + *
    + * + * @author Brian Clozel + * @see HttpRequestBuilders + * @see WebResponseExtractors + */ +public final class WebClient { + + private ClientHttpRequestFactory requestFactory; + + private List> messageEncoders; + + private List> messageDecoders; + + /** + * Create a {@code ReactiveRestClient} instance, using the {@link ClientHttpRequestFactory} + * implementation given as an argument to drive the underlying HTTP client implementation. + * + * Register by default the following Encoders and Decoders: + *
      + *
    • {@link ByteBufferEncoder} / {@link ByteBufferDecoder}
    • + *
    • {@link StringEncoder} / {@link StringDecoder}
    • + *
    • {@link JacksonJsonEncoder} / {@link JacksonJsonDecoder}
    • + *
    + * + * @param requestFactory the {@code ClientHttpRequestFactory} to use + */ + public WebClient(ClientHttpRequestFactory requestFactory) { + this.requestFactory = requestFactory; + DataBufferAllocator allocator = new DefaultDataBufferAllocator(); + this.messageEncoders = Arrays.asList(new ByteBufferEncoder(allocator), new StringEncoder(allocator), + new JacksonJsonEncoder(allocator)); + this.messageDecoders = Arrays.asList(new ByteBufferDecoder(), new StringDecoder(allocator), + new JacksonJsonDecoder(new JsonObjectDecoder(allocator))); + } + + /** + * Set the list of {@link Encoder}s to use for encoding messages + */ + public void setMessageEncoders(List> messageEncoders) { + this.messageEncoders = messageEncoders; + } + + /** + * Set the list of {@link Decoder}s to use for decoding messages + */ + public void setMessageDecoders(List> messageDecoders) { + this.messageDecoders = messageDecoders; + } + + /** + * Perform the actual HTTP request/response exchange + * + *

    Pulling demand from the exposed {@code Flux} will result in: + *

      + *
    • building the actual HTTP request using the provided {@code RequestBuilder}
    • + *
    • encoding the HTTP request body with the configured {@code Encoder}s
    • + *
    • returning the response with a publisher of the body
    • + *
    + */ + public WebResponseActions perform(DefaultHttpRequestBuilder builder) { + + ClientHttpRequest request = builder.setMessageEncoders(messageEncoders).build(requestFactory); + final Mono clientResponse = request.execute() + .log("org.springframework.http.client.reactive"); + + return new WebResponseActions() { + @Override + public void doWithStatus(Consumer consumer) { + // TODO: implement + } + + @Override + public T extract(WebResponseExtractor extractor) { + return extractor.extract(new DefaultWebResponse(clientResponse, messageDecoders)); + } + + }; + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebResponse.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebResponse.java new file mode 100644 index 0000000000..6a92847dd1 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebResponse.java @@ -0,0 +1,44 @@ +/* + * 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.web.client.reactive; + +import java.util.List; + +import reactor.core.publisher.Mono; + +import org.springframework.core.codec.Decoder; +import org.springframework.http.client.reactive.ClientHttpResponse; + +/** + * Result of a {@code ClientHttpRequest} sent to a remote server by the {@code WebClient} + * + *

    Contains all the required information to extract relevant information from the raw response. + * + * @author Brian Clozel + */ +public interface WebResponse { + + /** + * Return the raw response received by the {@code WebClient} + */ + Mono getClientResponse(); + + /** + * Return the configured list of {@link Decoder}s that can be used to decode the raw response body + */ + List> getMessageDecoders(); +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebResponseActions.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebResponseActions.java new file mode 100644 index 0000000000..e18e2decdf --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebResponseActions.java @@ -0,0 +1,49 @@ +/* + * 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.web.client.reactive; + +import java.util.function.Consumer; + +import org.springframework.http.HttpStatus; + +/** + * Allows applying actions, such as extractors, on the result of an executed + * {@link WebClient} request. + * + * @author Brian Clozel + */ +public interface WebResponseActions { + + /** + * Apply synchronous operations once the HTTP response status + * has been received. + */ + void doWithStatus(Consumer consumer); + + /** + * Perform an extraction of the response body into a higher level representation. + * + *

    +	 * static imports: HttpRequestBuilders.*, HttpResponseExtractors.*
    +	 *
    +	 * webClient
    +	 *   .perform(get(baseUrl.toString()).accept(MediaType.TEXT_PLAIN))
    +	 *   .extract(response(String.class));
    +	 * 
    + */ + T extract(WebResponseExtractor extractor); +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebResponseExtractor.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebResponseExtractor.java new file mode 100644 index 0000000000..2bb1592447 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebResponseExtractor.java @@ -0,0 +1,31 @@ +/* + * 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.web.client.reactive; + +/** + * A {@code WebResponseExtractor} extracts the relevant part of a + * raw {@link org.springframework.http.client.reactive.ClientHttpResponse}, + * optionally decoding the response body and using a target composition API. + * + *

    See static factory methods in {@link WebResponseExtractors}. + * + * @author Brian Clozel + */ +public interface WebResponseExtractor { + + T extract(WebResponse webResponse); +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebResponseExtractors.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebResponseExtractors.java new file mode 100644 index 0000000000..9c24c76d35 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebResponseExtractors.java @@ -0,0 +1,123 @@ +/* + * 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.web.client.reactive; + +import java.nio.charset.Charset; +import java.util.List; +import java.util.Optional; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.core.ResolvableType; +import org.springframework.core.codec.Decoder; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.reactive.ClientHttpResponse; + +/** + * Static factory methods for {@link WebResponseExtractor} + * based on the {@link Flux} and {@link Mono} API. + * + * @author Brian Clozel + */ +public class WebResponseExtractors { + + private static final Charset UTF_8 = Charset.forName("UTF-8"); + + private static final Object[] HINTS = new Object[] {UTF_8}; + + /** + * Extract the response body and decode it, returning it as a {@code Mono} + */ + public static WebResponseExtractor> body(Class sourceClass) { + + ResolvableType resolvableType = ResolvableType.forClass(sourceClass); + //noinspection unchecked + return webResponse -> (Mono) webResponse.getClientResponse() + .flatMap(resp -> decodeResponseBody(resp, resolvableType, webResponse.getMessageDecoders())) + .next(); + } + + /** + * Extract the response body and decode it, returning it as a {@code Flux} + */ + public static WebResponseExtractor> bodyStream(Class sourceClass) { + + ResolvableType resolvableType = ResolvableType.forClass(sourceClass); + return webResponse -> webResponse.getClientResponse() + .flatMap(resp -> decodeResponseBody(resp, resolvableType, webResponse.getMessageDecoders())); + } + + /** + * Extract the full response body as a {@code ResponseEntity} + * with its body decoded as a single type {@code T} + */ + public static WebResponseExtractor>> response(Class sourceClass) { + + ResolvableType resolvableType = ResolvableType.forClass(sourceClass); + return webResponse -> webResponse.getClientResponse() + .then(response -> + Mono.when( + decodeResponseBody(response, resolvableType, webResponse.getMessageDecoders()).next(), + Mono.just(response.getHeaders()), + Mono.just(response.getStatusCode()))) + .map(tuple -> { + //noinspection unchecked + return new ResponseEntity<>((T) tuple.getT1(), tuple.getT2(), tuple.getT3()); + }); + } + + /** + * Extract the full response body as a {@code ResponseEntity} + * with its body decoded as a {@code Flux} + */ + public static WebResponseExtractor>>> responseStream(Class sourceClass) { + ResolvableType resolvableType = ResolvableType.forClass(sourceClass); + return webResponse -> webResponse.getClientResponse() + .map(response -> new ResponseEntity<>( + decodeResponseBody(response, resolvableType, webResponse.getMessageDecoders()), + response.getHeaders(), response.getStatusCode())); + } + + /** + * Extract the response headers as an {@code HttpHeaders} instance + */ + public static WebResponseExtractor> headers() { + return webResponse -> webResponse.getClientResponse().map(resp -> resp.getHeaders()); + } + + protected static Flux decodeResponseBody(ClientHttpResponse response, ResolvableType responseType, + List> messageDecoders) { + + MediaType contentType = response.getHeaders().getContentType(); + Optional> decoder = resolveDecoder(messageDecoders, responseType, contentType); + if (!decoder.isPresent()) { + return Flux.error(new IllegalStateException("Could not decode response body of type '" + contentType + + "' with target type '" + responseType.toString() + "'")); + } + //noinspection unchecked + return (Flux) decoder.get().decode(response.getBody(), responseType, contentType, HINTS); + } + + + protected static Optional> resolveDecoder(List> messageDecoders, ResolvableType type, + MediaType mediaType) { + return messageDecoders.stream().filter(e -> e.canDecode(type, mediaType)).findFirst(); + } +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/client/reactive/WebClientIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/client/reactive/WebClientIntegrationTests.java new file mode 100644 index 0000000000..d77ac035dd --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/client/reactive/WebClientIntegrationTests.java @@ -0,0 +1,249 @@ +/* + * 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.web.client.reactive; + +import static org.junit.Assert.*; +import static org.springframework.web.client.reactive.HttpRequestBuilders.*; +import static org.springframework.web.client.reactive.WebResponseExtractors.*; + +import okhttp3.HttpUrl; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.hamcrest.Matchers; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.test.TestSubscriber; +import reactor.fn.Consumer; + +import org.springframework.core.codec.support.Pojo; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.reactive.ReactorHttpClientRequestFactory; + +/** + * @author Brian Clozel + */ +public class WebClientIntegrationTests { + + private MockWebServer server; + + private WebClient webClient; + + @Before + public void setup() { + this.server = new MockWebServer(); + this.webClient = new WebClient(new ReactorHttpClientRequestFactory()); + } + + @Test + public void shouldGetHeaders() throws Exception { + + HttpUrl baseUrl = server.url("/greeting?name=Spring"); + this.server.enqueue(new MockResponse().setHeader("Content-Type", "text/plain").setBody("Hello Spring!")); + + Mono result = this.webClient + .perform(get(baseUrl.toString())) + .extract(headers()); + + TestSubscriber ts = new TestSubscriber(); + result.subscribe(ts); + ts.awaitAndAssertValuesWith( + httpHeaders -> { + assertEquals(MediaType.TEXT_PLAIN, httpHeaders.getContentType()); + assertEquals(13L, httpHeaders.getContentLength()); + } + ).assertComplete(); + + RecordedRequest request = server.takeRequest(); + assertEquals("*/*", request.getHeader(HttpHeaders.ACCEPT)); + assertEquals("/greeting?name=Spring", request.getPath()); + } + + @Test + public void shouldGetPlainTextResponseAsObject() throws Exception { + + HttpUrl baseUrl = server.url("/greeting?name=Spring"); + this.server.enqueue(new MockResponse().setBody("Hello Spring!")); + + Mono result = this.webClient + .perform(get(baseUrl.toString()) + .header("X-Test-Header", "testvalue")) + .extract(body(String.class)); + + + TestSubscriber ts = new TestSubscriber(); + result.subscribe(ts); + ts.awaitAndAssertValues("Hello Spring!").assertComplete(); + + RecordedRequest request = server.takeRequest(); + assertEquals("testvalue", request.getHeader("X-Test-Header")); + assertEquals("*/*", request.getHeader(HttpHeaders.ACCEPT)); + assertEquals("/greeting?name=Spring", request.getPath()); + } + + @Test + public void shouldGetPlainTextResponse() throws Exception { + + HttpUrl baseUrl = server.url("/greeting?name=Spring"); + this.server.enqueue(new MockResponse().setHeader("Content-Type", "text/plain").setBody("Hello Spring!")); + + Mono> result = this.webClient + .perform(get(baseUrl.toString()) + .accept(MediaType.TEXT_PLAIN)) + .extract(response(String.class)); + + TestSubscriber> ts = new TestSubscriber(); + result.subscribe(ts); + ts.awaitAndAssertValuesWith(new Consumer>() { + @Override + public void accept(ResponseEntity response) { + assertEquals(200, response.getStatusCode().value()); + assertEquals(MediaType.TEXT_PLAIN, response.getHeaders().getContentType()); + assertEquals("Hello Spring!", response.getBody()); + } + }); + RecordedRequest request = server.takeRequest(); + assertEquals("/greeting?name=Spring", request.getPath()); + assertEquals("text/plain", request.getHeader(HttpHeaders.ACCEPT)); + } + + @Test + public void shouldGetJsonAsMonoOfPojo() throws Exception { + + HttpUrl baseUrl = server.url("/pojo"); + this.server.enqueue(new MockResponse().setHeader("Content-Type", "application/json") + .setBody("{\"bar\":\"barbar\",\"foo\":\"foofoo\"}")); + + Mono result = this.webClient + .perform(get(baseUrl.toString()) + .accept(MediaType.APPLICATION_JSON)) + .extract(body(Pojo.class)); + + TestSubscriber ts = new TestSubscriber(); + result.subscribe(ts); + ts.awaitAndAssertValuesWith(p -> assertEquals("barbar", p.getBar())).assertComplete(); + RecordedRequest request = server.takeRequest(); + assertEquals("/pojo", request.getPath()); + assertEquals("application/json", request.getHeader(HttpHeaders.ACCEPT)); + } + + @Test + public void shouldGetJsonAsFluxOfPojos() throws Exception { + + HttpUrl baseUrl = server.url("/pojos"); + this.server.enqueue(new MockResponse().setHeader("Content-Type", "application/json") + .setBody("[{\"bar\":\"bar1\",\"foo\":\"foo1\"},{\"bar\":\"bar2\",\"foo\":\"foo2\"}]")); + + Flux result = this.webClient + .perform(get(baseUrl.toString()) + .accept(MediaType.APPLICATION_JSON)) + .extract(bodyStream(Pojo.class)); + + TestSubscriber ts = new TestSubscriber(); + result.subscribe(ts); + ts.awaitAndAssertValuesWith( + p -> assertThat(p.getBar(), Matchers.is("bar1")), + p -> assertThat(p.getBar(), Matchers.is("bar2")) + ).assertValueCount(2).assertComplete(); + RecordedRequest request = server.takeRequest(); + assertEquals("/pojos", request.getPath()); + assertEquals("application/json", request.getHeader(HttpHeaders.ACCEPT)); + } + + @Test + public void shouldGetJsonAsResponseOfPojosStream() throws Exception { + + HttpUrl baseUrl = server.url("/pojos"); + this.server.enqueue(new MockResponse().setHeader("Content-Type", "application/json") + .setBody("[{\"bar\":\"bar1\",\"foo\":\"foo1\"},{\"bar\":\"bar2\",\"foo\":\"foo2\"}]")); + + Mono>> result = this.webClient + .perform(get(baseUrl.toString()) + .accept(MediaType.APPLICATION_JSON)) + .extract(responseStream(Pojo.class)); + + TestSubscriber>> ts = new TestSubscriber(); + result.subscribe(ts); + ts.awaitAndAssertValuesWith( + response -> { + assertEquals(200, response.getStatusCode().value()); + assertEquals(MediaType.APPLICATION_JSON, response.getHeaders().getContentType()); + } + ).assertComplete(); + RecordedRequest request = server.takeRequest(); + assertEquals("/pojos", request.getPath()); + assertEquals("application/json", request.getHeader(HttpHeaders.ACCEPT)); + } + + @Test + public void shouldPostPojoAsJson() throws Exception { + + HttpUrl baseUrl = server.url("/pojo/capitalize"); + this.server.enqueue(new MockResponse().setBody("{\"bar\":\"BARBAR\",\"foo\":\"FOOFOO\"}")); + + Pojo spring = new Pojo("foofoo", "barbar"); + Mono result = this.webClient + .perform(post(baseUrl.toString()) + .content(spring) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .extract(body(Pojo.class)); + + TestSubscriber ts = new TestSubscriber(); + result.subscribe(ts); + ts.awaitAndAssertValuesWith(p -> assertEquals("BARBAR", p.getBar())).assertComplete(); + + RecordedRequest request = server.takeRequest(); + assertEquals("/pojo/capitalize", request.getPath()); + assertEquals("{\"foo\":\"foofoo\",\"bar\":\"barbar\"}", request.getBody().readUtf8()); + assertEquals("chunked", request.getHeader(HttpHeaders.TRANSFER_ENCODING)); + assertEquals("application/json", request.getHeader(HttpHeaders.ACCEPT)); + assertEquals("application/json", request.getHeader(HttpHeaders.CONTENT_TYPE)); + } + + @Test + public void shouldGetErrorWhen404() throws Exception { + + HttpUrl baseUrl = server.url("/greeting?name=Spring"); + this.server.enqueue(new MockResponse().setResponseCode(404)); + + Mono result = this.webClient + .perform(get(baseUrl.toString())) + .extract(body(String.class)); + + + TestSubscriber ts = new TestSubscriber(); + result.subscribe(ts); + // TODO: error message should be converted to a ClientException + ts.await().assertError(); + + RecordedRequest request = server.takeRequest(); + assertEquals("*/*", request.getHeader(HttpHeaders.ACCEPT)); + assertEquals("/greeting?name=Spring", request.getPath()); + } + + @After + public void tearDown() throws Exception { + this.server.shutdown(); + } + +} From 7ab92cfccfc160c4a3e65963bc91166d39f84ed9 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Mon, 1 Feb 2016 21:03:29 +0100 Subject: [PATCH 213/498] Add WebResponseExtractor impl for RxJava1 This commit adds `RxJava1WebResponseExtractors`, a static factory of `WebResponseExtractor`s that are based on the RxJava1 composition API. All extracted types are based on the `Single` and `Observable` types. --- .../RxJava1WebResponseExtractors.java | 131 ++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/client/reactive/RxJava1WebResponseExtractors.java diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/RxJava1WebResponseExtractors.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/RxJava1WebResponseExtractors.java new file mode 100644 index 0000000000..0eb51c70d8 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/RxJava1WebResponseExtractors.java @@ -0,0 +1,131 @@ +/* + * 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.web.client.reactive; + +import java.nio.charset.Charset; +import java.util.List; +import java.util.Optional; + +import reactor.core.converter.RxJava1ObservableConverter; +import reactor.core.converter.RxJava1SingleConverter; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import rx.Observable; +import rx.Single; + +import org.springframework.core.ResolvableType; +import org.springframework.core.codec.Decoder; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.reactive.ClientHttpResponse; + +/** + * Static factory methods for {@link WebResponseExtractor} + * based on the {@link Observable} and {@link Single} API. + * + * @author Brian Clozel + */ +public class RxJava1WebResponseExtractors { + + private static final Charset UTF_8 = Charset.forName("UTF-8"); + + private static final Object[] HINTS = new Object[] {UTF_8}; + + /** + * Extract the response body and decode it, returning it as a {@code Single} + */ + public static WebResponseExtractor> body(Class sourceClass) { + + ResolvableType resolvableType = ResolvableType.forClass(sourceClass); + //noinspection unchecked + return webResponse -> (Single) RxJava1SingleConverter.from(webResponse.getClientResponse() + .flatMap(resp -> decodeResponseBody(resp, resolvableType, webResponse.getMessageDecoders())) + .next()); + } + + /** + * Extract the response body and decode it, returning it as an {@code Observable} + */ + public static WebResponseExtractor> bodyStream(Class sourceClass) { + + ResolvableType resolvableType = ResolvableType.forClass(sourceClass); + return webResponse -> RxJava1ObservableConverter.from(webResponse.getClientResponse() + .flatMap(resp -> decodeResponseBody(resp, resolvableType, webResponse.getMessageDecoders()))); + } + + /** + * Extract the full response body as a {@code ResponseEntity} + * with its body decoded as a single type {@code T} + */ + public static WebResponseExtractor>> response(Class sourceClass) { + + ResolvableType resolvableType = ResolvableType.forClass(sourceClass); + return webResponse -> (Single>) + RxJava1SingleConverter.from(webResponse.getClientResponse() + .then(response -> + Mono.when( + decodeResponseBody(response, resolvableType, webResponse.getMessageDecoders()).next(), + Mono.just(response.getHeaders()), + Mono.just(response.getStatusCode()))) + .map(tuple -> { + //noinspection unchecked + return new ResponseEntity<>((T) tuple.getT1(), tuple.getT2(), tuple.getT3()); + })); + } + + /** + * Extract the full response body as a {@code ResponseEntity} + * with its body decoded as an {@code Observable} + */ + public static WebResponseExtractor>>> responseStream(Class sourceClass) { + ResolvableType resolvableType = ResolvableType.forClass(sourceClass); + return webResponse -> RxJava1SingleConverter.from(webResponse.getClientResponse() + .map(response -> new ResponseEntity<>( + RxJava1ObservableConverter + .from(decodeResponseBody(response, resolvableType, webResponse.getMessageDecoders())), + response.getHeaders(), + response.getStatusCode()))); + } + + /** + * Extract the response headers as an {@code HttpHeaders} instance + */ + public static WebResponseExtractor> headers() { + return webResponse -> RxJava1SingleConverter + .from(webResponse.getClientResponse().map(resp -> resp.getHeaders())); + } + + protected static Flux decodeResponseBody(ClientHttpResponse response, ResolvableType responseType, + List> messageDecoders) { + + MediaType contentType = response.getHeaders().getContentType(); + Optional> decoder = resolveDecoder(messageDecoders, responseType, contentType); + if (!decoder.isPresent()) { + return Flux.error(new IllegalStateException("Could not decode response body of type '" + contentType + + "' with target type '" + responseType.toString() + "'")); + } + //noinspection unchecked + return (Flux) decoder.get().decode(response.getBody(), responseType, contentType, HINTS); + } + + + protected static Optional> resolveDecoder(List> messageDecoders, ResolvableType type, + MediaType mediaType) { + return messageDecoders.stream().filter(e -> e.canDecode(type, mediaType)).findFirst(); + } +} From 52292cafaeb9c9c5740510c747098c9560aed608 Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Thu, 4 Feb 2016 03:08:26 +0000 Subject: [PATCH 214/498] sync reactor-core scheduler --- .../http/server/reactive/AsyncIntegrationTests.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java index 8ac52a6114..8463c12949 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java @@ -25,7 +25,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import reactor.core.publisher.Mono; -import reactor.core.publisher.ProcessorGroup; +import reactor.core.publisher.SchedulerGroup; import reactor.core.timer.Timer; import reactor.rx.Stream; @@ -50,7 +50,7 @@ import static org.junit.Assert.assertThat; @RunWith(Parameterized.class) public class AsyncIntegrationTests { - private final ProcessorGroup asyncGroup = ProcessorGroup.async(); + private final SchedulerGroup asyncGroup = SchedulerGroup.async(); private final DataBufferAllocator allocator = new DefaultDataBufferAllocator(); From 4a2497918185c2dca973f114045f2d3c80bc851b Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Fri, 5 Feb 2016 14:31:48 +0100 Subject: [PATCH 215/498] Various DataBuffer utility enhancements - Javadoc for DataBufferUtils - Introduction of DataBufferTestUtils, containing methods for dumping the entire contents of a buffer --- .../core/io/buffer/DefaultDataBuffer.java | 2 +- .../io/buffer/support/DataBufferUtils.java | 33 ++++++-- .../buffer/support/DataBufferTestUtils.java | 65 +++++++++++++++ .../support/DataBufferTestUtilsTests.java | 79 +++++++++++++++++++ 4 files changed, 171 insertions(+), 8 deletions(-) create mode 100644 spring-web-reactive/src/test/java/org/springframework/core/io/buffer/support/DataBufferTestUtils.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/core/io/buffer/support/DataBufferTestUtilsTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java index 4a1c73bb34..27f123ac82 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java @@ -244,7 +244,7 @@ public class DefaultDataBuffer implements DataBuffer { @Override public int available() throws IOException { - return byteBuffer.limit() - readPosition; + return readableByteCount(); } @Override diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java index 4839958233..d4bcc1ad6c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java @@ -25,23 +25,42 @@ import org.springframework.core.io.buffer.DataBuffer; import org.springframework.util.Assert; /** + * Utility class for working with {@link DataBuffer}s. + * * @author Arjen Poutsma */ public abstract class DataBufferUtils { + /** + * Returns the given {@link DataBuffer} as a {@link Flux} of bytes. + * @param buffer the buffer to return the bytes of + * @return the bytes as a flux + */ public static Flux toPublisher(DataBuffer buffer) { Assert.notNull(buffer, "'buffer' must not be null"); - byte[] bytes1 = new byte[buffer.readableByteCount()]; - buffer.read(bytes1); + byte[] bytes = new byte[buffer.readableByteCount()]; + buffer.read(bytes); - Byte[] bytes2 = new Byte[bytes1.length]; - for (int i = 0; i < bytes1.length; i++) { - bytes2[i] = bytes1[i]; - } - return Flux.fromArray(bytes2); + Byte[] bytesObjects = box(bytes); + + return Flux.fromArray(bytesObjects); } + private static Byte[] box(byte[] bytes) { + Byte[] bytesObjects = new Byte[bytes.length]; + for (int i = 0; i < bytes.length; i++) { + bytesObjects[i] = bytes[i]; + } + return bytesObjects; + } + + /** + * Returns the given data buffer publisher as an input stream, streaming over all + * underlying buffers when available. + * @param publisher the publisher to create the input stream for + * @return the input stream + */ public static InputStream toInputStream(Publisher publisher) { return new DataBufferPublisherInputStream(publisher); } diff --git a/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/support/DataBufferTestUtils.java b/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/support/DataBufferTestUtils.java new file mode 100644 index 0000000000..9d7cecd137 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/support/DataBufferTestUtils.java @@ -0,0 +1,65 @@ +/* + * 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.io.buffer.support; + +import java.nio.charset.Charset; + +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.util.Assert; + +/** + * Utility class for working with {@link DataBuffer}s in tests. + * + *

    Note that this class is in the {@code test} tree of the project: the methods + * contained herein are not suitable for production code bases. + * + * @author Arjen Poutsma + */ +public abstract class DataBufferTestUtils { + + /** + * Dumps all the bytes in the given data buffer, and returns them as a byte array. + * + *

    Note that this method reads the entire buffer into the heap, which might + * consume a lot of memory. + * @param buffer the data buffer to dump the bytes of + * @return the bytes in the given data buffer + */ + public static byte[] dumpBytes(DataBuffer buffer) { + Assert.notNull(buffer, "'buffer' must not be null"); + + byte[] bytes = new byte[buffer.readableByteCount()]; + buffer.read(bytes); + return bytes; + } + + /** + * Dumps all the bytes in the given data buffer, and returns them as a string. + * + *

    Note that this method reads the entire buffer into the heap, which might + * consume a lot of memory. + * @param buffer the data buffer to dump the string contents of + * @param charset the charset of the data + * @return the string representation of the given data buffer + */ + public static String dumpString(DataBuffer buffer, Charset charset) { + Assert.notNull(charset, "'charset' must not be null"); + + byte[] bytes = dumpBytes(buffer); + return new String(bytes, charset); + } +} diff --git a/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/support/DataBufferTestUtilsTests.java b/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/support/DataBufferTestUtilsTests.java new file mode 100644 index 0000000000..98267b1547 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/support/DataBufferTestUtilsTests.java @@ -0,0 +1,79 @@ +/* + * 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.io.buffer.support; + +import java.nio.charset.StandardCharsets; + +import io.netty.buffer.PooledByteBufAllocator; +import io.netty.buffer.UnpooledByteBufAllocator; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferAllocator; +import org.springframework.core.io.buffer.DefaultDataBufferAllocator; +import org.springframework.core.io.buffer.NettyDataBufferAllocator; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +/** + * @author Arjen Poutsma + */ +@RunWith(Parameterized.class) +public class DataBufferTestUtilsTests { + + @Parameterized.Parameter + public DataBufferAllocator allocator; + + @Parameterized.Parameters(name = "{0}") + public static Object[][] buffers() { + + return new Object[][]{ + {new NettyDataBufferAllocator(new UnpooledByteBufAllocator(true))}, + {new NettyDataBufferAllocator(new UnpooledByteBufAllocator(false))}, + {new NettyDataBufferAllocator(new PooledByteBufAllocator(true))}, + {new NettyDataBufferAllocator(new PooledByteBufAllocator(false))}, + {new DefaultDataBufferAllocator(true)}, + {new DefaultDataBufferAllocator(false)}}; + } + + @Test + public void dumpBytes() { + DataBuffer buffer = allocator.allocateBuffer(4); + byte[] source = {'a', 'b', 'c', 'd'}; + buffer.write(source); + + byte[] result = DataBufferTestUtils.dumpBytes(buffer); + + assertArrayEquals(source, result); + } + + @Test + public void dumpString() { + DataBuffer buffer = allocator.allocateBuffer(4); + String source = "abcd"; + buffer.write(source.getBytes(StandardCharsets.UTF_8)); + + String result = DataBufferTestUtils.dumpString(buffer, StandardCharsets.UTF_8); + + + assertEquals(source, result); + } + +} \ No newline at end of file From b7b423a0033f22de8ce49c09fcb39e81eaecc94c Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Mon, 8 Feb 2016 00:25:52 +0000 Subject: [PATCH 216/498] Remove Stream#toBlockingQueue use --- .../DataBufferPublisherInputStream.java | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferPublisherInputStream.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferPublisherInputStream.java index abcd0ddfea..5405b9b65b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferPublisherInputStream.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferPublisherInputStream.java @@ -18,12 +18,12 @@ package org.springframework.core.io.buffer.support; import java.io.IOException; import java.io.InputStream; -import java.util.concurrent.BlockingQueue; +import java.util.Iterator; import java.util.concurrent.atomic.AtomicBoolean; import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; -import reactor.rx.Stream; +import reactor.core.publisher.Flux; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.util.Assert; @@ -35,7 +35,7 @@ class DataBufferPublisherInputStream extends InputStream { private final AtomicBoolean completed = new AtomicBoolean(); - private final BlockingQueue queue; + private final Iterator queue; private InputStream currentStream; @@ -57,8 +57,7 @@ class DataBufferPublisherInputStream extends InputStream { int requestSize) { Assert.notNull(publisher, "'publisher' must not be null"); - // TODO Avoid using Reactor Stream, it should not be a mandatory dependency of Spring Reactive - this.queue = Stream.from(publisher).toBlockingQueue(requestSize); + this.queue = Flux.from(publisher).toIterable(requestSize).iterator(); } @Override @@ -126,25 +125,22 @@ class DataBufferPublisherInputStream extends InputStream { return this.currentStream; } else { - // take() blocks until next or complete() then return null, - // but that's OK since this is a *blocking* InputStream - DataBuffer signal = this.queue.take(); - if (signal == null) { + // if upstream Publisher has completed, then complete() and return null, + if (!this.queue.hasNext()) { this.completed.set(true); return null; } + // next() blocks until next + // but that's OK since this is a *blocking* InputStream + DataBuffer signal = this.queue.next(); this.currentStream = signal.asInputStream(); return this.currentStream; } } - catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - } catch (Throwable error) { this.completed.set(true); throw new IOException(error); } - throw new IOException(); } From e92174c772c0d787be7721ae00c7189f16b05fb5 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 2 Feb 2016 11:40:49 -0500 Subject: [PATCH 217/498] Update HandlerResult Rename result to returnValue and resultType to returnValueType to reflect what they represent. The returnValue getter is also wrapped as Optional since the value returned from a handler may be null (e.g. void method, null value). --- .../web/reactive/DispatcherHandler.java | 2 +- .../web/reactive/HandlerResult.java | 61 +++++++++++++------ .../handler/SimpleHandlerResultHandler.java | 14 +++-- .../annotation/ResponseBodyResultHandler.java | 11 ++-- .../method/InvocableHandlerMethodTests.java | 8 +-- 5 files changed, 62 insertions(+), 34 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java index 7b37bf23a7..318f52a991 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java @@ -146,7 +146,7 @@ public class DispatcherHandler implements WebHandler, ApplicationContextAware { return resultHandler; } } - throw new IllegalStateException("No HandlerResultHandler for " + handlerResult.getResult()); + throw new IllegalStateException("No HandlerResultHandler for " + handlerResult.getReturnValue()); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java index 9ffae86ba4..69b21f9986 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java @@ -16,6 +16,7 @@ package org.springframework.web.reactive; +import java.util.Optional; import java.util.function.Function; import reactor.core.publisher.Mono; @@ -32,40 +33,54 @@ public class HandlerResult { private final Object handler; - private final Object result; + private final Optional returnValue; - private final ResolvableType resultType; + private final ResolvableType returnValueType; private Function> exceptionHandler; - public HandlerResult(Object handler, Object result, ResolvableType resultType) { + /** + * Create a new {@code HandlerResult}. + * @param handler the handler that handled the request + * @param returnValue the return value from the handler possibly {@code null} + * @param returnValueType the return value type + */ + public HandlerResult(Object handler, Object returnValue, ResolvableType returnValueType) { Assert.notNull(handler, "'handler' is required"); - Assert.notNull(handler, "'resultType' is required"); + Assert.notNull(returnValueType, "'returnValueType' is required"); this.handler = handler; - this.result = result; - this.resultType = resultType; + this.returnValue = Optional.ofNullable(returnValue); + this.returnValueType = returnValueType; } + /** + * Return the handler that handled the request. + */ public Object getHandler() { return this.handler; } - public Object getResult() { - return this.result; - } - - public ResolvableType getResultType() { - return this.resultType; + /** + * Return the value returned from the handler wrapped as {@link Optional}. + */ + public Optional getReturnValue() { + return this.returnValue; } /** - * For an async result, failures may occur later during result handling. - * Use this property to configure an exception handler to be invoked if - * result handling fails. - * - * @param function a function to map the the error to an alternative result. + * Return the type of the value returned from the handler. + */ + public ResolvableType getReturnValueType() { + return this.returnValueType; + } + + /** + * Configure an exception handler that may be used to produce an alternative + * result when result handling fails. Especially for an async return value + * errors may occur after the invocation of the handler. + * @param function the error handler * @return the current instance */ public HandlerResult setExceptionHandler(Function> function) { @@ -73,12 +88,20 @@ public class HandlerResult { return this; } + /** + * Whether there is an exception handler. + */ public boolean hasExceptionHandler() { return (this.exceptionHandler != null); } - public Mono applyExceptionHandler(Throwable ex) { - return (hasExceptionHandler() ? this.exceptionHandler.apply(ex) : Mono.error(ex)); + /** + * Apply the exception handler and return the alternative result. + * @param failure the exception + * @return the new result or the same error if there is no exception handler + */ + public Mono applyExceptionHandler(Throwable failure) { + return (hasExceptionHandler() ? this.exceptionHandler.apply(failure) : Mono.error(failure)); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java index ed3c513f8a..fc46f9d596 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java @@ -16,6 +16,8 @@ package org.springframework.web.reactive.handler; +import java.util.Optional; + import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; @@ -61,7 +63,7 @@ public class SimpleHandlerResultHandler implements Ordered, HandlerResultHandler @Override public boolean supports(HandlerResult result) { - ResolvableType type = result.getResultType(); + ResolvableType type = result.getReturnValueType(); return (type != null && Void.TYPE.equals(type.getRawClass()) || (isConvertibleToPublisher(type) && Void.class.isAssignableFrom(type.getGeneric(0).getRawClass()))); } @@ -75,12 +77,14 @@ public class SimpleHandlerResultHandler implements Ordered, HandlerResultHandler @SuppressWarnings("unchecked") @Override public Mono handleResult(ServerWebExchange exchange, HandlerResult result) { - Object value = result.getResult(); - if (Void.TYPE.equals(result.getResultType().getRawClass())) { + Optional value = result.getReturnValue(); + if (!value.isPresent() || Void.TYPE.equals(result.getReturnValueType().getRawClass())) { return Mono.empty(); } - return (value instanceof Mono ? (Mono)value : - Mono.from(this.conversionService.convert(value, Publisher.class))); + if (value.get() instanceof Mono) { + return (Mono) value.get(); + } + return Mono.from(this.conversionService.convert(value.get(), Publisher.class)); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java index da7d4a01cc..a27b5ce82f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java @@ -23,6 +23,7 @@ import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -132,23 +133,23 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered @SuppressWarnings("unchecked") public Mono handleResult(ServerWebExchange exchange, HandlerResult result) { - Object value = result.getResult(); - if (value == null) { + Optional value = result.getReturnValue(); + if (!value.isPresent()) { return Mono.empty(); } Publisher publisher; ResolvableType elementType; - ResolvableType returnType = result.getResultType(); + ResolvableType returnType = result.getReturnValueType(); if (this.conversionService.canConvert(returnType.getRawClass(), Publisher.class)) { - publisher = this.conversionService.convert(value, Publisher.class); + publisher = this.conversionService.convert(value.get(), Publisher.class); elementType = returnType.getGeneric(0); if (Void.class.equals(elementType.getRawClass())) { return (Mono)Mono.from(publisher); } } else { - publisher = Mono.just(value); + publisher = Mono.just(value.get()); elementType = returnType; } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java index 892d554e2a..25273edf2f 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java @@ -71,7 +71,7 @@ public class InvocableHandlerMethodTests { List values = Stream.from(publisher).toList().get(); assertEquals(1, values.size()); - assertEquals("success", values.get(0).getResult()); + assertEquals("success", values.get(0).getReturnValue().get()); } @Test @@ -84,7 +84,7 @@ public class InvocableHandlerMethodTests { List values = Stream.from(publisher).toList().get(); assertEquals(1, values.size()); - assertEquals("success:null", values.get(0).getResult()); + assertEquals("success:null", values.get(0).getReturnValue().get()); } @Test @@ -96,7 +96,7 @@ public class InvocableHandlerMethodTests { List values = Stream.from(publisher).toList().get(); assertEquals(1, values.size()); - assertEquals("success:value1", values.get(0).getResult()); + assertEquals("success:value1", values.get(0).getReturnValue().get()); } @Test @@ -108,7 +108,7 @@ public class InvocableHandlerMethodTests { List values = Stream.from(publisher).toList().get(); assertEquals(1, values.size()); - assertEquals("success:value1", values.get(0).getResult()); + assertEquals("success:value1", values.get(0).getReturnValue().get()); } @Test From 14997eccf3f07466d1ebf07c6bd2111a5f01d139 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 2 Feb 2016 14:30:05 -0500 Subject: [PATCH 218/498] Support Model as method argument and in HandlerResult A model is created and passed to argument resolvers including a new ModelArgumentResolver. The model is then exposed for result handling as a field in HandlerResult. --- .../web/reactive/HandlerResult.java | 17 ++++++- .../handler/HttpHandlerHandlerAdapter.java | 5 ++- .../method/HandlerMethodArgumentResolver.java | 6 ++- .../method/InvocableHandlerMethod.java | 14 +++--- .../annotation/ModelArgumentResolver.java | 44 +++++++++++++++++++ .../RequestBodyArgumentResolver.java | 5 ++- .../RequestMappingHandlerAdapter.java | 12 +++-- .../RequestParamArgumentResolver.java | 3 +- .../SimpleHandlerResultHandlerTests.java | 29 +++++++----- .../method/InvocableHandlerMethodTests.java | 27 +++++++----- .../ResponseBodyResultHandlerTests.java | 7 +-- 11 files changed, 129 insertions(+), 40 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ModelArgumentResolver.java diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java index 69b21f9986..28a691e3d5 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java @@ -22,6 +22,8 @@ import java.util.function.Function; import reactor.core.publisher.Mono; import org.springframework.core.ResolvableType; +import org.springframework.ui.ExtendedModelMap; +import org.springframework.ui.ModelMap; import org.springframework.util.Assert; /** @@ -37,6 +39,8 @@ public class HandlerResult { private final ResolvableType returnValueType; + private final ModelMap model; + private Function> exceptionHandler; @@ -45,13 +49,16 @@ public class HandlerResult { * @param handler the handler that handled the request * @param returnValue the return value from the handler possibly {@code null} * @param returnValueType the return value type + * @param model the model used for request handling */ - public HandlerResult(Object handler, Object returnValue, ResolvableType returnValueType) { + public HandlerResult(Object handler, Object returnValue, ResolvableType returnValueType, ModelMap model) { Assert.notNull(handler, "'handler' is required"); Assert.notNull(returnValueType, "'returnValueType' is required"); + Assert.notNull(model, "'model' is required"); this.handler = handler; this.returnValue = Optional.ofNullable(returnValue); this.returnValueType = returnValueType; + this.model = new ExtendedModelMap(); } @@ -76,6 +83,14 @@ public class HandlerResult { return this.returnValueType; } + /** + * Return the model used during request handling with attributes that may be + * used to render HTML templates with. + */ + public ModelMap getModel() { + return this.model; + } + /** * Configure an exception handler that may be used to produce an alternative * result when result handling fails. Especially for an async return value diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/HttpHandlerHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/HttpHandlerHandlerAdapter.java index bfe32fae98..67cc1fc2cc 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/HttpHandlerHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/HttpHandlerHandlerAdapter.java @@ -20,6 +20,8 @@ import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; import org.springframework.core.ResolvableType; +import org.springframework.ui.ExtendedModelMap; +import org.springframework.ui.ModelMap; import org.springframework.web.reactive.DispatcherHandler; import org.springframework.web.reactive.HandlerAdapter; import org.springframework.web.reactive.HandlerResult; @@ -48,7 +50,8 @@ public class HttpHandlerHandlerAdapter implements HandlerAdapter { public Mono handle(ServerWebExchange exchange, Object handler) { WebHandler webHandler = (WebHandler) handler; Mono completion = webHandler.handle(exchange); - return Mono.just(new HandlerResult(webHandler, completion, PUBLISHER_VOID)); + ModelMap model = new ExtendedModelMap(); + return Mono.just(new HandlerResult(webHandler, completion, PUBLISHER_VOID, model)); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/HandlerMethodArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/HandlerMethodArgumentResolver.java index defc46ea64..2c2e7c0c72 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/HandlerMethodArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/HandlerMethodArgumentResolver.java @@ -19,6 +19,7 @@ package org.springframework.web.reactive.method; import reactor.core.publisher.Mono; import org.springframework.core.MethodParameter; +import org.springframework.ui.ModelMap; import org.springframework.web.server.ServerWebExchange; @@ -34,7 +35,10 @@ public interface HandlerMethodArgumentResolver { * The returned {@link Mono} may produce one or zero values if the argument * does not resolve to any value, which will result in {@code null} passed * as the argument value. + * @param parameter the method parameter + * @param model the implicit model for request handling + * @param exchange the current exchange */ - Mono resolveArgument(MethodParameter parameter, ServerWebExchange exchange); + Mono resolveArgument(MethodParameter parameter, ModelMap model, ServerWebExchange exchange); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java index 47d84dfc6c..860995a962 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java @@ -32,6 +32,7 @@ import org.springframework.core.GenericTypeResolver; import org.springframework.core.MethodParameter; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.ResolvableType; +import org.springframework.ui.ModelMap; import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; import org.springframework.web.method.HandlerMethod; @@ -77,17 +78,20 @@ public class InvocableHandlerMethod extends HandlerMethod { /** * Invoke the method and return a Publisher for the return value. * @param exchange the current exchange + * @param model the model for request handling * @param providedArgs optional list of argument values to check by type * (via {@code instanceof}) for resolving method arguments. * @return Publisher that produces a single HandlerResult or an error signal; * never throws an exception */ - public Mono invokeForRequest(ServerWebExchange exchange, Object... providedArgs) { - return resolveArguments(exchange, providedArgs).then(args -> { + public Mono invokeForRequest(ServerWebExchange exchange, ModelMap model, + Object... providedArgs) { + + return resolveArguments(exchange, model, providedArgs).then(args -> { try { Object value = doInvoke(args); ResolvableType type = ResolvableType.forMethodParameter(getReturnType()); - HandlerResult handlerResult = new HandlerResult(this, value, type); + HandlerResult handlerResult = new HandlerResult(this, value, type, model); return Mono.just(handlerResult); } catch (InvocationTargetException ex) { @@ -100,7 +104,7 @@ public class InvocableHandlerMethod extends HandlerMethod { }); } - private Mono resolveArguments(ServerWebExchange exchange, Object... providedArgs) { + private Mono resolveArguments(ServerWebExchange exchange, ModelMap model, Object... providedArgs) { if (ObjectUtils.isEmpty(getMethodParameters())) { return NO_ARGS; } @@ -121,7 +125,7 @@ public class InvocableHandlerMethod extends HandlerMethod { .findFirst() .orElseThrow(() -> getArgError("No resolver for ", param, null)); try { - return resolver.resolveArgument(param, exchange) + return resolver.resolveArgument(param, model, exchange) .defaultIfEmpty(NO_VALUE) .otherwise(ex -> Mono.error(getArgError("Error resolving ", param, ex))) .log("reactor.unresolved"); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ModelArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ModelArgumentResolver.java new file mode 100644 index 0000000000..9365172f87 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ModelArgumentResolver.java @@ -0,0 +1,44 @@ +/* + * 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.web.reactive.method.annotation; + +import reactor.core.publisher.Mono; + +import org.springframework.core.MethodParameter; +import org.springframework.ui.Model; +import org.springframework.ui.ModelMap; +import org.springframework.web.reactive.method.HandlerMethodArgumentResolver; +import org.springframework.web.server.ServerWebExchange; + +/** + * Resolver for the {@link Model} controller method argument. + * + * @author Rossen Stoyanchev + */ +public class ModelArgumentResolver implements HandlerMethodArgumentResolver { + + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return Model.class.isAssignableFrom(parameter.getParameterType()); + } + + @Override + public Mono resolveArgument(MethodParameter parameter, ModelMap model, ServerWebExchange exchange) { + return Mono.just(model); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestBodyArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestBodyArgumentResolver.java index d7bbf50149..c39aa0da50 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestBodyArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestBodyArgumentResolver.java @@ -28,6 +28,7 @@ import org.springframework.core.codec.Decoder; import org.springframework.core.convert.ConversionService; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.MediaType; +import org.springframework.ui.ModelMap; import org.springframework.util.Assert; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.reactive.method.HandlerMethodArgumentResolver; @@ -58,7 +59,9 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve } @Override - public Mono resolveArgument(MethodParameter parameter, ServerWebExchange exchange) { + public Mono resolveArgument(MethodParameter parameter, ModelMap model, + ServerWebExchange exchange) { + MediaType mediaType = exchange.getRequest().getHeaders().getContentType(); if (mediaType == null) { mediaType = MediaType.APPLICATION_OCTET_STREAM; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java index 9457a395fb..5a81e4de7a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java @@ -38,6 +38,8 @@ import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.io.buffer.DataBufferAllocator; import org.springframework.core.io.buffer.NettyDataBufferAllocator; +import org.springframework.ui.ExtendedModelMap; +import org.springframework.ui.ModelMap; import org.springframework.util.ObjectUtils; import org.springframework.web.method.HandlerMethod; import org.springframework.web.method.annotation.ExceptionHandlerMethodResolver; @@ -64,7 +66,7 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Initializin new NettyDataBufferAllocator(new UnpooledByteBufAllocator(false)); private final Map, ExceptionHandlerMethodResolver> exceptionHandlerCache = - new ConcurrentHashMap, ExceptionHandlerMethodResolver>(64); + new ConcurrentHashMap<>(64); /** @@ -105,6 +107,7 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Initializin this.argumentResolvers.add(new RequestParamArgumentResolver()); this.argumentResolvers.add(new RequestBodyArgumentResolver(decoders, this.conversionService)); + this.argumentResolvers.add(new ModelArgumentResolver()); } } @@ -118,8 +121,8 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Initializin HandlerMethod handlerMethod = (HandlerMethod) handler; InvocableHandlerMethod invocable = new InvocableHandlerMethod(handlerMethod); invocable.setHandlerMethodArgumentResolvers(this.argumentResolvers); - - return invocable.invokeForRequest(exchange) + ModelMap model = new ExtendedModelMap(); + return invocable.invokeForRequest(exchange, model) .map(result -> result.setExceptionHandler(ex -> handleException(ex, handlerMethod, exchange))) .otherwise(ex -> handleException(ex, handlerMethod, exchange)); } @@ -135,7 +138,8 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Initializin logger.debug("Invoking @ExceptionHandler method: " + invocable); } invocable.setHandlerMethodArgumentResolvers(getArgumentResolvers()); - return invocable.invokeForRequest(exchange, ex); + ExtendedModelMap errorModel = new ExtendedModelMap(); + return invocable.invokeForRequest(exchange, errorModel, ex); } catch (Exception invocationEx) { if (logger.isErrorEnabled()) { diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestParamArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestParamArgumentResolver.java index 0bbc40906f..0d0dff9b2a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestParamArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestParamArgumentResolver.java @@ -19,6 +19,7 @@ package org.springframework.web.reactive.method.annotation; import reactor.core.publisher.Mono; import org.springframework.core.MethodParameter; +import org.springframework.ui.ModelMap; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.reactive.method.HandlerMethodArgumentResolver; import org.springframework.web.server.ServerWebExchange; @@ -41,7 +42,7 @@ public class RequestParamArgumentResolver implements HandlerMethodArgumentResolv @Override - public Mono resolveArgument(MethodParameter param, ServerWebExchange exchange) { + public Mono resolveArgument(MethodParameter param, ModelMap model, ServerWebExchange exchange) { RequestParam annotation = param.getParameterAnnotation(RequestParam.class); String name = (annotation.value().length() != 0 ? annotation.value() : param.getParameterName()); UriComponents uriComponents = UriComponentsBuilder.fromUri(exchange.getRequest().getURI()).build(); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandlerTests.java index 9e6381c672..907992e14d 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandlerTests.java @@ -30,6 +30,7 @@ import org.springframework.core.convert.support.GenericConversionService; import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter; import org.springframework.core.convert.support.ReactiveStreamsToReactorStreamConverter; import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter; +import org.springframework.ui.ExtendedModelMap; import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerResult; @@ -46,28 +47,28 @@ public class SimpleHandlerResultHandlerTests { HandlerMethod hm = new HandlerMethod(controller, TestController.class.getMethod("voidReturnValue")); ResolvableType type = ResolvableType.forMethodParameter(hm.getReturnType()); - assertTrue(resultHandler.supports(new HandlerResult(hm, null, type))); + assertTrue(resultHandler.supports(createHandlerResult(hm, type))); hm = new HandlerMethod(controller, TestController.class.getMethod("publisherString")); type = ResolvableType.forMethodParameter(hm.getReturnType()); - assertFalse(resultHandler.supports(new HandlerResult(hm, null, type))); + assertFalse(resultHandler.supports(createHandlerResult(hm, type))); hm = new HandlerMethod(controller, TestController.class.getMethod("publisherVoid")); type = ResolvableType.forMethodParameter(hm.getReturnType()); - assertTrue(resultHandler.supports(new HandlerResult(hm, null, type))); + assertTrue(resultHandler.supports(createHandlerResult(hm, type))); hm = new HandlerMethod(controller, TestController.class.getMethod("streamVoid")); type = ResolvableType.forMethodParameter(hm.getReturnType()); // Reactor Stream is a Publisher - assertTrue(resultHandler.supports(new HandlerResult(hm, null, type))); + assertTrue(resultHandler.supports(createHandlerResult(hm, type))); hm = new HandlerMethod(controller, TestController.class.getMethod("observableVoid")); type = ResolvableType.forMethodParameter(hm.getReturnType()); - assertFalse(resultHandler.supports(new HandlerResult(hm, null, type))); + assertFalse(resultHandler.supports(createHandlerResult(hm, type))); hm = new HandlerMethod(controller, TestController.class.getMethod("completableFutureVoid")); type = ResolvableType.forMethodParameter(hm.getReturnType()); - assertFalse(resultHandler.supports(new HandlerResult(hm, null, type))); + assertFalse(resultHandler.supports(createHandlerResult(hm, type))); } @Test @@ -82,27 +83,31 @@ public class SimpleHandlerResultHandlerTests { HandlerMethod hm = new HandlerMethod(controller, TestController.class.getMethod("voidReturnValue")); ResolvableType type = ResolvableType.forMethodParameter(hm.getReturnType()); - assertTrue(resultHandler.supports(new HandlerResult(hm, null, type))); + assertTrue(resultHandler.supports(createHandlerResult(hm, type))); hm = new HandlerMethod(controller, TestController.class.getMethod("publisherString")); type = ResolvableType.forMethodParameter(hm.getReturnType()); - assertFalse(resultHandler.supports(new HandlerResult(hm, null, type))); + assertFalse(resultHandler.supports(createHandlerResult(hm, type))); hm = new HandlerMethod(controller, TestController.class.getMethod("publisherVoid")); type = ResolvableType.forMethodParameter(hm.getReturnType()); - assertTrue(resultHandler.supports(new HandlerResult(hm, null, type))); + assertTrue(resultHandler.supports(createHandlerResult(hm, type))); hm = new HandlerMethod(controller, TestController.class.getMethod("streamVoid")); type = ResolvableType.forMethodParameter(hm.getReturnType()); - assertTrue(resultHandler.supports(new HandlerResult(hm, null, type))); + assertTrue(resultHandler.supports(createHandlerResult(hm, type))); hm = new HandlerMethod(controller, TestController.class.getMethod("observableVoid")); type = ResolvableType.forMethodParameter(hm.getReturnType()); - assertTrue(resultHandler.supports(new HandlerResult(hm, null, type))); + assertTrue(resultHandler.supports(createHandlerResult(hm, type))); hm = new HandlerMethod(controller, TestController.class.getMethod("completableFutureVoid")); type = ResolvableType.forMethodParameter(hm.getReturnType()); - assertTrue(resultHandler.supports(new HandlerResult(hm, null, type))); + assertTrue(resultHandler.supports(createHandlerResult(hm, type))); + } + + private HandlerResult createHandlerResult(HandlerMethod hm, ResolvableType type) { + return new HandlerResult(hm, null, type, new ExtendedModelMap()); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java index 25273edf2f..26e4c3ac36 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java @@ -31,6 +31,8 @@ import reactor.rx.Stream; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.ui.ExtendedModelMap; +import org.springframework.ui.ModelMap; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerResult; @@ -54,12 +56,15 @@ public class InvocableHandlerMethodTests { private ServerWebExchange exchange; + private ModelMap model; + @Before public void setUp() throws Exception { WebSessionManager sessionManager = mock(WebSessionManager.class); this.request = mock(ServerHttpRequest.class); this.exchange = new DefaultServerWebExchange(request, mock(ServerHttpResponse.class), sessionManager); + this.model = new ExtendedModelMap(); } @@ -67,7 +72,7 @@ public class InvocableHandlerMethodTests { public void noArgsMethod() throws Exception { InvocableHandlerMethod hm = createHandlerMethod("noArgs"); - Publisher publisher = hm.invokeForRequest(this.exchange); + Publisher publisher = hm.invokeForRequest(this.exchange, this.model); List values = Stream.from(publisher).toList().get(); assertEquals(1, values.size()); @@ -80,7 +85,7 @@ public class InvocableHandlerMethodTests { InvocableHandlerMethod hm = createHandlerMethod("singleArg", String.class); hm.setHandlerMethodArgumentResolvers(Collections.singletonList(new RequestParamArgumentResolver())); - Publisher publisher = hm.invokeForRequest(this.exchange); + Publisher publisher = hm.invokeForRequest(this.exchange, this.model); List values = Stream.from(publisher).toList().get(); assertEquals(1, values.size()); @@ -92,7 +97,7 @@ public class InvocableHandlerMethodTests { InvocableHandlerMethod hm = createHandlerMethod("singleArg", String.class); addResolver(hm, Mono.just("value1")); - Publisher publisher = hm.invokeForRequest(this.exchange); + Publisher publisher = hm.invokeForRequest(this.exchange, this.model); List values = Stream.from(publisher).toList().get(); assertEquals(1, values.size()); @@ -104,7 +109,7 @@ public class InvocableHandlerMethodTests { InvocableHandlerMethod hm = createHandlerMethod("singleArg", String.class); addResolver(hm, Flux.fromIterable(Arrays.asList("value1", "value2", "value3"))); - Publisher publisher = hm.invokeForRequest(this.exchange); + Publisher publisher = hm.invokeForRequest(this.exchange, this.model); List values = Stream.from(publisher).toList().get(); assertEquals(1, values.size()); @@ -115,7 +120,7 @@ public class InvocableHandlerMethodTests { public void noResolverForArg() throws Exception { InvocableHandlerMethod hm = createHandlerMethod("singleArg", String.class); - Publisher publisher = hm.invokeForRequest(this.exchange); + Publisher publisher = hm.invokeForRequest(this.exchange, this.model); Throwable ex = awaitErrorSignal(publisher); assertEquals(IllegalStateException.class, ex.getClass()); @@ -127,12 +132,12 @@ public class InvocableHandlerMethodTests { public void resolveArgumentWithThrownException() throws Exception { HandlerMethodArgumentResolver resolver = mock(HandlerMethodArgumentResolver.class); when(resolver.supportsParameter(any())).thenReturn(true); - when(resolver.resolveArgument(any(), any())).thenThrow(new IllegalStateException("boo")); + when(resolver.resolveArgument(any(), any(), any())).thenThrow(new IllegalStateException("boo")); InvocableHandlerMethod hm = createHandlerMethod("singleArg", String.class); hm.setHandlerMethodArgumentResolvers(Collections.singletonList(resolver)); - Publisher publisher = hm.invokeForRequest(this.exchange); + Publisher publisher = hm.invokeForRequest(this.exchange, this.model); Throwable ex = awaitErrorSignal(publisher); assertEquals(IllegalStateException.class, ex.getClass()); @@ -146,7 +151,7 @@ public class InvocableHandlerMethodTests { InvocableHandlerMethod hm = createHandlerMethod("singleArg", String.class); addResolver(hm, Mono.error(new IllegalStateException("boo"))); - Publisher publisher = hm.invokeForRequest(this.exchange); + Publisher publisher = hm.invokeForRequest(this.exchange, this.model); Throwable ex = awaitErrorSignal(publisher); assertEquals(IllegalStateException.class, ex.getClass()); @@ -160,7 +165,7 @@ public class InvocableHandlerMethodTests { InvocableHandlerMethod hm = createHandlerMethod("singleArg", String.class); addResolver(hm, Mono.just(1)); - Publisher publisher = hm.invokeForRequest(this.exchange); + Publisher publisher = hm.invokeForRequest(this.exchange, this.model); Throwable ex = awaitErrorSignal(publisher); assertEquals(IllegalStateException.class, ex.getClass()); @@ -173,7 +178,7 @@ public class InvocableHandlerMethodTests { public void invocationTargetExceptionIsUnwrapped() throws Exception { InvocableHandlerMethod hm = createHandlerMethod("exceptionMethod"); - Publisher publisher = hm.invokeForRequest(this.exchange); + Publisher publisher = hm.invokeForRequest(this.exchange, this.model); Throwable ex = awaitErrorSignal(publisher); assertEquals(IllegalStateException.class, ex.getClass()); @@ -190,7 +195,7 @@ public class InvocableHandlerMethodTests { private void addResolver(InvocableHandlerMethod handlerMethod, Publisher resolvedValue) { HandlerMethodArgumentResolver resolver = mock(HandlerMethodArgumentResolver.class); when(resolver.supportsParameter(any())).thenReturn(true); - when(resolver.resolveArgument(any(), any())).thenReturn(Mono.from(resolvedValue)); + when(resolver.resolveArgument(any(), any(), any())).thenReturn(Mono.from(resolvedValue)); handlerMethod.setHandlerMethodArgumentResolvers(Collections.singletonList(resolver)); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandlerTests.java index 3fd05e77c2..b0e0b09c50 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandlerTests.java @@ -25,6 +25,7 @@ import org.springframework.core.ResolvableType; import org.springframework.core.codec.support.StringEncoder; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.io.buffer.DefaultDataBufferAllocator; +import org.springframework.ui.ExtendedModelMap; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerResult; @@ -47,15 +48,15 @@ public class ResponseBodyResultHandlerTests { HandlerMethod hm = new HandlerMethod(controller,TestController.class.getMethod("notAnnotated")); ResolvableType type = ResolvableType.forMethodParameter(hm.getReturnType()); - assertFalse(handler.supports(new HandlerResult(hm, null, type))); + assertFalse(handler.supports(new HandlerResult(hm, null, type, new ExtendedModelMap()))); hm = new HandlerMethod(controller, TestController.class.getMethod("publisherString")); type = ResolvableType.forMethodParameter(hm.getReturnType()); - assertTrue(handler.supports(new HandlerResult(hm, null, type))); + assertTrue(handler.supports(new HandlerResult(hm, null, type, new ExtendedModelMap()))); hm = new HandlerMethod(controller, TestController.class.getMethod("publisherVoid")); type = ResolvableType.forMethodParameter(hm.getReturnType()); - assertTrue(handler.supports(new HandlerResult(hm, null, type))); + assertTrue(handler.supports(new HandlerResult(hm, null, type, new ExtendedModelMap()))); } From 55d37c0522d6005d14c76a520fd69511ba30cb99 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 3 Feb 2016 06:45:16 -0500 Subject: [PATCH 219/498] Add View, ViewResolver, and ViewResolverResultHandler This commit adds a View and ViewResolver contracts to support HTML template based rendering. ViewResolverResultHandler applies view resolution by iterating the resolvers to resolve to a view and then use it to render. --- .../web/reactive/HandlerResult.java | 2 +- .../springframework/web/reactive/View.java | 59 ++++ .../web/reactive/ViewResolver.java | 30 ++ .../view/ViewResolverResultHandler.java | 139 +++++++++ .../web/reactive/view/package-info.java | 4 + .../reactive/MockServerHttpResponse.java | 12 +- .../view/ViewResolverResultHandlerTests.java | 289 ++++++++++++++++++ 7 files changed, 524 insertions(+), 11 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/View.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/ViewResolver.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/view/ViewResolverResultHandler.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/view/package-info.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/view/ViewResolverResultHandlerTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java index 28a691e3d5..6f4bd8220b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java @@ -58,7 +58,7 @@ public class HandlerResult { this.handler = handler; this.returnValue = Optional.ofNullable(returnValue); this.returnValueType = returnValueType; - this.model = new ExtendedModelMap(); + this.model = model; } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/View.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/View.java new file mode 100644 index 0000000000..91fb8c3dd0 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/View.java @@ -0,0 +1,59 @@ +/* + * 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.web.reactive; + +import java.util.List; +import java.util.Optional; + +import reactor.core.publisher.Flux; + +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.http.MediaType; +import org.springframework.web.server.ServerWebExchange; + +/** + * Contract to render {@link HandlerResult} to the HTTP response. + * + *

    In contrast to an {@link org.springframework.core.codec.Encoder Encoder} + * which is a singleton and encodes any object of a given type, a {@code View} + * is typically selected by name and resolved using a {@link ViewResolver} + * which may for example match it to an HTML template. Furthermore a {@code View} + * may render based on multiple attributes contained in the model. + * + *

    A {@code View} can also choose to select an attribute from the model use + * any existing {@code Encoder} to render alternate media types. + * + * @author Rossen Stoyanchev + */ +public interface View { + + /** + * Return the list of media types this encoder supports. + */ + List getSupportedMediaTypes(); + + /** + * Render the view based on the given {@link HandlerResult}. Implementations + * can access and use the model or only a specific attribute in it. + * @param result the result from handler execution + * @param contentType the content type selected to render with which should + * match one of the {@link #getSupportedMediaTypes() supported media types}. + * @param exchange the current exchange + * @return the output stream + */ + Flux render(HandlerResult result, Optional contentType, ServerWebExchange exchange); + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/ViewResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/ViewResolver.java new file mode 100644 index 0000000000..942f050193 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/ViewResolver.java @@ -0,0 +1,30 @@ +package org.springframework.web.reactive; + +import java.util.Locale; + +import reactor.core.publisher.Mono; + +/** + * Contract to resolve a view name to a {@link View} instance. The view name may + * correspond to an HTML template or be generated dynamically. + * + *

    The process of view resolution is driven through a ViewResolver-based + * {@code HandlerResultHandler} implementation called + * {@link org.springframework.web.reactive.view.ViewResolverResultHandler + * ViewResolverResultHandler}. + * + * @author Rossen Stoyanchev + * @see org.springframework.web.reactive.view.ViewResolverResultHandler + + */ +public interface ViewResolver { + + /** + * Resolve the view name to a View instance. + * @param viewName the name of the view to resolve + * @param locale the locale for the request + * @return the resolved view or an empty stream + */ + Mono resolveViewName(String viewName, Locale locale); + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/ViewResolverResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/ViewResolverResultHandler.java new file mode 100644 index 0000000000..76c69134f1 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/ViewResolverResultHandler.java @@ -0,0 +1,139 @@ +/* + * 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.web.reactive.view; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Optional; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.util.Assert; +import org.springframework.web.reactive.HandlerResult; +import org.springframework.web.reactive.HandlerResultHandler; +import org.springframework.web.reactive.View; +import org.springframework.web.reactive.ViewResolver; +import org.springframework.web.server.ServerWebExchange; + + +/** + * {@code HandlerResultHandler} that resolves a String return value from a + * handler to a {@link View} which is then used to render the response. + * A handler may also return a {@code View} instance and/or async variants that + * provide a String view name or a {@code View}. + * + *

    This result handler should be ordered after others that may also interpret + * a String return value for example in combination with {@code @ResponseBody}. + * + * @author Rossen Stoyanchev + */ +public class ViewResolverResultHandler implements HandlerResultHandler { + + private final List viewResolvers = new ArrayList<>(4); + + private final ConversionService conversionService; + + + public ViewResolverResultHandler(List resolvers, ConversionService service) { + Assert.notEmpty(resolvers, "At least one ViewResolver is required."); + Assert.notNull(service, "'conversionService' is required."); + this.viewResolvers.addAll(resolvers); + this.conversionService = service; + } + + + /** + * Return a read-only list of view resolvers. + */ + public List getViewResolvers() { + return Collections.unmodifiableList(this.viewResolvers); + } + + + // TODO: @ModelAttribute return value, declared Object return value (either String or View) + + @Override + public boolean supports(HandlerResult result) { + Class clazz = result.getReturnValueType().getRawClass(); + if (isViewNameOrViewReference(clazz)) { + return true; + } + if (this.conversionService.canConvert(clazz, Mono.class)) { + clazz = result.getReturnValueType().getGeneric(0).getRawClass(); + return isViewNameOrViewReference(clazz); + } + return false; + } + + private boolean isViewNameOrViewReference(Class clazz) { + return (CharSequence.class.isAssignableFrom(clazz) || View.class.isAssignableFrom(clazz)); + } + + @Override + public Mono handleResult(ServerWebExchange exchange, HandlerResult result) { + + Mono returnValueMono; + if (this.conversionService.canConvert(result.getReturnValueType().getRawClass(), Mono.class)) { + returnValueMono = this.conversionService.convert(result.getReturnValue().get(), Mono.class); + } + else if (result.getReturnValue().isPresent()) { + returnValueMono = Mono.just(result.getReturnValue().get()); + } + else { + Optional viewName = getDefaultViewName(result, exchange); + if (viewName.isPresent()) { + returnValueMono = Mono.just(viewName.get()); + } + else { + returnValueMono = Mono.error(new IllegalStateException("Handler [" + result.getHandler() + "] " + + "neither returned a view name nor a View object")); + } + } + + return returnValueMono.then(returnValue -> { + if (returnValue instanceof View) { + Flux body = ((View) returnValue).render(result, Optional.empty(), exchange); + return exchange.getResponse().setBody(body); + } + else if (returnValue instanceof CharSequence) { + String viewName = returnValue.toString(); + Locale locale = Locale.getDefault(); // TODO + return Flux.fromIterable(getViewResolvers()) + .concatMap(resolver -> resolver.resolveViewName(viewName, locale)) + .next() + .then(view -> { + Flux body = view.render(result, Optional.empty(), exchange); + return exchange.getResponse().setBody(body); + }); + } + else { + // Should not happen + return Mono.error(new IllegalStateException( + "Unexpected return value: " + returnValue.getClass())); + } + }); + } + + protected Optional getDefaultViewName(HandlerResult result, ServerWebExchange exchange) { + return Optional.empty(); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/package-info.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/package-info.java new file mode 100644 index 0000000000..602c2ea992 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/package-info.java @@ -0,0 +1,4 @@ +/** + * Support for result handling through view resolution. + */ +package org.springframework.web.reactive.view; diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java index cfd7f18305..4fd1d1e2b2 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java @@ -28,7 +28,7 @@ import org.springframework.http.HttpStatus; /** * @author Rossen Stoyanchev */ -public class MockServerHttpResponse extends AbstractServerHttpResponse { +public class MockServerHttpResponse implements ServerHttpResponse { private HttpStatus status; @@ -56,19 +56,11 @@ public class MockServerHttpResponse extends AbstractServerHttpResponse { } @Override - protected Mono setBodyInternal(Publisher body) { + public Mono setBody(Publisher body) { this.body = body; return Flux.from(this.body).after(); } - @Override - protected void writeHeaders() { - } - - @Override - protected void writeCookies() { - } - @Override public void beforeCommit(Supplier> action) { } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/view/ViewResolverResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/view/ViewResolverResultHandlerTests.java new file mode 100644 index 0000000000..36a787e349 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/view/ViewResolverResultHandlerTests.java @@ -0,0 +1,289 @@ +/* + * 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.web.reactive.view; + +import java.lang.reflect.Method; +import java.net.URI; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import org.junit.Before; +import org.junit.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.test.TestSubscriber; +import rx.Single; + +import org.springframework.core.ResolvableType; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DefaultDataBufferAllocator; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.ui.ExtendedModelMap; +import org.springframework.ui.ModelMap; +import org.springframework.web.reactive.HandlerResult; +import org.springframework.web.reactive.HandlerResultHandler; +import org.springframework.web.reactive.View; +import org.springframework.web.reactive.ViewResolver; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.DefaultWebSessionManager; +import org.springframework.web.server.session.WebSessionManager; + +import static org.hamcrest.CoreMatchers.endsWith; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + + +/** + * Unit tests for {@link ViewResolverResultHandler}. + * @author Rossen Stoyanchev + */ +public class ViewResolverResultHandlerTests { + + private static final Charset UTF_8 = Charset.forName("UTF-8"); + + + private MockServerHttpResponse response; + + private ServerWebExchange exchange; + + private ModelMap model; + + private DefaultConversionService conversionService; + + + @Before + public void setUp() throws Exception { + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/path")); + this.response = new MockServerHttpResponse(); + WebSessionManager sessionManager = new DefaultWebSessionManager(); + this.exchange = new DefaultServerWebExchange(request, this.response, sessionManager); + this.model = new ExtendedModelMap().addAttribute("id", "123"); + this.conversionService = new DefaultConversionService(); + this.conversionService.addConverter(new ReactiveStreamsToRxJava1Converter()); + } + + + @Test + public void supportsWithNullReturnValue() throws Exception { + testSupports("handleString", null); + testSupports("handleView", null); + testSupports("handleMonoString", null); + testSupports("handleMonoView", null); + testSupports("handleSingleString", null); + testSupports("handleSingleView", null); + } + + private void testSupports(String methodName, Object returnValue) throws NoSuchMethodException { + Method method = TestController.class.getMethod(methodName); + ResolvableType returnType = ResolvableType.forMethodParameter(method, -1); + HandlerResult result = new HandlerResult(new Object(), returnValue, returnType, this.model); + List resolvers = Collections.singletonList(mock(ViewResolver.class)); + ViewResolverResultHandler handler = new ViewResolverResultHandler(resolvers, this.conversionService); + assertTrue(handler.supports(result)); + } + + @Test + public void viewReference() throws Exception { + TestView view = new TestView("account"); + List resolvers = Collections.singletonList(mock(ViewResolver.class)); + ViewResolverResultHandler handler = new ViewResolverResultHandler(resolvers, this.conversionService); + handle(handler, view, ResolvableType.forClass(View.class)); + + new TestSubscriber().bindTo(this.response.getBody()) + .assertValuesWith(buf -> assertEquals("account: {id=123}", asString(buf))); + } + + @Test + public void viewReferenceMono() throws Exception { + TestView view = new TestView("account"); + List resolvers = Collections.singletonList(mock(ViewResolver.class)); + ViewResolverResultHandler handler = new ViewResolverResultHandler(resolvers, this.conversionService); + handle(handler, Mono.just(view), ResolvableType.forClass(Mono.class)); + + new TestSubscriber().bindTo(this.response.getBody()) + .assertValuesWith(buf -> assertEquals("account: {id=123}", asString(buf))); + } + + @Test + public void viewName() throws Exception { + TestView view = new TestView("account"); + TestViewResolver resolver = new TestViewResolver().addView(view); + List resolvers = Collections.singletonList(resolver); + ViewResolverResultHandler handler = new ViewResolverResultHandler(resolvers, this.conversionService); + handle(handler, "account", ResolvableType.forClass(String.class)); + + TestSubscriber subscriber = new TestSubscriber<>(); + subscriber.bindTo(this.response.getBody()) + .assertValuesWith(buf -> assertEquals("account: {id=123}", asString(buf))); + } + + @Test + public void viewNameMono() throws Exception { + TestView view = new TestView("account"); + TestViewResolver resolver = new TestViewResolver().addView(view); + List resolvers = Collections.singletonList(resolver); + ViewResolverResultHandler handler = new ViewResolverResultHandler(resolvers, this.conversionService); + handle(handler, Mono.just("account"), ResolvableType.forClass(Mono.class)); + + new TestSubscriber().bindTo(this.response.getBody()) + .assertValuesWith(buf -> assertEquals("account: {id=123}", asString(buf))); + } + + @Test + public void viewNameWithMultipleResolvers() throws Exception { + TestView view1 = new TestView("account"); + TestView view2 = new TestView("profile"); + TestViewResolver resolver1 = new TestViewResolver().addView(view1); + TestViewResolver resolver2 = new TestViewResolver().addView(view2); + List resolvers = Arrays.asList(resolver1, resolver2); + ViewResolverResultHandler handler = new ViewResolverResultHandler(resolvers, this.conversionService); + handle(handler, "profile", ResolvableType.forClass(String.class)); + + new TestSubscriber().bindTo(this.response.getBody()) + .assertValuesWith(buf -> assertEquals("profile: {id=123}", asString(buf))); + } + + @Test + public void viewNameWithNoMatch() throws Exception { + List resolvers = Collections.singletonList(mock(ViewResolver.class)); + ViewResolverResultHandler handler = new ViewResolverResultHandler(resolvers, this.conversionService); + TestSubscriber subscriber = handle(handler, "account", ResolvableType.forClass(String.class)); + + subscriber.assertNoValues(); + } + + @Test + public void viewNameNotSpecified() throws Exception { + List resolvers = Collections.singletonList(mock(ViewResolver.class)); + ViewResolverResultHandler handler = new ViewResolverResultHandler(resolvers, this.conversionService); + TestSubscriber subscriber = handle(handler, null, ResolvableType.forClass(String.class)); + + subscriber.assertErrorWith(ex -> + assertThat(ex.getMessage(), endsWith("neither returned a view name nor a View object"))); + } + + private TestSubscriber handle(HandlerResultHandler handler, Object value, ResolvableType type) { + HandlerResult result = new HandlerResult(new Object(), value, type, this.model); + Mono mono = handler.handleResult(this.exchange, result); + TestSubscriber subscriber = new TestSubscriber<>(); + return subscriber.bindTo(mono).await(1, TimeUnit.SECONDS); + } + + private static DataBuffer asDataBuffer(String value) { + ByteBuffer byteBuffer = ByteBuffer.wrap(value.getBytes(UTF_8)); + return new DefaultDataBufferAllocator().wrap(byteBuffer); + } + + private static String asString(DataBuffer dataBuffer) { + ByteBuffer byteBuffer = dataBuffer.asByteBuffer(); + final byte[] bytes = new byte[byteBuffer.remaining()]; + byteBuffer.get(bytes); + return new String(bytes, UTF_8); + } + + + private static class TestViewResolver implements ViewResolver { + + private final Map views = new HashMap<>(); + + + public TestViewResolver addView(TestView view) { + this.views.put(view.getName(), view); + return this; + } + + @Override + public Mono resolveViewName(String viewName, Locale locale) { + View view = this.views.get(viewName); + return (view != null ? Mono.just(view) : Mono.empty()); + } + } + + public static final class TestView implements View { + + private final String name; + + + public TestView(String name) { + this.name = name; + } + + public String getName() { + return this.name; + } + + @Override + public List getSupportedMediaTypes() { + return null; + } + + @Override + public Flux render(HandlerResult result, Optional contentType, + ServerWebExchange exchange) { + + String value = this.name + ": " + result.getModel().toString(); + assertNotNull(value); + return Flux.just(asDataBuffer(value)); + } + } + + @SuppressWarnings("unused") + private static class TestController { + + public String handleString() { + return null; + } + + public Mono handleMonoString() { + return null; + } + + public Single handleSingleString() { + return null; + } + + public View handleView() { + return null; + } + + public Mono handleMonoView() { + return null; + } + + public Single handleSingleView() { + return null; + } + } + +} \ No newline at end of file From 29db80c3e8f1d61ef97129ff9e139b24d9de93ba Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Sun, 7 Feb 2016 17:24:42 -0500 Subject: [PATCH 220/498] Add FreeMarker View, ViewResolver and basic hierarchy --- spring-web-reactive/build.gradle | 2 + .../annotation/ResponseBodyResultHandler.java | 2 +- .../reactive/view/AbstractUrlBasedView.java | 85 +++++++ .../web/reactive/view/AbstractView.java | 166 +++++++++++++ .../reactive/view/UrlBasedViewResolver.java | 168 ++++++++++++++ .../view/ViewResolverResultHandler.java | 14 +- .../reactive/view/ViewResolverSupport.java | 116 ++++++++++ .../view/freemarker/FreeMarkerConfig.java | 39 ++++ .../view/freemarker/FreeMarkerConfigurer.java | 116 ++++++++++ .../view/freemarker/FreeMarkerView.java | 219 ++++++++++++++++++ .../freemarker/FreeMarkerViewResolver.java | 58 +++++ .../view/freemarker/FreeMarkerViewTests.java | 150 ++++++++++++ .../web/reactive/view/freemarker/test.ftl | 1 + 13 files changed, 1134 insertions(+), 2 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/view/AbstractUrlBasedView.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/view/AbstractView.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/view/UrlBasedViewResolver.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/view/ViewResolverSupport.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/view/freemarker/FreeMarkerConfig.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/view/freemarker/FreeMarkerConfigurer.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/view/freemarker/FreeMarkerView.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/view/freemarker/FreeMarkerViewResolver.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/view/freemarker/FreeMarkerViewTests.java create mode 100644 spring-web-reactive/src/test/resources/org/springframework/web/reactive/view/freemarker/test.ftl diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index 2d359c0d66..a6d4226ad6 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -84,6 +84,7 @@ dependencies { compile "io.projectreactor:reactor-core:${reactorVersion}" compile "commons-logging:commons-logging:1.2" + optional "org.springframework:spring-context-support:${springVersion}" // for FreeMarker optional 'io.reactivex:rxjava:1.1.0' optional "io.reactivex:rxnetty-http:0.5.0-SNAPSHOT" optional "com.fasterxml.jackson.core:jackson-databind:2.6.2" @@ -95,6 +96,7 @@ dependencies { optional 'io.undertow:undertow-core:1.3.5.Final' optional "org.eclipse.jetty:jetty-server:${jettyVersion}" optional "org.eclipse.jetty:jetty-servlet:${jettyVersion}" + optional("org.freemarker:freemarker:2.3.23") provided "javax.servlet:javax.servlet-api:3.1.0" diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java index a27b5ce82f..8c0b57d969 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java @@ -66,7 +66,7 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered private final Map, List> mediaTypesByEncoder; - private int order = 0; + private int order = 0; // TODO: should be MAX_VALUE public ResponseBodyResultHandler(List> encoders, ConversionService service) { diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/AbstractUrlBasedView.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/AbstractUrlBasedView.java new file mode 100644 index 0000000000..50ffac0b3c --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/AbstractUrlBasedView.java @@ -0,0 +1,85 @@ +/* + * 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.web.reactive.view; + +import java.util.Locale; + +import org.springframework.beans.factory.InitializingBean; + +/** + * Abstract base class for URL-based views. Provides a consistent way of + * holding the URL that a View wraps, in the form of a "url" bean property. + * + * @author Rossen Stoyanchev + */ +public abstract class AbstractUrlBasedView extends AbstractView implements InitializingBean { + + private String url; + + + /** + * Constructor for use as a bean. + */ + protected AbstractUrlBasedView() { + } + + /** + * Create a new AbstractUrlBasedView with the given URL. + */ + protected AbstractUrlBasedView(String url) { + this.url = url; + } + + + /** + * Set the URL of the resource that this view wraps. + * The URL must be appropriate for the concrete View implementation. + */ + public void setUrl(String url) { + this.url = url; + } + + /** + * Return the URL of the resource that this view wraps. + */ + public String getUrl() { + return this.url; + } + + + @Override + public void afterPropertiesSet() throws Exception { + if (getUrl() == null) { + throw new IllegalArgumentException("Property 'url' is required"); + } + } + + /** + * Check whether the resource for the configured URL actually exists. + * @param locale the desired Locale that we're looking for + * @return {@code false} if the resource exists + * {@code false} if we know that it does not exist + * @throws Exception if the resource exists but is invalid (e.g. could not be parsed) + */ + public abstract boolean checkResourceExists(Locale locale) throws Exception; + + + @Override + public String toString() { + return super.toString() + "; URL [" + getUrl() + "]"; + } + +} \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/AbstractView.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/AbstractView.java new file mode 100644 index 0000000000..436bb63fe7 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/AbstractView.java @@ -0,0 +1,166 @@ +/* + * 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.web.reactive.view; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import reactor.core.publisher.Flux; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferAllocator; +import org.springframework.core.io.buffer.DefaultDataBufferAllocator; +import org.springframework.http.MediaType; +import org.springframework.ui.ModelMap; +import org.springframework.util.Assert; +import org.springframework.web.reactive.HandlerResult; +import org.springframework.web.reactive.View; +import org.springframework.web.server.ServerWebExchange; + +/** + * + * @author Rossen Stoyanchev + */ +public abstract class AbstractView implements View, ApplicationContextAware { + + /** Logger that is available to subclasses */ + protected final Log logger = LogFactory.getLog(getClass()); + + + private final List mediaTypes = new ArrayList<>(4); + + private DataBufferAllocator bufferAllocator = new DefaultDataBufferAllocator(); + + private ApplicationContext applicationContext; + + + public AbstractView() { + this.mediaTypes.add(ViewResolverSupport.DEFAULT_CONTENT_TYPE); + } + + + /** + * Set the supported media types for this view. + * Default is "text/html;charset=UTF-8". + */ + public void setSupportedMediaTypes(List supportedMediaTypes) { + Assert.notEmpty(supportedMediaTypes, "'supportedMediaTypes' is required."); + this.mediaTypes.clear(); + if (supportedMediaTypes != null) { + this.mediaTypes.addAll(supportedMediaTypes); + } + } + + /** + * Return the configured media types supported by this view. + */ + @Override + public List getSupportedMediaTypes() { + return this.mediaTypes; + } + + /** + * Configure the {@link DataBufferAllocator} to use for write I/O. + *

    By default this is set to {@link DefaultDataBufferAllocator}. + * @param bufferAllocator the allocator to use + */ + public void setBufferAllocator(DataBufferAllocator bufferAllocator) { + Assert.notNull(bufferAllocator, "'bufferAllocator' is required."); + this.bufferAllocator = bufferAllocator; + } + + /** + * Return the configured buffer allocator, never {@code null}. + */ + public DataBufferAllocator getBufferAllocator() { + return this.bufferAllocator; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + public ApplicationContext getApplicationContext() { + return applicationContext; + } + + + /** + * Prepare the model to render. + * @param result the result from handler execution + * @param contentType the content type selected to render with which should + * match one of the {@link #getSupportedMediaTypes() supported media types}. + * @param exchange the current exchange + * @return + */ + @Override + public Flux render(HandlerResult result, Optional contentType, + ServerWebExchange exchange) { + + if (logger.isTraceEnabled()) { + logger.trace("Rendering view with model " + result.getModel()); + } + + if (contentType.isPresent()) { + exchange.getResponse().getHeaders().setContentType(contentType.get()); + } + + Map mergedModel = getModelAttributes(result, exchange); + return renderInternal(mergedModel, exchange); + } + + /** + * Prepare the model to use for rendering. + *

    The default implementation creates a combined output Map that includes + * model as well as static attributes with the former taking precedence. + */ + protected Map getModelAttributes(HandlerResult result, ServerWebExchange exchange) { + ModelMap model = result.getModel(); + int size = (model != null ? model.size() : 0); + + Map attributes = new LinkedHashMap<>(size); + if (model != null) { + attributes.putAll(model); + } + + return attributes; + } + + /** + * Subclasses must implement this method to actually render the view. + * @param renderAttributes combined output Map (never {@code null}), + * with dynamic values taking precedence over static attributes + * @param exchange current exchange + */ + protected abstract Flux renderInternal(Map renderAttributes, + ServerWebExchange exchange); + + + @Override + public String toString() { + return getClass().getName(); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/UrlBasedViewResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/UrlBasedViewResolver.java new file mode 100644 index 0000000000..5449493789 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/UrlBasedViewResolver.java @@ -0,0 +1,168 @@ +/* + * 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.web.reactive.view; + +import java.util.Locale; + +import reactor.core.publisher.Mono; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.web.reactive.View; + + +/** + * A {@link org.springframework.web.reactive.ViewResolver ViewResolver} that + * allow direct resolution of symbolic view names to URLs without explicit + * mapping definition. This is useful if symbolic names match the names of view + * resources in a straightforward manner (i.e. the symbolic name is the unique + * part of the resource's filename), without the need for a dedicated mapping + * to be defined for each view. + * + *

    Supports {@link AbstractUrlBasedView} subclasses like + * {@link org.springframework.web.reactive.view.freemarker.FreeMarkerView}. + * The view class for all views generated by this resolver can be specified + * via the "viewClass" property. + * + *

    View names can either be resource URLs themselves, or get augmented by a + * specified prefix and/or suffix. Exporting an attribute that holds the + * RequestContext to all views is explicitly supported. + * + *

    Example: prefix="templates/", suffix=".ftl", viewname="test" -> + * "templates/test.ftl" + * + *

    As a special feature, redirect URLs can be specified via the "redirect:" + * prefix. E.g.: "redirect:myAction" will trigger a redirect to the given + * URL, rather than resolution as standard view name. This is typically used + * for redirecting to a controller URL after finishing a form workflow. + * + *

    Note: This class does not support localized resolution, i.e. resolving + * a symbolic view name to different resources depending on the current locale. + * * @author Rossen Stoyanchev + */ +public class UrlBasedViewResolver extends ViewResolverSupport implements InitializingBean { + + private Class viewClass; + + private String prefix = ""; + + private String suffix = ""; + + + /** + * Set the view class to instantiate through {@link #createUrlBasedView(String)}. + * @param viewClass a class that is assignable to the required view class + * which by default is AbstractUrlBasedView. + */ + public void setViewClass(Class viewClass) { + if (viewClass == null || !requiredViewClass().isAssignableFrom(viewClass)) { + String name = (viewClass != null ? viewClass.getName() : null); + throw new IllegalArgumentException("Given view class [" + name + "] " + + "is not of type [" + requiredViewClass().getName() + "]"); + } + this.viewClass = viewClass; + } + + /** + * Return the view class to be used to create views. + */ + protected Class getViewClass() { + return this.viewClass; + } + + /** + * Return the required type of view for this resolver. + * This implementation returns {@link AbstractUrlBasedView}. + * @see AbstractUrlBasedView + */ + protected Class requiredViewClass() { + return AbstractUrlBasedView.class; + } + + /** + * Set the prefix that gets prepended to view names when building a URL. + */ + public void setPrefix(String prefix) { + this.prefix = (prefix != null ? prefix : ""); + } + + /** + * Return the prefix that gets prepended to view names when building a URL. + */ + protected String getPrefix() { + return this.prefix; + } + + /** + * Set the suffix that gets appended to view names when building a URL. + */ + public void setSuffix(String suffix) { + this.suffix = (suffix != null ? suffix : ""); + } + + /** + * Return the suffix that gets appended to view names when building a URL. + */ + protected String getSuffix() { + return this.suffix; + } + + + @Override + public void afterPropertiesSet() throws Exception { + if (getViewClass() == null) { + throw new IllegalArgumentException("Property 'viewClass' is required"); + } + } + + + @Override + public Mono resolveViewName(String viewName, Locale locale) { + AbstractUrlBasedView urlBasedView = createUrlBasedView(viewName); + View view = applyLifecycleMethods(viewName, urlBasedView); + try { + return (urlBasedView.checkResourceExists(locale) ? Mono.just(view) : Mono.empty()); + } + catch (Exception ex) { + return Mono.error(ex); + } + } + + /** + * Creates a new View instance of the specified view class and configures it. + * Does not perform any lookup for pre-defined View instances. + *

    Spring lifecycle methods as defined by the bean container do not have to + * be called here; those will be applied by the {@code loadView} method + * after this method returns. + *

    Subclasses will typically call {@code super.buildView(viewName)} + * first, before setting further properties themselves. {@code loadView} + * will then apply Spring lifecycle methods at the end of this process. + * @param viewName the name of the view to build + * @return the View instance + */ + protected AbstractUrlBasedView createUrlBasedView(String viewName) { + AbstractUrlBasedView view = (AbstractUrlBasedView) BeanUtils.instantiateClass(getViewClass()); + view.setSupportedMediaTypes(getSupportedMediaTypes()); + view.setBufferAllocator(getBufferAllocator()); + view.setUrl(getPrefix() + viewName + getSuffix()); + return view; + } + + private View applyLifecycleMethods(String viewName, AbstractView view) { + return (View) getApplicationContext().getAutowireCapableBeanFactory().initializeBean(view, viewName); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/ViewResolverResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/ViewResolverResultHandler.java index 76c69134f1..317e23e2a5 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/ViewResolverResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/ViewResolverResultHandler.java @@ -24,6 +24,7 @@ import java.util.Optional; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import org.springframework.core.Ordered; import org.springframework.core.convert.ConversionService; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.util.Assert; @@ -45,12 +46,14 @@ import org.springframework.web.server.ServerWebExchange; * * @author Rossen Stoyanchev */ -public class ViewResolverResultHandler implements HandlerResultHandler { +public class ViewResolverResultHandler implements HandlerResultHandler, Ordered { private final List viewResolvers = new ArrayList<>(4); private final ConversionService conversionService; + private int order = Integer.MAX_VALUE; + public ViewResolverResultHandler(List resolvers, ConversionService service) { Assert.notEmpty(resolvers, "At least one ViewResolver is required."); @@ -67,6 +70,15 @@ public class ViewResolverResultHandler implements HandlerResultHandler { return Collections.unmodifiableList(this.viewResolvers); } + public void setOrder(int order) { + this.order = order; + } + + @Override + public int getOrder() { + return this.order; + } + // TODO: @ModelAttribute return value, declared Object return value (either String or View) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/ViewResolverSupport.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/ViewResolverSupport.java new file mode 100644 index 0000000000..7bafe058ec --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/ViewResolverSupport.java @@ -0,0 +1,116 @@ +/* + * 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.web.reactive.view; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.Ordered; +import org.springframework.core.io.buffer.DataBufferAllocator; +import org.springframework.core.io.buffer.DefaultDataBufferAllocator; +import org.springframework.http.MediaType; +import org.springframework.util.Assert; +import org.springframework.web.reactive.ViewResolver; + +/** + * Base class for {@code ViewResolver} implementations with shared properties. + * + * @author Rossen Stoyanchev + * @since 4.3 + */ +public abstract class ViewResolverSupport implements ViewResolver, ApplicationContextAware, Ordered { + + public static final MediaType DEFAULT_CONTENT_TYPE = MediaType.parseMediaType("text/html;charset=UTF-8"); + + + private List mediaTypes = new ArrayList<>(4); + + private DataBufferAllocator bufferAllocator = new DefaultDataBufferAllocator(); + + private ApplicationContext applicationContext; + + private int order = Integer.MAX_VALUE; + + + public ViewResolverSupport() { + this.mediaTypes.add(DEFAULT_CONTENT_TYPE); + } + + + /** + * Set the supported media types for this view. + * Default is "text/html;charset=UTF-8". + */ + public void setSupportedMediaTypes(List supportedMediaTypes) { + Assert.notEmpty(supportedMediaTypes, "'supportedMediaTypes' is required."); + this.mediaTypes.clear(); + if (supportedMediaTypes != null) { + this.mediaTypes.addAll(supportedMediaTypes); + } + } + + /** + * Return the configured media types supported by this view. + */ + public List getSupportedMediaTypes() { + return this.mediaTypes; + } + + /** + * Configure the {@link DataBufferAllocator} to use for write I/O. + *

    By default this is set to {@link DefaultDataBufferAllocator}. + * @param bufferAllocator the allocator to use + */ + public void setBufferAllocator(DataBufferAllocator bufferAllocator) { + Assert.notNull(bufferAllocator, "'bufferAllocator' is required."); + this.bufferAllocator = bufferAllocator; + } + + /** + * Return the configured buffer allocator, never {@code null}. + */ + public DataBufferAllocator getBufferAllocator() { + return this.bufferAllocator; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + public ApplicationContext getApplicationContext() { + return this.applicationContext; + } + + /** + * Set the order in which this {@link ViewResolver} + * is evaluated. + */ + public void setOrder(int order) { + this.order = order; + } + + /** + * Return the order in which this {@link ViewResolver} is evaluated. + */ + @Override + public int getOrder() { + return this.order; + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/freemarker/FreeMarkerConfig.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/freemarker/FreeMarkerConfig.java new file mode 100644 index 0000000000..6502f3e182 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/freemarker/FreeMarkerConfig.java @@ -0,0 +1,39 @@ +/* + * 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.web.reactive.view.freemarker; + +import freemarker.template.Configuration; + +/** + * Interface to be implemented by objects that configure and manage a + * FreeMarker Configuration object in a web environment. Detected and + * used by {@link FreeMarkerView}. + * + * @author Rossen Stoyanchev + */ +public interface FreeMarkerConfig { + + /** + * Return the FreeMarker Configuration object for the current + * web application context. + *

    A FreeMarker Configuration object may be used to set FreeMarker + * properties and shared objects, and allows to retrieve templates. + * @return the FreeMarker Configuration + */ + Configuration getConfiguration(); + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/freemarker/FreeMarkerConfigurer.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/freemarker/FreeMarkerConfigurer.java new file mode 100644 index 0000000000..5dc3901ea5 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/freemarker/FreeMarkerConfigurer.java @@ -0,0 +1,116 @@ +/* + * 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.web.reactive.view.freemarker; + +import java.io.IOException; +import java.util.List; + +import freemarker.cache.ClassTemplateLoader; +import freemarker.cache.TemplateLoader; +import freemarker.template.Configuration; +import freemarker.template.TemplateException; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ResourceLoaderAware; +import org.springframework.ui.freemarker.FreeMarkerConfigurationFactory; + +/** + * Configures FreeMarker for web usage via the "configLocation" and/or + * "freemarkerSettings" and/or "templateLoaderPath" properties. + * The simplest way to use this class is to specify just a "templateLoaderPath" + * (e.g. "classpath:templates"); you do not need any further configuration then. + * + *

    This bean must be included in the application context of any application + * using {@link FreeMarkerView}. It exists purely to configure FreeMarker. + * It is not meant to be referenced by application components but just internally + * by {@code FreeMarkerView}. Implements {@link FreeMarkerConfig} to be found by + * {@code FreeMarkerView} without depending on the bean name the configurer. + * + *

    Note that you can also refer to a pre-configured FreeMarker Configuration + * instance via the "configuration" property. This allows to share a FreeMarker + * Configuration for web and email usage for example. + * + *

    TODO: macros + * + *

    This configurer registers a template loader for this package, allowing to + * reference the "spring.ftl" macro library contained in this package: + * + *

    + * <#import "/spring.ftl" as spring/>
    + * <@spring.bind "person.age"/>
    + * age is ${spring.status.value}
    + * + * Note: Spring's FreeMarker support requires FreeMarker 2.3 or higher. + * + * @author Rossen Stoyanchev + */ +public class FreeMarkerConfigurer extends FreeMarkerConfigurationFactory + implements FreeMarkerConfig, InitializingBean, ResourceLoaderAware { + + private Configuration configuration; + + + public FreeMarkerConfigurer() { + setDefaultEncoding("UTF-8"); + } + + + /** + * Set a pre-configured Configuration to use for the FreeMarker web config, + * e.g. a shared one for web and email usage. If this is not set, + * FreeMarkerConfigurationFactory's properties (inherited by this class) + * have to be specified. + */ + public void setConfiguration(Configuration configuration) { + this.configuration = configuration; + } + + + /** + * Initialize FreeMarkerConfigurationFactory's Configuration + * if not overridden by a pre-configured FreeMarker Configuation. + *

    Sets up a ClassTemplateLoader to use for loading Spring macros. + * @see #createConfiguration + * @see #setConfiguration + */ + @Override + public void afterPropertiesSet() throws IOException, TemplateException { + if (this.configuration == null) { + this.configuration = createConfiguration(); + } + } + + /** + * This implementation registers an additional ClassTemplateLoader + * for the Spring-provided macros, added to the end of the list. + */ + @Override + protected void postProcessTemplateLoaders(List templateLoaders) { + templateLoaders.add(new ClassTemplateLoader(FreeMarkerConfigurer.class, "")); + logger.info("ClassTemplateLoader for Spring macros added to FreeMarker configuration"); + } + + + /** + * Return the Configuration object wrapped by this bean. + */ + @Override + public Configuration getConfiguration() { + return this.configuration; + } + +} \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/freemarker/FreeMarkerView.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/freemarker/FreeMarkerView.java new file mode 100644 index 0000000000..9efd05265b --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/freemarker/FreeMarkerView.java @@ -0,0 +1,219 @@ +/* + * 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.web.reactive.view.freemarker; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.util.Locale; +import java.util.Map; + +import freemarker.core.ParseException; +import freemarker.template.Configuration; +import freemarker.template.DefaultObjectWrapperBuilder; +import freemarker.template.ObjectWrapper; +import freemarker.template.SimpleHash; +import freemarker.template.Template; +import freemarker.template.Version; +import reactor.core.publisher.Flux; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.context.ApplicationContextException; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.web.reactive.view.AbstractUrlBasedView; +import org.springframework.web.server.ServerWebExchange; + +/** + * A {@code View} implementation that uses the FreeMarker template engine. + * + *

    Depends on a single {@link FreeMarkerConfig} object such as + * {@link FreeMarkerConfigurer} being accessible in the application context. + * Alternatively set the FreeMarker configuration can be set directly on this + * class via {@link #setConfiguration}. + * + *

    The {@link #setUrl(String) url} property is the location of the FreeMarker + * template relative to the FreeMarkerConfigurer's + * {@link FreeMarkerConfigurer#setTemplateLoaderPath templateLoaderPath}. + * + *

    Note: Spring's FreeMarker support requires FreeMarker 2.3 or higher. + * + * @author Rossen Stoyanchev + */ +public class FreeMarkerView extends AbstractUrlBasedView { + + private Configuration configuration; + + private String encoding; + + + /** + * Set the FreeMarker Configuration to be used by this view. + *

    Typically this property is not set directly. Instead a single + * {@link FreeMarkerConfig} is expected in the Spring application context + * which is used to obtain the FreeMarker configuration. + */ + public void setConfiguration(Configuration configuration) { + this.configuration = configuration; + } + + /** + * Return the FreeMarker configuration used by this view. + */ + protected Configuration getConfiguration() { + return this.configuration; + } + + /** + * Set the encoding of the FreeMarker template file. + *

    By default {@link FreeMarkerConfigurer} sets the default encoding in + * the FreeMarker configuration to "UTF-8". It's recommended to specify the + * encoding in the FreeMarker Configuration rather than per template if all + * your templates share a common encoding. + */ + public void setEncoding(String encoding) { + this.encoding = encoding; + } + + /** + * Return the encoding for the FreeMarker template. + */ + protected String getEncoding() { + return this.encoding; + } + + + @Override + public void afterPropertiesSet() throws Exception { + super.afterPropertiesSet(); + if (getConfiguration() == null) { + FreeMarkerConfig config = autodetectConfiguration(); + setConfiguration(config.getConfiguration()); + } + } + + /** + * Autodetect a {@link FreeMarkerConfig} object via the ApplicationContext. + * @return the Configuration instance to use for FreeMarkerViews + * @throws BeansException if no Configuration instance could be found + * @see #setConfiguration + */ + protected FreeMarkerConfig autodetectConfiguration() throws BeansException { + try { + return BeanFactoryUtils.beanOfTypeIncludingAncestors( + getApplicationContext(), FreeMarkerConfig.class, true, false); + } + catch (NoSuchBeanDefinitionException ex) { + throw new ApplicationContextException( + "Must define a single FreeMarkerConfig bean in this web application context " + + "(may be inherited): FreeMarkerConfigurer is the usual implementation. " + + "This bean may be given any name.", ex); + } + } + + + /** + * Check that the FreeMarker template used for this view exists and is valid. + *

    Can be overridden to customize the behavior, for example in case of + * multiple templates to be rendered into a single view. + */ + @Override + public boolean checkResourceExists(Locale locale) throws Exception { + try { + // Check that we can get the template, even if we might subsequently get it again. + getTemplate(locale); + return true; + } + catch (FileNotFoundException ex) { + if (logger.isDebugEnabled()) { + logger.debug("No FreeMarker view found for URL: " + getUrl()); + } + return false; + } + catch (ParseException ex) { + throw new ApplicationContextException( + "Failed to parse FreeMarker template for URL [" + getUrl() + "]", ex); + } + catch (IOException ex) { + throw new ApplicationContextException( + "Could not load FreeMarker template for URL [" + getUrl() + "]", ex); + } + } + + @Override + protected Flux renderInternal(Map renderAttributes, ServerWebExchange exchange) { + // Expose all standard FreeMarker hash models. + SimpleHash freeMarkerModel = getTemplateModel(renderAttributes, exchange); + if (logger.isDebugEnabled()) { + logger.debug("Rendering FreeMarker template [" + getUrl() + "]."); + } + Locale locale = Locale.getDefault(); // TODO + DataBuffer dataBuffer = getBufferAllocator().allocateBuffer(); + try { + Writer writer = new OutputStreamWriter(dataBuffer.asOutputStream()); + getTemplate(locale).process(freeMarkerModel, writer); + } + catch (IOException ex) { + String message = "Could not load FreeMarker template for URL [" + getUrl() + "]"; + return Flux.error(new IllegalStateException(message, ex)); + } + catch (Throwable ex) { + return Flux.error(ex); + } + return Flux.just(dataBuffer); + } + + /** + * Build a FreeMarker template model for the given model Map. + *

    The default implementation builds a {@link SimpleHash}. + * @param model the model to use for rendering + * @param exchange current exchange + * @return the FreeMarker template model, as a {@link SimpleHash} or subclass thereof + */ + protected SimpleHash getTemplateModel(Map model, ServerWebExchange exchange) { + SimpleHash fmModel = new SimpleHash(getObjectWrapper()); + fmModel.putAll(model); + return fmModel; + } + + /** + * Return the configured FreeMarker {@link ObjectWrapper}, or the + * {@link ObjectWrapper#DEFAULT_WRAPPER default wrapper} if none specified. + * @see freemarker.template.Configuration#getObjectWrapper() + */ + protected ObjectWrapper getObjectWrapper() { + ObjectWrapper ow = getConfiguration().getObjectWrapper(); + Version version = Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS; + return (ow != null ? ow : new DefaultObjectWrapperBuilder(version).build()); + } + + /** + * Retrieve the FreeMarker template for the given locale, + * to be rendering by this view. + *

    By default, the template specified by the "url" bean property + * will be retrieved. + * @param locale the current locale + * @return the FreeMarker template to render + */ + protected Template getTemplate(Locale locale) throws IOException { + return (getEncoding() != null ? + getConfiguration().getTemplate(getUrl(), locale, getEncoding()) : + getConfiguration().getTemplate(getUrl(), locale)); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/freemarker/FreeMarkerViewResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/freemarker/FreeMarkerViewResolver.java new file mode 100644 index 0000000000..631d6fbcd7 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/freemarker/FreeMarkerViewResolver.java @@ -0,0 +1,58 @@ +/* + * 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.web.reactive.view.freemarker; + +import org.springframework.web.reactive.view.UrlBasedViewResolver; + +/** + * A {@code ViewResolver} for resolving {@link FreeMarkerView} instances, i.e. + * FreeMarker templates and custom subclasses of it. + * + *

    The view class for all views generated by this resolver can be specified + * via the "viewClass" property. See {@link UrlBasedViewResolver} for details. + * + * @author Rossen Stoyanchev + */public class FreeMarkerViewResolver extends UrlBasedViewResolver { + + + /** + * Simple constructor. + */ + public FreeMarkerViewResolver() { + setViewClass(requiredViewClass()); + } + + /** + * Convenience constructor with a prefix and suffix. + * @param suffix the suffix to prepend view names with + * @param prefix the prefix to prepend view names with + */ + public FreeMarkerViewResolver(String prefix, String suffix) { + setViewClass(requiredViewClass()); + setPrefix(prefix); + setSuffix(suffix); + } + + + /** + * Requires {@link FreeMarkerView}. + */ + @Override + protected Class requiredViewClass() { + return FreeMarkerView.class; + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/view/freemarker/FreeMarkerViewTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/view/freemarker/FreeMarkerViewTests.java new file mode 100644 index 0000000000..7123553d47 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/view/freemarker/FreeMarkerViewTests.java @@ -0,0 +1,150 @@ +/* + * 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.web.reactive.view.freemarker; + +import java.net.URI; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.Locale; +import java.util.Optional; + +import freemarker.template.Configuration; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import reactor.core.publisher.Flux; +import reactor.core.test.TestSubscriber; + +import org.springframework.context.ApplicationContextException; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.ResolvableType; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.http.HttpMethod; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.ui.ExtendedModelMap; +import org.springframework.ui.ModelMap; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.reactive.HandlerResult; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.DefaultWebSessionManager; +import org.springframework.web.server.session.WebSessionManager; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * @author Rossen Stoyanchev + */ +public class FreeMarkerViewTests { + + public static final String TEMPLATE_PATH = "classpath*:org/springframework/web/reactive/view/freemarker/"; + + private static final Charset UTF_8 = Charset.forName("UTF-8"); + + + private ServerWebExchange exchange; + + private GenericApplicationContext context; + + private Configuration freeMarkerConfig; + + @Rule + public final ExpectedException exception = ExpectedException.none(); + + + @Before + public void setUp() throws Exception { + this.context = new GenericApplicationContext(); + this.context.refresh(); + + FreeMarkerConfigurer configurer = new FreeMarkerConfigurer(); + configurer.setPreferFileSystemAccess(false); + configurer.setTemplateLoaderPath(TEMPLATE_PATH); + configurer.setResourceLoader(this.context); + this.freeMarkerConfig = configurer.createConfiguration(); + + FreeMarkerView fv = new FreeMarkerView(); + fv.setApplicationContext(this.context); + + MockServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/path")); + MockServerHttpResponse response = new MockServerHttpResponse(); + WebSessionManager manager = new DefaultWebSessionManager(); + this.exchange = new DefaultServerWebExchange(request, response, manager); + } + + + @Test + public void noFreeMarkerConfig() throws Exception { + this.exception.expect(ApplicationContextException.class); + this.exception.expectMessage("Must define a single FreeMarkerConfig bean"); + + FreeMarkerView view = new FreeMarkerView(); + view.setApplicationContext(this.context); + view.setUrl("anythingButNull"); + view.afterPropertiesSet(); + } + + @Test + public void noTemplateName() throws Exception { + this.exception.expect(IllegalArgumentException.class); + this.exception.expectMessage("Property 'url' is required"); + + FreeMarkerView freeMarkerView = new FreeMarkerView(); + freeMarkerView.afterPropertiesSet(); + } + + @Test + public void checkResourceExists() throws Exception { + FreeMarkerView view = new FreeMarkerView(); + view.setConfiguration(this.freeMarkerConfig); + view.setUrl("test.ftl"); + + assertTrue(view.checkResourceExists(Locale.US)); + } + + @Test + public void render() throws Exception { + FreeMarkerView view = new FreeMarkerView(); + view.setConfiguration(this.freeMarkerConfig); + view.setUrl("test.ftl"); + + ModelMap model = new ExtendedModelMap(); + model.addAttribute("hello", "hi FreeMarker"); + HandlerResult result = new HandlerResult(new Object(), "", ResolvableType.NONE, model); + Flux flux = view.render(result, Optional.empty(), this.exchange); + + TestSubscriber subscriber = new TestSubscriber<>(); + subscriber.bindTo(flux).assertValuesWith(dataBuffer -> + assertEquals("hi FreeMarker", asString(dataBuffer))); + } + + + + private static String asString(DataBuffer dataBuffer) { + ByteBuffer byteBuffer = dataBuffer.asByteBuffer(); + final byte[] bytes = new byte[byteBuffer.remaining()]; + byteBuffer.get(bytes); + return new String(bytes, UTF_8); + } + +} diff --git a/spring-web-reactive/src/test/resources/org/springframework/web/reactive/view/freemarker/test.ftl b/spring-web-reactive/src/test/resources/org/springframework/web/reactive/view/freemarker/test.ftl new file mode 100644 index 0000000000..f9ad1fdc6e --- /dev/null +++ b/spring-web-reactive/src/test/resources/org/springframework/web/reactive/view/freemarker/test.ftl @@ -0,0 +1 @@ +${hello} \ No newline at end of file From 971ca6beb8be8a12cc3e9197f5b46d78ad89f3c6 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 8 Feb 2016 17:30:44 -0500 Subject: [PATCH 221/498] Add HTML rendering integration test --- .../RequestMappingIntegrationTests.java | 82 +++++++++++++++++-- 1 file changed, 74 insertions(+), 8 deletions(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java index 2ec5b58d94..7c84c6e6eb 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java @@ -20,6 +20,7 @@ import java.net.URI; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.concurrent.CompletableFuture; @@ -38,6 +39,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ResolvableType; +import org.springframework.core.codec.Encoder; import org.springframework.core.codec.support.ByteBufferEncoder; import org.springframework.core.codec.support.JacksonJsonEncoder; import org.springframework.core.codec.support.JsonObjectEncoder; @@ -56,6 +58,8 @@ import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; import org.springframework.http.server.reactive.AbstractHttpHandlerIntegrationTests; import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -63,7 +67,11 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; import org.springframework.web.reactive.DispatcherHandler; +import org.springframework.web.reactive.ViewResolver; import org.springframework.web.reactive.handler.SimpleHandlerResultHandler; +import org.springframework.web.reactive.view.ViewResolverResultHandler; +import org.springframework.web.reactive.view.freemarker.FreeMarkerConfigurer; +import org.springframework.web.reactive.view.freemarker.FreeMarkerViewResolver; import org.springframework.web.server.adapter.WebHttpHandlerBuilder; import static org.junit.Assert.assertArrayEquals; @@ -291,6 +299,18 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati create("http://localhost:" + this.port + "/observable-create"); } + @Test + public void html() throws Exception { + + RestTemplate restTemplate = new RestTemplate(); + + URI url = new URI("http://localhost:" + port + "/html?name=Jason"); + RequestEntity request = RequestEntity.get(url).accept(MediaType.TEXT_HTML).build(); + ResponseEntity response = restTemplate.exchange(request, String.class); + + assertEquals("Hello: Jason!", response.getBody()); + } + private void serializeAsPojo(String requestUrl) throws Exception { RestTemplate restTemplate = new RestTemplate(); @@ -350,7 +370,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati ResponseEntity response = restTemplate.exchange(request, Void.class); assertEquals(HttpStatus.OK, response.getStatusCode()); - assertEquals(2, this.wac.getBean(TestController.class).persons.size()); + assertEquals(2, this.wac.getBean(TestRestController.class).persons.size()); } @@ -358,6 +378,9 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @SuppressWarnings("unused") static class FrameworkConfig { + private DataBufferAllocator allocator = new DefaultDataBufferAllocator(); + + @Bean public RequestMappingHandlerMapping handlerMapping() { return new RequestMappingHandlerMapping(); @@ -382,16 +405,42 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @Bean public ResponseBodyResultHandler responseBodyResultHandler() { - DataBufferAllocator allocator = new DefaultDataBufferAllocator(); - return new ResponseBodyResultHandler(Arrays.asList( - new ByteBufferEncoder(allocator), new StringEncoder(allocator), - new JacksonJsonEncoder(allocator, new JsonObjectEncoder(allocator))), - conversionService()); + List> encoders = Arrays.asList( + new ByteBufferEncoder(this.allocator), new StringEncoder(this.allocator), + new JacksonJsonEncoder(this.allocator, new JsonObjectEncoder(this.allocator))); + ResponseBodyResultHandler resultHandler = new ResponseBodyResultHandler(encoders, conversionService()); + resultHandler.setOrder(1); + return resultHandler; } @Bean public SimpleHandlerResultHandler simpleHandlerResultHandler() { - return new SimpleHandlerResultHandler(conversionService()); + SimpleHandlerResultHandler resultHandler = new SimpleHandlerResultHandler(conversionService()); + resultHandler.setOrder(2); + return resultHandler; + } + + @Bean + public ViewResolverResultHandler viewResolverResultHandler() { + List resolvers = Collections.singletonList(freeMarkerViewResolver()); + ViewResolverResultHandler resultHandler = new ViewResolverResultHandler(resolvers, conversionService()); + resultHandler.setOrder(3); + return resultHandler; + } + + @Bean + public ViewResolver freeMarkerViewResolver() { + FreeMarkerViewResolver viewResolver = new FreeMarkerViewResolver("", ".ftl"); + viewResolver.setBufferAllocator(this.allocator); + return viewResolver; + } + + @Bean + public FreeMarkerConfigurer freeMarkerConfig() { + FreeMarkerConfigurer configurer = new FreeMarkerConfigurer(); + configurer.setPreferFileSystemAccess(false); + configurer.setTemplateLoaderPath("classpath*:org/springframework/web/reactive/view/freemarker/"); + return configurer; } } @@ -400,6 +449,11 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @SuppressWarnings("unused") static class ApplicationConfig { + @Bean + public TestRestController testRestController() { + return new TestRestController(); + } + @Bean public TestController testController() { return new TestController(); @@ -409,7 +463,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @RestController @SuppressWarnings("unused") - private static class TestController { + private static class TestRestController { final List persons = new ArrayList<>(); @@ -601,6 +655,18 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati } + @Controller + @SuppressWarnings("unused") + private static class TestController { + + @RequestMapping("/html") + public String getHtmlPage(@RequestParam String name, Model model) { + model.addAttribute("hello", "Hello: " + name + "!"); + return "test"; + } + + } + private static class Person { private String name; From 36788207f398c0c4219a085eb5ab0098115f43e1 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Tue, 9 Feb 2016 17:50:28 +0100 Subject: [PATCH 222/498] Polish RequestMappingIntegrationTests --- .../RequestMappingIntegrationTests.java | 46 +++++-------------- 1 file changed, 11 insertions(+), 35 deletions(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java index 7c84c6e6eb..07547e21a2 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java @@ -546,74 +546,50 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @RequestMapping("/publisher-capitalize") public Publisher publisherCapitalize(@RequestBody Publisher persons) { - return Stream.from(persons).map(person -> { - person.setName(person.getName().toUpperCase()); - return person; - }); + return Stream + .from(persons) + .map(person -> new Person(person.getName().toUpperCase())); } @RequestMapping("/flux-capitalize") public Flux fluxCapitalize(@RequestBody Flux persons) { - return persons.map(person -> { - person.setName(person.getName().toUpperCase()); - return person; - }); + return persons.map(person -> new Person(person.getName().toUpperCase())); } @RequestMapping("/observable-capitalize") public Observable observableCapitalize(@RequestBody Observable persons) { - return persons.map(person -> { - person.setName(person.getName().toUpperCase()); - return person; - }); + return persons.map(person -> new Person(person.getName().toUpperCase())); } @RequestMapping("/stream-capitalize") public Stream streamCapitalize(@RequestBody Stream persons) { - return persons.map(person -> { - person.setName(person.getName().toUpperCase()); - return person; - }); + return persons.map(person -> new Person(person.getName().toUpperCase())); } @RequestMapping("/person-capitalize") public Person personCapitalize(@RequestBody Person person) { - person.setName(person.getName().toUpperCase()); - return person; + return new Person(person.getName().toUpperCase()); } @RequestMapping("/completable-future-capitalize") public CompletableFuture completableFutureCapitalize( @RequestBody CompletableFuture personFuture) { - - return personFuture.thenApply(person -> { - person.setName(person.getName().toUpperCase()); - return person; - }); + return personFuture.thenApply(person -> new Person(person.getName().toUpperCase())); } @RequestMapping("/mono-capitalize") public Mono monoCapitalize(@RequestBody Mono personFuture) { - return personFuture.map(person -> { - person.setName(person.getName().toUpperCase()); - return person; - }); + return personFuture.map(person -> new Person(person.getName().toUpperCase())); } @RequestMapping("/single-capitalize") public Single singleCapitalize(@RequestBody Single personFuture) { - return personFuture.map(person -> { - person.setName(person.getName().toUpperCase()); - return person; - }); + return personFuture.map(person -> new Person(person.getName().toUpperCase())); } @RequestMapping("/promise-capitalize") public Promise promiseCapitalize(@RequestBody Promise personFuture) { - return Stream.from(personFuture.map(person -> { - person.setName(person.getName().toUpperCase()); - return person; - })).promise(); + return Stream.from(personFuture.map(person -> new Person(person.getName().toUpperCase()))).promise(); } @RequestMapping("/publisher-create") From 628ae2fda8b3106f2f4df8436e0c5f56af4cd24d Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Tue, 9 Feb 2016 17:58:12 +0100 Subject: [PATCH 223/498] Use Flux instead of Stream in Publisher based integration tests --- .../method/annotation/RequestMappingIntegrationTests.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java index 07547e21a2..aeeaa94f23 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java @@ -469,7 +469,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @RequestMapping("/param") public Publisher handleWithParam(@RequestParam String name) { - return Stream.just("Hello ", name, "!"); + return Flux.just("Hello ", name, "!"); } @RequestMapping("/person") @@ -485,7 +485,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @RequestMapping("/raw") public Publisher rawResponseBody() { JacksonJsonEncoder encoder = new JacksonJsonEncoder(new DefaultDataBufferAllocator()); - return encoder.encode(Stream.just(new Person("Robert")), + return encoder.encode(Mono.just(new Person("Robert")), ResolvableType.forClass(Person.class), MediaType.APPLICATION_JSON).map(DataBuffer::asByteBuffer); } @@ -526,7 +526,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @RequestMapping("/publisher") public Publisher publisherResponseBody() { - return Stream.just(new Person("Robert"), new Person("Marie")); + return Flux.just(new Person("Robert"), new Person("Marie")); } @RequestMapping("/flux") @@ -546,7 +546,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @RequestMapping("/publisher-capitalize") public Publisher publisherCapitalize(@RequestBody Publisher persons) { - return Stream + return Flux .from(persons) .map(person -> new Person(person.getName().toUpperCase())); } From d68ff29e9b1b991ea1b7d64119603f524c9084f6 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Wed, 10 Feb 2016 23:16:48 +0100 Subject: [PATCH 224/498] Use default buffer allocator in RequestMappingHandlerAdapter This commit removes RequestMappingHandlerAdapter's hard dependency on Netty by switching to the DefaultDataBufferAllocator. --- .../method/annotation/RequestMappingHandlerAdapter.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java index 5a81e4de7a..bd19cc446e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java @@ -23,7 +23,6 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import io.netty.buffer.UnpooledByteBufAllocator; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import reactor.core.publisher.Mono; @@ -37,7 +36,7 @@ import org.springframework.core.codec.support.StringDecoder; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.io.buffer.DataBufferAllocator; -import org.springframework.core.io.buffer.NettyDataBufferAllocator; +import org.springframework.core.io.buffer.DefaultDataBufferAllocator; import org.springframework.ui.ExtendedModelMap; import org.springframework.ui.ModelMap; import org.springframework.util.ObjectUtils; @@ -62,8 +61,7 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Initializin private ConversionService conversionService = new DefaultConversionService(); - private DataBufferAllocator allocator = - new NettyDataBufferAllocator(new UnpooledByteBufAllocator(false)); + private DataBufferAllocator allocator = new DefaultDataBufferAllocator(); private final Map, ExceptionHandlerMethodResolver> exceptionHandlerCache = new ConcurrentHashMap<>(64); From f036f745a64a9c0143a2f8cd1ff4a9f40edff281 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Thu, 18 Feb 2016 15:49:45 +0100 Subject: [PATCH 225/498] Improved RxNetty support. --- .../reactive/RxNettyHttpHandlerAdapter.java | 10 ++++---- .../reactive/RxNettyServerHttpResponse.java | 24 ++++++++++++------- .../reactive/boot/RxNettyHttpServer.java | 15 +----------- 3 files changed, 20 insertions(+), 29 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyHttpHandlerAdapter.java index 6ed6f3e641..6023d34ff7 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyHttpHandlerAdapter.java @@ -34,18 +34,16 @@ public class RxNettyHttpHandlerAdapter implements RequestHandler handle(HttpServerRequest request, HttpServerResponse response) { + NettyDataBufferAllocator allocator = + new NettyDataBufferAllocator(response.unsafeNettyChannel().alloc()); + RxNettyServerHttpRequest adaptedRequest = new RxNettyServerHttpRequest(request, allocator); RxNettyServerHttpResponse adaptedResponse = new RxNettyServerHttpResponse(response); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java index e1ad9ad646..ca064ad2b5 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java @@ -16,6 +16,8 @@ package org.springframework.http.server.reactive; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.cookie.Cookie; import io.netty.handler.codec.http.cookie.DefaultCookie; @@ -26,6 +28,7 @@ import reactor.core.publisher.Mono; import rx.Observable; import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.NettyDataBuffer; import org.springframework.http.HttpCookie; import org.springframework.http.HttpStatus; import org.springframework.util.Assert; @@ -38,10 +41,9 @@ import org.springframework.util.Assert; */ public class RxNettyServerHttpResponse extends AbstractServerHttpResponse { - private final HttpServerResponse response; + private final HttpServerResponse response; - - public RxNettyServerHttpResponse(HttpServerResponse response) { + public RxNettyServerHttpResponse(HttpServerResponse response) { Assert.notNull("'response', response must not be null."); this.response = response; } @@ -58,15 +60,19 @@ public class RxNettyServerHttpResponse extends AbstractServerHttpResponse { @Override protected Mono setBodyInternal(Publisher publisher) { - Observable content = RxJava1ObservableConverter.from(publisher).map(this::toBytes); - Observable completion = this.response.writeBytes(content); + Observable content = + RxJava1ObservableConverter.from(publisher).map(this::toByteBuf); + Observable completion = this.response.write(content); return RxJava1ObservableConverter.from(completion).after(); } - private byte[] toBytes(DataBuffer buffer) { - byte[] bytes = new byte[buffer.readableByteCount()]; - buffer.read(bytes); - return bytes; + private ByteBuf toByteBuf(DataBuffer buffer) { + if (buffer instanceof NettyDataBuffer) { + return ((NettyDataBuffer) buffer).getNativeBuffer(); + } + else { + return Unpooled.wrappedBuffer(buffer.asByteBuffer()); + } } @Override diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/RxNettyHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/RxNettyHttpServer.java index bb23123281..d73fe0385f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/RxNettyHttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/RxNettyHttpServer.java @@ -17,10 +17,7 @@ package org.springframework.http.server.reactive.boot; import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.UnpooledByteBufAllocator; -import org.springframework.core.io.buffer.NettyDataBufferAllocator; import org.springframework.http.server.reactive.RxNettyHttpHandlerAdapter; import org.springframework.util.Assert; @@ -34,22 +31,12 @@ public class RxNettyHttpServer extends HttpServerSupport implements HttpServer { private io.reactivex.netty.protocol.http.server.HttpServer rxNettyServer; - private NettyDataBufferAllocator allocator; - private boolean running; - public void setAllocator(ByteBufAllocator allocator) { - Assert.notNull(allocator, "'allocator' must not be null"); - this.allocator = new NettyDataBufferAllocator(allocator); - } - @Override public void afterPropertiesSet() throws Exception { Assert.notNull(getHttpHandler()); - if (allocator == null) { - allocator = new NettyDataBufferAllocator(UnpooledByteBufAllocator.DEFAULT); - } - this.rxNettyHandler = new RxNettyHttpHandlerAdapter(getHttpHandler(), allocator); + this.rxNettyHandler = new RxNettyHttpHandlerAdapter(getHttpHandler()); this.rxNettyServer = (getPort() != -1 ? io.reactivex.netty.protocol.http.server.HttpServer.newServer(getPort()) : From 51a90465d40168ed27e9ea93220b7e6f35bc203b Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Tue, 23 Feb 2016 10:24:18 +0100 Subject: [PATCH 226/498] Polishing --- .../reactive/ServletHttpHandlerAdapter.java | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java index 213164902a..048fa38014 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java @@ -17,6 +17,7 @@ package org.springframework.http.server.reactive; import java.io.IOException; +import java.io.InputStream; import java.util.concurrent.atomic.AtomicLong; import javax.servlet.AsyncContext; import javax.servlet.ReadListener; @@ -41,6 +42,7 @@ import org.springframework.core.io.buffer.DataBufferAllocator; import org.springframework.core.io.buffer.DefaultDataBufferAllocator; import org.springframework.http.HttpStatus; import org.springframework.util.Assert; +import org.springframework.util.StreamUtils; /** * @author Arjen Poutsma @@ -80,7 +82,7 @@ public class ServletHttpHandlerAdapter extends HttpServlet { servletRequest.getInputStream().setReadListener(requestBody); ResponseBodySubscriber responseBodySubscriber = - new ResponseBodySubscriber(synchronizer, allocator); + new ResponseBodySubscriber(synchronizer); ServletServerHttpResponse response = new ServletServerHttpResponse(servletResponse, publisher -> Mono.from(subscriber -> publisher.subscribe(responseBodySubscriber))); servletResponse.getOutputStream().setWriteListener(responseBodySubscriber); @@ -280,18 +282,14 @@ public class ServletHttpHandlerAdapter extends HttpServlet { private final ServletAsyncContextSynchronizer synchronizer; - private final DataBufferAllocator allocator; - private Subscription subscription; - private DataBuffer buffer; + private DataBuffer dataBuffer; private volatile boolean subscriberComplete = false; - public ResponseBodySubscriber(ServletAsyncContextSynchronizer synchronizer, - DataBufferAllocator allocator) { + public ResponseBodySubscriber(ServletAsyncContextSynchronizer synchronizer) { this.synchronizer = synchronizer; - this.allocator = allocator; } @@ -303,9 +301,9 @@ public class ServletHttpHandlerAdapter extends HttpServlet { @Override public void onNext(DataBuffer bytes) { - Assert.isNull(buffer); + Assert.isNull(dataBuffer); - this.buffer = bytes; + this.dataBuffer = bytes; try { onWritePossible(); } @@ -316,11 +314,11 @@ public class ServletHttpHandlerAdapter extends HttpServlet { @Override public void onComplete() { - logger.debug("Complete buffer: " + (buffer == null)); + logger.debug("Complete buffer: " + (dataBuffer == null)); this.subscriberComplete = true; - if (buffer == null) { + if (dataBuffer == null) { this.synchronizer.writeComplete(); } } @@ -330,14 +328,16 @@ public class ServletHttpHandlerAdapter extends HttpServlet { ServletOutputStream output = this.synchronizer.getOutputStream(); boolean ready = output.isReady(); - logger.debug("Output: " + ready + " buffer: " + (buffer == null)); + logger.debug("Output: " + ready + " buffer: " + (dataBuffer == null)); if (ready) { - if (this.buffer != null) { - byte[] bytes = new byte[this.buffer.readableByteCount()]; - this.buffer.read(bytes); - this.buffer = null; - output.write(bytes); + if (this.dataBuffer != null) { + InputStream in = this.dataBuffer.asInputStream(); + byte[] buffer = new byte[BUFFER_SIZE]; + int bytesRead; + while ((bytesRead = in.read(buffer)) != -1) { + output.write(buffer, 0, bytesRead); + } if (!subscriberComplete) { this.subscription.request(1); } From f920d2ce1e3cd1736be8567bfed7c1b8170b232e Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Tue, 23 Feb 2016 09:39:20 +0000 Subject: [PATCH 227/498] sync with reactor-stream update --- .../http/server/reactive/AsyncIntegrationTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java index 8463c12949..e06acc147a 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java @@ -106,7 +106,7 @@ public class AsyncIntegrationTests { @Override public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { return response.setBody(Stream.just("h", "e", "l", "l", "o") - .timer(Timer.global()) + .useTimer(Timer.global()) .throttleRequest(100) .dispatchOn(asyncGroup) .collect(allocator::allocateBuffer, From 2fcbeb659515dc9b650aa2f0ba09ab2348e67a1d Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Tue, 23 Feb 2016 14:14:34 +0100 Subject: [PATCH 228/498] Fixed issue with DefaultDataBuffer.asInputStream() --- .../springframework/core/io/buffer/DefaultDataBuffer.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java index 27f123ac82..3bd7859957 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java @@ -250,14 +250,15 @@ public class DefaultDataBuffer implements DataBuffer { @Override public int read() { return readInternal( - buffer -> buffer.hasRemaining() ? buffer.get() & 0xFF : -1); + buffer -> readableByteCount() > 0 ? buffer.get() & 0xFF : -1); } @Override public int read(byte[] bytes, int off, int len) throws IOException { return readInternal(buffer -> { - if (buffer.hasRemaining()) { - int minLen = Math.min(len, buffer.remaining()); + int count = readableByteCount(); + if (count > 0) { + int minLen = Math.min(len, count); buffer.get(bytes, off, minLen); return minLen; } From 361707c448f41612b018f2e50452dfb185fbbd5e Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Tue, 23 Feb 2016 12:17:14 +0100 Subject: [PATCH 229/498] Servlet 3.1 support cleanup Claneup of the Servlet 3.1 support: - moved RequestBodyPublisher to ServletServerHttpRequest - moved ResponseBodySubscribera to ServletServerHttpResponse - response body is now copied to ServletOutputStream in chunks, rather than one big byte[] --- .../ServletAsyncContextSynchronizer.java | 18 +- .../reactive/ServletHttpHandlerAdapter.java | 321 ++---------------- .../reactive/ServletServerHttpRequest.java | 216 +++++++++++- .../reactive/ServletServerHttpResponse.java | 132 ++++++- .../reactive/AsyncIntegrationTests.java | 50 +-- 5 files changed, 376 insertions(+), 361 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletAsyncContextSynchronizer.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletAsyncContextSynchronizer.java index dc1e015b5e..283b598e9e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletAsyncContextSynchronizer.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletAsyncContextSynchronizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -21,6 +21,8 @@ import java.util.concurrent.atomic.AtomicInteger; import javax.servlet.AsyncContext; import javax.servlet.ServletInputStream; import javax.servlet.ServletOutputStream; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; /** * Utility class for synchronizing between the reading and writing side of an @@ -40,10 +42,12 @@ final class ServletAsyncContextSynchronizer { private static final int COMPLETE = READ_COMPLETE | WRITE_COMPLETE; + private final AsyncContext asyncContext; private final AtomicInteger complete = new AtomicInteger(NONE_COMPLETE); + /** * Creates a new {@code AsyncContextSynchronizer} based on the given context. * @param asyncContext the context to base this synchronizer on @@ -52,13 +56,21 @@ final class ServletAsyncContextSynchronizer { this.asyncContext = asyncContext; } + public ServletRequest getRequest() { + return this.asyncContext.getRequest(); + } + + public ServletResponse getResponse() { + return this.asyncContext.getResponse(); + } + /** * Returns the input stream of this synchronizer. * @return the input stream * @throws IOException if an input or output exception occurred */ public ServletInputStream getInputStream() throws IOException { - return this.asyncContext.getRequest().getInputStream(); + return getRequest().getInputStream(); } /** @@ -67,7 +79,7 @@ final class ServletAsyncContextSynchronizer { * @throws IOException if an input or output exception occurred */ public ServletOutputStream getOutputStream() throws IOException { - return this.asyncContext.getResponse().getOutputStream(); + return getResponse().getOutputStream(); } /** diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java index 048fa38014..80b447c43c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java @@ -17,14 +17,8 @@ package org.springframework.http.server.reactive; import java.io.IOException; -import java.io.InputStream; -import java.util.concurrent.atomic.AtomicLong; import javax.servlet.AsyncContext; -import javax.servlet.ReadListener; import javax.servlet.ServletException; -import javax.servlet.ServletInputStream; -import javax.servlet.ServletOutputStream; -import javax.servlet.WriteListener; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; @@ -32,17 +26,13 @@ import javax.servlet.http.HttpServletResponse; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; -import reactor.core.publisher.Mono; -import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferAllocator; import org.springframework.core.io.buffer.DefaultDataBufferAllocator; import org.springframework.http.HttpStatus; import org.springframework.util.Assert; -import org.springframework.util.StreamUtils; /** * @author Arjen Poutsma @@ -51,24 +41,35 @@ import org.springframework.util.StreamUtils; @WebServlet(asyncSupported = true) public class ServletHttpHandlerAdapter extends HttpServlet { - private static final int BUFFER_SIZE = 8192; + private static final int DEFAULT_BUFFER_SIZE = 8192; private static Log logger = LogFactory.getLog(ServletHttpHandlerAdapter.class); private HttpHandler handler; - private DataBufferAllocator allocator = new DefaultDataBufferAllocator(); + // Servlet is based on blocking I/O, hence the usage of non-direct, heap-based buffers + // (i.e. 'false' as constructor argument) + private DataBufferAllocator allocator = new DefaultDataBufferAllocator(false); + + private int bufferSize = DEFAULT_BUFFER_SIZE; public void setHandler(HttpHandler handler) { + Assert.notNull(handler, "'handler' must not be null"); this.handler = handler; } public void setAllocator(DataBufferAllocator allocator) { + Assert.notNull(allocator, "'allocator' must not be null"); this.allocator = allocator; } + public void setBufferSize(int bufferSize) { + Assert.isTrue(bufferSize > 0); + this.bufferSize = bufferSize; + } + @Override protected void service(HttpServletRequest servletRequest, HttpServletResponse servletResponse) throws ServletException, IOException { @@ -76,299 +77,25 @@ public class ServletHttpHandlerAdapter extends HttpServlet { AsyncContext context = servletRequest.startAsync(); ServletAsyncContextSynchronizer synchronizer = new ServletAsyncContextSynchronizer(context); - RequestBodyPublisher requestBody = - new RequestBodyPublisher(synchronizer, allocator, BUFFER_SIZE); - ServletServerHttpRequest request = new ServletServerHttpRequest(servletRequest, requestBody); - servletRequest.getInputStream().setReadListener(requestBody); + ServletServerHttpRequest request = + new ServletServerHttpRequest(synchronizer, this.allocator, + this.bufferSize); - ResponseBodySubscriber responseBodySubscriber = - new ResponseBodySubscriber(synchronizer); - ServletServerHttpResponse response = new ServletServerHttpResponse(servletResponse, - publisher -> Mono.from(subscriber -> publisher.subscribe(responseBodySubscriber))); - servletResponse.getOutputStream().setWriteListener(responseBodySubscriber); + ServletServerHttpResponse response = + new ServletServerHttpResponse(synchronizer, this.bufferSize); + + HandlerResultSubscriber resultSubscriber = + new HandlerResultSubscriber(synchronizer); - HandlerResultSubscriber resultSubscriber = new HandlerResultSubscriber(synchronizer, response); this.handler.handle(request, response).subscribe(resultSubscriber); } - private static class RequestBodyPublisher - implements ReadListener, Publisher { - - private final ServletAsyncContextSynchronizer synchronizer; - - private final DataBufferAllocator allocator; - - private final byte[] buffer; - - private final DemandCounter demand = new DemandCounter(); - - private Subscriber subscriber; - - private boolean stalled; - - private boolean cancelled; - - public RequestBodyPublisher(ServletAsyncContextSynchronizer synchronizer, - DataBufferAllocator allocator, int bufferSize) { - this.synchronizer = synchronizer; - this.allocator = allocator; - this.buffer = new byte[bufferSize]; - } - - @Override - public void subscribe(Subscriber subscriber) { - if (subscriber == null) { - throw new NullPointerException(); - } - else if (this.subscriber != null) { - subscriber.onError(new IllegalStateException("Only one subscriber allowed")); - } - this.subscriber = subscriber; - this.subscriber.onSubscribe(new RequestBodySubscription()); - } - - @Override - public void onDataAvailable() throws IOException { - if (cancelled) { - return; - } - ServletInputStream input = this.synchronizer.getInputStream(); - logger.debug("onDataAvailable: " + input); - - while (true) { - logger.debug("Demand: " + this.demand); - - if (!demand.hasDemand()) { - stalled = true; - break; - } - - boolean ready = input.isReady(); - logger.debug("Input ready: " + ready + " finished: " + input.isFinished()); - - if (!ready) { - break; - } - - int read = input.read(buffer); - logger.debug("Input read:" + read); - - if (read == -1) { - break; - } - else if (read > 0) { - this.demand.decrement(); - - DataBuffer dataBuffer = allocator.allocateBuffer(read); - dataBuffer.write(this.buffer, 0, read); - - this.subscriber.onNext(dataBuffer); - - } - } - } - - @Override - public void onAllDataRead() throws IOException { - if (cancelled) { - return; - } - logger.debug("All data read"); - this.synchronizer.readComplete(); - if (this.subscriber != null) { - this.subscriber.onComplete(); - } - } - - @Override - public void onError(Throwable t) { - if (cancelled) { - return; - } - logger.error("RequestBodyPublisher Error", t); - this.synchronizer.readComplete(); - if (this.subscriber != null) { - this.subscriber.onError(t); - } - } - - private class RequestBodySubscription implements Subscription { - - @Override - public void request(long n) { - if (cancelled) { - return; - } - logger.debug("Updating demand " + demand + " by " + n); - - demand.increase(n); - - logger.debug("Stalled: " + stalled); - - if (stalled) { - stalled = false; - try { - onDataAvailable(); - } - catch (IOException ex) { - onError(ex); - } - } - } - - @Override - public void cancel() { - if (cancelled) { - return; - } - cancelled = true; - synchronizer.readComplete(); - demand.reset(); - } - } - - - /** - * Small utility class for keeping track of Reactive Streams demand. - */ - private static final class DemandCounter { - - private final AtomicLong demand = new AtomicLong(); - - /** - * Increases the demand by the given number - * @param n the positive number to increase demand by - * @return the increased demand - * @see org.reactivestreams.Subscription#request(long) - */ - public long increase(long n) { - Assert.isTrue(n > 0, "'n' must be higher than 0"); - return demand.updateAndGet(d -> d != Long.MAX_VALUE ? d + n : Long.MAX_VALUE); - } - - /** - * Decreases the demand by one. - * @return the decremented demand - */ - public long decrement() { - return demand.updateAndGet(d -> d != Long.MAX_VALUE ? d - 1 : Long.MAX_VALUE); - } - - /** - * Indicates whether this counter has demand, i.e. whether it is higher than 0. - * @return {@code true} if this counter has demand; {@code false} otherwise - */ - public boolean hasDemand() { - return this.demand.get() > 0; - } - - /** - * Resets this counter to 0. - * @see org.reactivestreams.Subscription#cancel() - */ - public void reset() { - this.demand.set(0); - } - - @Override - public String toString() { - return demand.toString(); - } - } - } - - private static class ResponseBodySubscriber - implements WriteListener, Subscriber { - - private final ServletAsyncContextSynchronizer synchronizer; - - private Subscription subscription; - - private DataBuffer dataBuffer; - - private volatile boolean subscriberComplete = false; - - public ResponseBodySubscriber(ServletAsyncContextSynchronizer synchronizer) { - this.synchronizer = synchronizer; - } - - - @Override - public void onSubscribe(Subscription subscription) { - this.subscription = subscription; - this.subscription.request(1); - } - - @Override - public void onNext(DataBuffer bytes) { - Assert.isNull(dataBuffer); - - this.dataBuffer = bytes; - try { - onWritePossible(); - } - catch (IOException e) { - onError(e); - } - } - - @Override - public void onComplete() { - logger.debug("Complete buffer: " + (dataBuffer == null)); - - this.subscriberComplete = true; - - if (dataBuffer == null) { - this.synchronizer.writeComplete(); - } - } - - @Override - public void onWritePossible() throws IOException { - ServletOutputStream output = this.synchronizer.getOutputStream(); - - boolean ready = output.isReady(); - logger.debug("Output: " + ready + " buffer: " + (dataBuffer == null)); - - if (ready) { - if (this.dataBuffer != null) { - InputStream in = this.dataBuffer.asInputStream(); - byte[] buffer = new byte[BUFFER_SIZE]; - int bytesRead; - while ((bytesRead = in.read(buffer)) != -1) { - output.write(buffer, 0, bytesRead); - } - if (!subscriberComplete) { - this.subscription.request(1); - } - else { - this.synchronizer.writeComplete(); - } - } - else { - this.subscription.request(1); - } - } - } - - @Override - public void onError(Throwable t) { - logger.error("ResponseBodySubscriber error", t); - } - } - private static class HandlerResultSubscriber implements Subscriber { private final ServletAsyncContextSynchronizer synchronizer; - private final ServletServerHttpResponse response; - - - public HandlerResultSubscriber(ServletAsyncContextSynchronizer synchronizer, - ServletServerHttpResponse response) { - + public HandlerResultSubscriber(ServletAsyncContextSynchronizer synchronizer) { this.synchronizer = synchronizer; - this.response = response; } @@ -385,7 +112,9 @@ public class ServletHttpHandlerAdapter extends HttpServlet { @Override public void onError(Throwable ex) { logger.error("Error from request handling. Completing the request.", ex); - this.response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR); + HttpServletResponse response = + (HttpServletResponse) this.synchronizer.getResponse(); + response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); this.synchronizer.complete(); } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java index cbfb6e7342..2374d86231 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java @@ -16,6 +16,7 @@ package org.springframework.http.server.reactive; +import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.Charset; @@ -23,13 +24,21 @@ import java.util.ArrayList; import java.util.Enumeration; import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; import reactor.core.publisher.Flux; import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferAllocator; import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -45,16 +54,22 @@ import org.springframework.util.StringUtils; */ public class ServletServerHttpRequest extends AbstractServerHttpRequest { + private static final Log logger = LogFactory.getLog(ServletServerHttpRequest.class); + private final HttpServletRequest request; private final Flux requestBodyPublisher; - public ServletServerHttpRequest(HttpServletRequest request, - Publisher body) { - Assert.notNull(request, "'request' must not be null."); - Assert.notNull(body, "'body' must not be null."); - this.request = request; - this.requestBodyPublisher = Flux.from(body); + public ServletServerHttpRequest(ServletAsyncContextSynchronizer synchronizer, + DataBufferAllocator allocator, int bufferSize) throws IOException { + Assert.notNull(synchronizer, "'synchronizer' must not be null"); + Assert.notNull(allocator, "'allocator' must not be null"); + + this.request = (HttpServletRequest) synchronizer.getRequest(); + RequestBodyPublisher bodyPublisher = + new RequestBodyPublisher(synchronizer, allocator, bufferSize); + this.requestBodyPublisher = Flux.from(bodyPublisher); + this.request.getInputStream().setReadListener(bodyPublisher); } @@ -132,4 +147,193 @@ public class ServletServerHttpRequest extends AbstractServerHttpRequest { return this.requestBodyPublisher; } + private static class RequestBodyPublisher + implements ReadListener, Publisher { + + private final ServletAsyncContextSynchronizer synchronizer; + + private final DataBufferAllocator allocator; + + private final byte[] buffer; + + private final DemandCounter demand = new DemandCounter(); + + private Subscriber subscriber; + + private boolean stalled; + + private boolean cancelled; + + public RequestBodyPublisher(ServletAsyncContextSynchronizer synchronizer, + DataBufferAllocator allocator, int bufferSize) { + this.synchronizer = synchronizer; + this.allocator = allocator; + this.buffer = new byte[bufferSize]; + } + + @Override + public void subscribe(Subscriber subscriber) { + if (subscriber == null) { + throw new NullPointerException(); + } + else if (this.subscriber != null) { + subscriber.onError( + new IllegalStateException("Only one subscriber allowed")); + } + this.subscriber = subscriber; + this.subscriber.onSubscribe(new RequestBodySubscription()); + } + + @Override + public void onDataAvailable() throws IOException { + if (cancelled) { + return; + } + ServletInputStream input = this.synchronizer.getInputStream(); + logger.trace("onDataAvailable: " + input); + + while (true) { + logger.trace("Demand: " + this.demand); + + if (!demand.hasDemand()) { + stalled = true; + break; + } + + boolean ready = input.isReady(); + logger.trace( + "Input ready: " + ready + " finished: " + input.isFinished()); + + if (!ready) { + break; + } + + int read = input.read(buffer); + logger.trace("Input read:" + read); + + if (read == -1) { + break; + } + else if (read > 0) { + this.demand.decrement(); + + DataBuffer dataBuffer = allocator.allocateBuffer(read); + dataBuffer.write(this.buffer, 0, read); + + this.subscriber.onNext(dataBuffer); + + } + } + } + + @Override + public void onAllDataRead() throws IOException { + if (cancelled) { + return; + } + logger.trace("All data read"); + this.synchronizer.readComplete(); + if (this.subscriber != null) { + this.subscriber.onComplete(); + } + } + + @Override + public void onError(Throwable t) { + if (cancelled) { + return; + } + logger.trace("RequestBodyPublisher Error", t); + this.synchronizer.readComplete(); + if (this.subscriber != null) { + this.subscriber.onError(t); + } + } + + private class RequestBodySubscription implements Subscription { + + @Override + public void request(long n) { + if (cancelled) { + return; + } + logger.trace("Updating demand " + demand + " by " + n); + + demand.increase(n); + + logger.trace("Stalled: " + stalled); + + if (stalled) { + stalled = false; + try { + onDataAvailable(); + } + catch (IOException ex) { + onError(ex); + } + } + } + + @Override + public void cancel() { + if (cancelled) { + return; + } + cancelled = true; + synchronizer.readComplete(); + demand.reset(); + } + } + + /** + * Small utility class for keeping track of Reactive Streams demand. + */ + private static final class DemandCounter { + + private final AtomicLong demand = new AtomicLong(); + + /** + * Increases the demand by the given number + * @param n the positive number to increase demand by + * @return the increased demand + * @see Subscription#request(long) + */ + public long increase(long n) { + Assert.isTrue(n > 0, "'n' must be higher than 0"); + return demand + .updateAndGet(d -> d != Long.MAX_VALUE ? d + n : Long.MAX_VALUE); + } + + /** + * Decreases the demand by one. + * @return the decremented demand + */ + public long decrement() { + return demand + .updateAndGet(d -> d != Long.MAX_VALUE ? d - 1 : Long.MAX_VALUE); + } + + /** + * Indicates whether this counter has demand, i.e. whether it is higher than + * 0. + * @return {@code true} if this counter has demand; {@code false} otherwise + */ + public boolean hasDemand() { + return this.demand.get() > 0; + } + + /** + * Resets this counter to 0. + * @see Subscription#cancel() + */ + public void reset() { + this.demand.set(0); + } + + @Override + public String toString() { + return demand.toString(); + } + } + } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java index eeb50d96c8..2ee70f42a9 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java @@ -16,14 +16,21 @@ package org.springframework.http.server.reactive; +import java.io.IOException; +import java.io.InputStream; import java.nio.charset.Charset; import java.util.List; import java.util.Map; -import java.util.function.Function; +import javax.servlet.ServletOutputStream; +import javax.servlet.WriteListener; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletResponse; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; import reactor.core.publisher.Mono; import org.springframework.core.io.buffer.DataBuffer; @@ -39,18 +46,20 @@ import org.springframework.util.Assert; */ public class ServletServerHttpResponse extends AbstractServerHttpResponse { + private static final Log logger = LogFactory.getLog(ServletServerHttpResponse.class); + private final HttpServletResponse response; - private final Function, Mono> responseBodyWriter; + private final ResponseBodySubscriber responseBodySubscriber; + public ServletServerHttpResponse(ServletAsyncContextSynchronizer synchronizer, + int bufferSize) throws IOException { + Assert.notNull(synchronizer, "'synchronizer' must not be null"); - public ServletServerHttpResponse(HttpServletResponse response, - Function, Mono> responseBodyWriter) { - - Assert.notNull(response, "'response' must not be null"); - Assert.notNull(responseBodyWriter, "'responseBodyWriter' must not be null"); - this.response = response; - this.responseBodyWriter = responseBodyWriter; + this.response = (HttpServletResponse) synchronizer.getResponse(); + this.responseBodySubscriber = + new ResponseBodySubscriber(synchronizer, bufferSize); + this.response.getOutputStream().setWriteListener(responseBodySubscriber); } @@ -65,7 +74,8 @@ public class ServletServerHttpResponse extends AbstractServerHttpResponse { @Override protected Mono setBodyInternal(Publisher publisher) { - return this.responseBodyWriter.apply(publisher); + return Mono.from((Publisher) subscriber -> publisher + .subscribe(this.responseBodySubscriber)); } @Override @@ -107,4 +117,106 @@ public class ServletServerHttpResponse extends AbstractServerHttpResponse { } } + private static class ResponseBodySubscriber + implements WriteListener, Subscriber { + + private final ServletAsyncContextSynchronizer synchronizer; + + private final int bufferSize; + + private Subscription subscription; + + private DataBuffer dataBuffer; + + private volatile boolean subscriberComplete = false; + + public ResponseBodySubscriber(ServletAsyncContextSynchronizer synchronizer, + int bufferSize) { + this.synchronizer = synchronizer; + this.bufferSize = bufferSize; + } + + @Override + public void onSubscribe(Subscription subscription) { + this.subscription = subscription; + this.subscription.request(1); + } + + @Override + public void onNext(DataBuffer dataBuffer) { + Assert.isNull(this.dataBuffer); + logger.trace("onNext. buffer: " + dataBuffer); + + this.dataBuffer = dataBuffer; + try { + onWritePossible(); + } + catch (IOException e) { + onError(e); + } + } + + @Override + public void onComplete() { + logger.trace("onComplete. buffer: " + dataBuffer); + + this.subscriberComplete = true; + + if (dataBuffer == null) { + this.synchronizer.writeComplete(); + } + } + + @Override + public void onWritePossible() throws IOException { + ServletOutputStream output = this.synchronizer.getOutputStream(); + + boolean ready = output.isReady(); + logger.trace("onWritePossible. ready: " + ready + " buffer: " + dataBuffer); + + if (ready) { + if (this.dataBuffer != null) { + int toBeWritten = this.dataBuffer.readableByteCount(); + InputStream input = this.dataBuffer.asInputStream(); + int writeCount = write(input, output); + logger.trace("written: " + writeCount + " total: " + toBeWritten); + if (writeCount == toBeWritten) { + this.dataBuffer = null; + if (!this.subscriberComplete) { + this.subscription.request(1); + } + else { + this.synchronizer.writeComplete(); + } + } + } + else if (this.subscription != null) { + this.subscription.request(1); + } + } + } + + private int write(InputStream in, ServletOutputStream output) throws IOException { + int byteCount = 0; + byte[] buffer = new byte[bufferSize]; + int bytesRead = -1; + while (output.isReady() && (bytesRead = in.read(buffer)) != -1) { + output.write(buffer, 0, bytesRead); + byteCount += bytesRead; + } + return byteCount; + } + + @Override + public void onError(Throwable ex) { + if (this.subscription != null) { + this.subscription.cancel(); + } + logger.error("ResponseBodySubscriber error", ex); + HttpServletResponse response = + (HttpServletResponse) this.synchronizer.getResponse(); + response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); + this.synchronizer.complete(); + } + } } \ No newline at end of file diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java index e06acc147a..b11739f554 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java @@ -19,11 +19,7 @@ package org.springframework.http.server.reactive; import java.net.URI; import org.hamcrest.Matchers; -import org.junit.After; -import org.junit.Before; import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; import reactor.core.publisher.Mono; import reactor.core.publisher.SchedulerGroup; import reactor.core.timer.Timer; @@ -33,11 +29,6 @@ import org.springframework.core.io.buffer.DataBufferAllocator; import org.springframework.core.io.buffer.DefaultDataBufferAllocator; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; -import org.springframework.http.server.reactive.boot.HttpServer; -import org.springframework.http.server.reactive.boot.ReactorHttpServer; -import org.springframework.http.server.reactive.boot.RxNettyHttpServer; -import org.springframework.http.server.reactive.boot.UndertowHttpServer; -import org.springframework.util.SocketUtils; import org.springframework.web.client.RestTemplate; import static org.junit.Assert.assertThat; @@ -47,48 +38,15 @@ import static org.junit.Assert.assertThat; * * @author Stephane Maldini */ -@RunWith(Parameterized.class) -public class AsyncIntegrationTests { +public class AsyncIntegrationTests extends AbstractHttpHandlerIntegrationTests { private final SchedulerGroup asyncGroup = SchedulerGroup.async(); private final DataBufferAllocator allocator = new DefaultDataBufferAllocator(); - protected int port; - - @Parameterized.Parameter(0) - public HttpServer server; - - private AsyncHandler asyncHandler; - - @Parameterized.Parameters(name = "server [{0}]") - public static Object[][] arguments() { - return new Object[][]{ - //{new JettyHttpServer()}, - {new RxNettyHttpServer()}, - {new ReactorHttpServer()}, - //{new TomcatHttpServer()}, - {new UndertowHttpServer()} - }; - } - - @Before - public void setup() throws Exception { - this.port = SocketUtils.findAvailableTcpPort(); - this.server.setPort(this.port); - this.server.setHandler(createHttpHandler()); - this.server.afterPropertiesSet(); - this.server.start(); - } - - protected HttpHandler createHttpHandler() { - this.asyncHandler = new AsyncHandler(); - return this.asyncHandler; - } - - @After - public void tearDown() throws Exception { - this.server.stop(); + @Override + protected AsyncHandler createHttpHandler() { + return new AsyncHandler(); } @SuppressWarnings("unchecked") From 9f94f8c88ed77b2a9e3c45696c9a7c6c081524b8 Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Fri, 26 Feb 2016 21:42:52 +0000 Subject: [PATCH 230/498] Sync to Fluxion to remove Stream confusion --- spring-web-reactive/build.gradle | 2 +- ...ctiveStreamsToReactorFluxionConverter.java} | 16 ++++++++-------- .../reactive/AbstractServerHttpResponse.java | 1 - .../server/reactive/AsyncIntegrationTests.java | 4 ++-- .../reactive/WriteWithOperatorTests.java | 12 ++++++------ .../reactive/DispatcherHandlerErrorTests.java | 16 ++++++++-------- .../ResponseStatusExceptionHandlerTests.java | 12 +++++------- .../SimpleHandlerResultHandlerTests.java | 15 ++++++++------- .../method/InvocableHandlerMethodTests.java | 14 +++++++------- .../RequestMappingIntegrationTests.java | 18 +++++++++--------- 10 files changed, 54 insertions(+), 56 deletions(-) rename spring-web-reactive/src/main/java/org/springframework/core/convert/support/{ReactiveStreamsToReactorStreamConverter.java => ReactiveStreamsToReactorFluxionConverter.java} (79%) diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index a6d4226ad6..b75911785b 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -88,7 +88,7 @@ dependencies { optional 'io.reactivex:rxjava:1.1.0' optional "io.reactivex:rxnetty-http:0.5.0-SNAPSHOT" optional "com.fasterxml.jackson.core:jackson-databind:2.6.2" - optional "io.projectreactor:reactor-stream:${reactorVersion}" + optional "io.projectreactor:reactor-fluxion:${reactorVersion}" optional "io.projectreactor:reactor-net:${reactorVersion}" optional "io.projectreactor:reactor-io:${reactorVersion}" optional "org.apache.tomcat:tomcat-util:${tomcatVersion}" diff --git a/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToReactorStreamConverter.java b/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToReactorFluxionConverter.java similarity index 79% rename from spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToReactorStreamConverter.java rename to spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToReactorFluxionConverter.java index 3cc34af221..8540be91bf 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToReactorStreamConverter.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToReactorFluxionConverter.java @@ -20,8 +20,8 @@ import java.util.LinkedHashSet; import java.util.Set; import org.reactivestreams.Publisher; +import reactor.rx.Fluxion; import reactor.rx.Promise; -import reactor.rx.Stream; import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.GenericConverter; @@ -30,13 +30,13 @@ import org.springframework.core.convert.converter.GenericConverter; * @author Stephane Maldini * @author Sebastien Deleuze */ -public final class ReactiveStreamsToReactorStreamConverter implements GenericConverter { +public final class ReactiveStreamsToReactorFluxionConverter implements GenericConverter { @Override public Set getConvertibleTypes() { Set pairs = new LinkedHashSet<>(); - pairs.add(new GenericConverter.ConvertiblePair(Publisher.class, Stream.class)); - pairs.add(new GenericConverter.ConvertiblePair(Stream.class, Publisher.class)); + pairs.add(new GenericConverter.ConvertiblePair(Publisher.class, Fluxion.class)); + pairs.add(new GenericConverter.ConvertiblePair(Fluxion.class, Publisher.class)); pairs.add(new GenericConverter.ConvertiblePair(Publisher.class, Promise.class)); pairs.add(new GenericConverter.ConvertiblePair(Promise.class, Publisher.class)); return pairs; @@ -47,17 +47,17 @@ public final class ReactiveStreamsToReactorStreamConverter implements GenericCon if (source == null) { return null; } - if (Stream.class.isAssignableFrom(source.getClass())) { + if (Fluxion.class.isAssignableFrom(source.getClass())) { return source; } - else if (Stream.class.isAssignableFrom(targetType.getResolvableType().getRawClass())) { - return Stream.from((Publisher)source); + else if (Fluxion.class.isAssignableFrom(targetType.getResolvableType().getRawClass())) { + return Fluxion.from((Publisher)source); } else if (Promise.class.isAssignableFrom(source.getClass())) { return source; } else if (Promise.class.isAssignableFrom(targetType.getResolvableType().getRawClass())) { - return Stream.from((Publisher)source).promise(); + return Fluxion.from((Publisher)source).promise(); } return null; } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java index 704ddf4db6..bf79e54391 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java @@ -23,7 +23,6 @@ import java.util.function.Supplier; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import reactor.rx.Stream; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpHeaders; diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java index b11739f554..5c121a2ecf 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java @@ -23,7 +23,7 @@ import org.junit.Test; import reactor.core.publisher.Mono; import reactor.core.publisher.SchedulerGroup; import reactor.core.timer.Timer; -import reactor.rx.Stream; +import reactor.rx.Fluxion; import org.springframework.core.io.buffer.DataBufferAllocator; import org.springframework.core.io.buffer.DefaultDataBufferAllocator; @@ -63,7 +63,7 @@ public class AsyncIntegrationTests extends AbstractHttpHandlerIntegrationTests { @Override public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { - return response.setBody(Stream.just("h", "e", "l", "l", "o") + return response.setBody(Fluxion.just("h", "e", "l", "l", "o") .useTimer(Timer.global()) .throttleRequest(100) .dispatchOn(asyncGroup) diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/WriteWithOperatorTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/WriteWithOperatorTests.java index 973909a12e..56a2e02655 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/WriteWithOperatorTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/WriteWithOperatorTests.java @@ -29,8 +29,8 @@ import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import reactor.core.publisher.Flux; import reactor.core.subscriber.SubscriberBarrier; +import reactor.rx.Fluxion; import reactor.rx.Signal; -import reactor.rx.Stream; import static org.junit.Assert.*; @@ -55,7 +55,7 @@ public class WriteWithOperatorTests { public void errorBeforeFirstItem() throws Exception { IllegalStateException error = new IllegalStateException("boo"); Publisher completion = Flux.error(error).lift(this.operator); - List> signals = Stream.from(completion).materialize().toList().get(); + List> signals = Fluxion.from(completion).materialize().toList().get(); assertEquals(1, signals.size()); assertSame("Unexpected signal: " + signals.get(0), error, signals.get(0).getThrowable()); @@ -64,7 +64,7 @@ public class WriteWithOperatorTests { @Test public void completionBeforeFirstItem() throws Exception { Publisher completion = Flux.empty().lift(this.operator); - List> signals = Stream.from(completion).materialize().toList().get(); + List> signals = Fluxion.from(completion).materialize().toList().get(); assertEquals(1, signals.size()); assertTrue("Unexpected signal: " + signals.get(0), signals.get(0).isOnComplete()); @@ -76,7 +76,7 @@ public class WriteWithOperatorTests { @Test public void writeOneItem() throws Exception { Publisher completion = Flux.just("one").lift(this.operator); - List> signals = Stream.from(completion).materialize().toList().get(); + List> signals = Fluxion.from(completion).materialize().toList().get(); assertEquals(1, signals.size()); assertTrue("Unexpected signal: " + signals.get(0), signals.get(0).isOnComplete()); @@ -91,7 +91,7 @@ public class WriteWithOperatorTests { public void writeMultipleItems() throws Exception { List items = Arrays.asList("one", "two", "three"); Publisher completion = Flux.fromIterable(items).lift(this.operator); - List> signals = Stream.from(completion).materialize().toList().get(); + List> signals = Fluxion.from(completion).materialize().toList().get(); assertEquals(1, signals.size()); assertTrue("Unexpected signal: " + signals.get(0), signals.get(0).isOnComplete()); @@ -114,7 +114,7 @@ public class WriteWithOperatorTests { } }, subscriber -> new AtomicInteger()); Publisher completion = publisher.lift(this.operator); - List> signals = Stream.from(completion).materialize().toList().get(); + List> signals = Fluxion.from(completion).materialize().toList().get(); assertEquals(1, signals.size()); assertSame("Unexpected signal: " + signals.get(0), error, signals.get(0).getThrowable()); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java index 776277fbb6..843ca5c567 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java @@ -24,8 +24,8 @@ import org.junit.Before; import org.junit.Test; import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; +import reactor.rx.Fluxion; import reactor.rx.Signal; -import reactor.rx.Stream; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; @@ -49,14 +49,14 @@ import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.reactive.method.annotation.RequestMappingHandlerAdapter; import org.springframework.web.reactive.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.reactive.method.annotation.ResponseBodyResultHandler; -import org.springframework.web.server.adapter.DefaultServerWebExchange; -import org.springframework.web.server.handler.ExceptionHandlingWebHandler; -import org.springframework.web.server.handler.FilteringWebHandler; +import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebExceptionHandler; import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilterChain; import org.springframework.web.server.WebHandler; -import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.handler.ExceptionHandlingWebHandler; +import org.springframework.web.server.handler.FilteringWebHandler; import org.springframework.web.server.session.WebSessionManager; import static org.hamcrest.CoreMatchers.startsWith; @@ -192,7 +192,7 @@ public class DispatcherHandlerErrorTests { WebHandler webHandler = new ExceptionHandlingWebHandler(this.dispatcherHandler, exceptionHandler); Publisher publisher = webHandler.handle(this.exchange); - Stream.from(publisher).toList().get(); + Fluxion.from(publisher).toList().get(); assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, this.response.getStatus()); } @@ -204,13 +204,13 @@ public class DispatcherHandlerErrorTests { webHandler = new ExceptionHandlingWebHandler(webHandler, new ServerError500ExceptionHandler()); Publisher publisher = webHandler.handle(this.exchange); - Stream.from(publisher).toList().get(); + Fluxion.from(publisher).toList().get(); assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, this.response.getStatus()); } private Throwable awaitErrorSignal(Publisher publisher) throws Exception { - Signal signal = Stream.from(publisher).materialize().toList().get().get(0); + Signal signal = Fluxion.from(publisher).materialize().toList().get().get(0); assertEquals("Unexpected signal: " + signal, Signal.Type.ERROR, signal.getType()); return signal.getThrowable(); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java index 4257499747..bc64ab2277 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java @@ -21,21 +21,19 @@ import java.util.List; import org.junit.Before; import org.junit.Test; import org.reactivestreams.Publisher; +import reactor.rx.Fluxion; import reactor.rx.Signal; -import reactor.rx.Stream; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.MockServerHttpRequest; import org.springframework.http.server.reactive.MockServerHttpResponse; import org.springframework.web.ResponseStatusException; -import org.springframework.web.server.adapter.DefaultServerWebExchange; import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; import org.springframework.web.server.session.WebSessionManager; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; import static org.mockito.Mockito.mock; /** @@ -65,7 +63,7 @@ public class ResponseStatusExceptionHandlerTests { Throwable ex = new ResponseStatusException(HttpStatus.BAD_REQUEST); Publisher publisher = this.handler.handle(this.exchange, ex); - Stream.from(publisher).toList().get(); + Fluxion.from(publisher).toList().get(); assertEquals(HttpStatus.BAD_REQUEST, this.response.getStatus()); } @@ -74,7 +72,7 @@ public class ResponseStatusExceptionHandlerTests { Throwable ex = new IllegalStateException(); Publisher publisher = this.handler.handle(this.exchange, ex); - List> signals = Stream.from(publisher).materialize().toList().get(); + List> signals = Fluxion.from(publisher).materialize().toList().get(); assertEquals(1, signals.size()); assertTrue(signals.get(0).hasError()); assertSame(ex, signals.get(0).getThrowable()); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandlerTests.java index 907992e14d..a0ba187223 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandlerTests.java @@ -18,22 +18,23 @@ package org.springframework.web.reactive.handler; import java.util.concurrent.CompletableFuture; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; import org.junit.Test; import org.reactivestreams.Publisher; -import reactor.rx.Stream; +import reactor.rx.Fluxion; import rx.Observable; import org.springframework.core.ResolvableType; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter; -import org.springframework.core.convert.support.ReactiveStreamsToReactorStreamConverter; +import org.springframework.core.convert.support.ReactiveStreamsToReactorFluxionConverter; import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter; import org.springframework.ui.ExtendedModelMap; import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerResult; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + /** * @author Sebastien Deleuze */ @@ -59,7 +60,7 @@ public class SimpleHandlerResultHandlerTests { hm = new HandlerMethod(controller, TestController.class.getMethod("streamVoid")); type = ResolvableType.forMethodParameter(hm.getReturnType()); - // Reactor Stream is a Publisher + // Reactor Fluxion is a Publisher assertTrue(resultHandler.supports(createHandlerResult(hm, type))); hm = new HandlerMethod(controller, TestController.class.getMethod("observableVoid")); @@ -76,7 +77,7 @@ public class SimpleHandlerResultHandlerTests { GenericConversionService conversionService = new GenericConversionService(); conversionService.addConverter(new ReactiveStreamsToCompletableFutureConverter()); - conversionService.addConverter(new ReactiveStreamsToReactorStreamConverter()); + conversionService.addConverter(new ReactiveStreamsToReactorFluxionConverter()); conversionService.addConverter(new ReactiveStreamsToRxJava1Converter()); SimpleHandlerResultHandler resultHandler = new SimpleHandlerResultHandler(conversionService); TestController controller = new TestController(); @@ -125,7 +126,7 @@ public class SimpleHandlerResultHandlerTests { return null; } - public Stream streamVoid() { + public Fluxion streamVoid() { return null; } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java index 26e4c3ac36..ff628be7ef 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java @@ -26,8 +26,8 @@ import org.junit.Test; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.rx.Fluxion; import reactor.rx.Signal; -import reactor.rx.Stream; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; @@ -37,8 +37,8 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.method.annotation.RequestParamArgumentResolver; -import org.springframework.web.server.adapter.DefaultServerWebExchange; import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; import org.springframework.web.server.session.WebSessionManager; import static org.junit.Assert.assertEquals; @@ -73,7 +73,7 @@ public class InvocableHandlerMethodTests { InvocableHandlerMethod hm = createHandlerMethod("noArgs"); Publisher publisher = hm.invokeForRequest(this.exchange, this.model); - List values = Stream.from(publisher).toList().get(); + List values = Fluxion.from(publisher).toList().get(); assertEquals(1, values.size()); assertEquals("success", values.get(0).getReturnValue().get()); @@ -86,7 +86,7 @@ public class InvocableHandlerMethodTests { hm.setHandlerMethodArgumentResolvers(Collections.singletonList(new RequestParamArgumentResolver())); Publisher publisher = hm.invokeForRequest(this.exchange, this.model); - List values = Stream.from(publisher).toList().get(); + List values = Fluxion.from(publisher).toList().get(); assertEquals(1, values.size()); assertEquals("success:null", values.get(0).getReturnValue().get()); @@ -98,7 +98,7 @@ public class InvocableHandlerMethodTests { addResolver(hm, Mono.just("value1")); Publisher publisher = hm.invokeForRequest(this.exchange, this.model); - List values = Stream.from(publisher).toList().get(); + List values = Fluxion.from(publisher).toList().get(); assertEquals(1, values.size()); assertEquals("success:value1", values.get(0).getReturnValue().get()); @@ -110,7 +110,7 @@ public class InvocableHandlerMethodTests { addResolver(hm, Flux.fromIterable(Arrays.asList("value1", "value2", "value3"))); Publisher publisher = hm.invokeForRequest(this.exchange, this.model); - List values = Stream.from(publisher).toList().get(); + List values = Fluxion.from(publisher).toList().get(); assertEquals(1, values.size()); assertEquals("success:value1", values.get(0).getReturnValue().get()); @@ -200,7 +200,7 @@ public class InvocableHandlerMethodTests { } private Throwable awaitErrorSignal(Publisher publisher) throws Exception { - Signal signal = Stream.from(publisher).materialize().toList().get().get(0); + Signal signal = Fluxion.from(publisher).materialize().toList().get().get(0); assertEquals("Unexpected signal: " + signal, Signal.Type.ERROR, signal.getType()); return signal.getThrowable(); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java index aeeaa94f23..39b6057fe4 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java @@ -29,8 +29,8 @@ import org.junit.Test; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.rx.Fluxion; import reactor.rx.Promise; -import reactor.rx.Stream; import rx.Observable; import rx.Single; @@ -47,7 +47,7 @@ import org.springframework.core.codec.support.StringEncoder; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter; -import org.springframework.core.convert.support.ReactiveStreamsToReactorStreamConverter; +import org.springframework.core.convert.support.ReactiveStreamsToReactorFluxionConverter; import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferAllocator; @@ -398,7 +398,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati // TODO: test failures with DefaultConversionService GenericConversionService service = new GenericConversionService(); service.addConverter(new ReactiveStreamsToCompletableFutureConverter()); - service.addConverter(new ReactiveStreamsToReactorStreamConverter()); + service.addConverter(new ReactiveStreamsToReactorFluxionConverter()); service.addConverter(new ReactiveStreamsToRxJava1Converter()); return service; } @@ -491,7 +491,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @RequestMapping("/stream-result") public Publisher stringStreamResponseBody() { - return Flux.interval(1).as(Stream::from).take(5); + return Flux.interval(1).as(Fluxion::from).take(5); } @RequestMapping("/raw-flux") @@ -540,8 +540,8 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati } @RequestMapping("/stream") - public Stream reactorStreamResponseBody() { - return Stream.just(new Person("Robert"), new Person("Marie")); + public Fluxion reactorStreamResponseBody() { + return Fluxion.just(new Person("Robert"), new Person("Marie")); } @RequestMapping("/publisher-capitalize") @@ -562,7 +562,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati } @RequestMapping("/stream-capitalize") - public Stream streamCapitalize(@RequestBody Stream persons) { + public Fluxion streamCapitalize(@RequestBody Fluxion persons) { return persons.map(person -> new Person(person.getName().toUpperCase())); } @@ -589,7 +589,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @RequestMapping("/promise-capitalize") public Promise promiseCapitalize(@RequestBody Promise personFuture) { - return Stream.from(personFuture.map(person -> new Person(person.getName().toUpperCase()))).promise(); + return Fluxion.from(personFuture.map(person -> new Person(person.getName().toUpperCase()))).promise(); } @RequestMapping("/publisher-create") @@ -603,7 +603,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati } @RequestMapping("/stream-create") - public Publisher streamCreate(@RequestBody Stream personStream) { + public Publisher streamCreate(@RequestBody Fluxion personStream) { return personStream.toList().doOnSuccess(persons::addAll).after(); } From 4c477189a6fbaf1d39cc3fbc35cc7351e987cf57 Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Sat, 27 Feb 2016 11:06:07 +0000 Subject: [PATCH 231/498] revert module Reactor Fluxion to Reactor Stream --- spring-web-reactive/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index b75911785b..a6d4226ad6 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -88,7 +88,7 @@ dependencies { optional 'io.reactivex:rxjava:1.1.0' optional "io.reactivex:rxnetty-http:0.5.0-SNAPSHOT" optional "com.fasterxml.jackson.core:jackson-databind:2.6.2" - optional "io.projectreactor:reactor-fluxion:${reactorVersion}" + optional "io.projectreactor:reactor-stream:${reactorVersion}" optional "io.projectreactor:reactor-net:${reactorVersion}" optional "io.projectreactor:reactor-io:${reactorVersion}" optional "org.apache.tomcat:tomcat-util:${tomcatVersion}" From cc77a338b1270010212cefe6b6c1221aed08ec88 Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Sat, 27 Feb 2016 13:34:15 +0000 Subject: [PATCH 232/498] Sync reactor-core jdk8 bump --- .../core/codec/support/JsonObjectDecoder.java | 2 +- .../ReactiveStreamsToCompletableFutureConverter.java | 3 ++- .../http/server/reactive/WriteWithOperator.java | 6 ++++-- .../client/reactive/WebClientIntegrationTests.java | 11 +++++++---- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectDecoder.java index 2bdc000320..f9f92357ae 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectDecoder.java @@ -19,13 +19,13 @@ package org.springframework.core.codec.support; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; +import java.util.function.Function; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; -import reactor.fn.Function; import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.DataBuffer; diff --git a/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToCompletableFutureConverter.java b/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToCompletableFutureConverter.java index 692633ec03..a20c50e1c2 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToCompletableFutureConverter.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToCompletableFutureConverter.java @@ -21,6 +21,7 @@ import java.util.Set; import java.util.concurrent.CompletableFuture; import org.reactivestreams.Publisher; +import reactor.core.publisher.Mono; import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.GenericConverter; @@ -44,7 +45,7 @@ public class ReactiveStreamsToCompletableFutureConverter implements GenericConve return null; } else if (CompletableFuture.class.isAssignableFrom(source.getClass())) { - return reactor.core.converter.CompletableFutureConverter.from((CompletableFuture) source); + return Mono.fromCompletableFuture((CompletableFuture)source); } else if (CompletableFuture.class.isAssignableFrom(targetType.getResolvableType().getRawClass())) { return reactor.core.converter.CompletableFutureConverter.fromSingle((Publisher) source); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/WriteWithOperator.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/WriteWithOperator.java index 873a0e6407..f2c2354a75 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/WriteWithOperator.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/WriteWithOperator.java @@ -15,12 +15,13 @@ */ package org.springframework.http.server.reactive; +import java.util.function.Function; + import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import reactor.core.subscriber.SubscriberBarrier; import reactor.core.util.Assert; -import reactor.fn.Function; /** @@ -81,7 +82,8 @@ public class WriteWithOperator implements Function, @Override protected void doOnSubscribe(Subscription subscription) { super.doOnSubscribe(subscription); - ((Subscription) super.upstream()).request(1); // bypass doRequest + super.upstream() + .request(1); // bypass doRequest } @Override diff --git a/spring-web-reactive/src/test/java/org/springframework/web/client/reactive/WebClientIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/client/reactive/WebClientIntegrationTests.java index d77ac035dd..ac4bc368f5 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/client/reactive/WebClientIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/client/reactive/WebClientIntegrationTests.java @@ -16,9 +16,7 @@ package org.springframework.web.client.reactive; -import static org.junit.Assert.*; -import static org.springframework.web.client.reactive.HttpRequestBuilders.*; -import static org.springframework.web.client.reactive.WebResponseExtractors.*; +import java.util.function.Consumer; import okhttp3.HttpUrl; import okhttp3.mockwebserver.MockResponse; @@ -31,7 +29,6 @@ import org.junit.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.test.TestSubscriber; -import reactor.fn.Consumer; import org.springframework.core.codec.support.Pojo; import org.springframework.http.HttpHeaders; @@ -39,6 +36,12 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.http.client.reactive.ReactorHttpClientRequestFactory; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.springframework.web.client.reactive.HttpRequestBuilders.get; +import static org.springframework.web.client.reactive.HttpRequestBuilders.post; +import static org.springframework.web.client.reactive.WebResponseExtractors.*; + /** * @author Brian Clozel */ From 24d9e99de1468ddd1bad3230faf88d5cfab98f20 Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Sat, 27 Feb 2016 21:36:31 +0000 Subject: [PATCH 233/498] Sync toCompletableFuture --- .../support/ReactiveStreamsToCompletableFutureConverter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToCompletableFutureConverter.java b/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToCompletableFutureConverter.java index a20c50e1c2..707b5c8078 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToCompletableFutureConverter.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToCompletableFutureConverter.java @@ -48,7 +48,7 @@ public class ReactiveStreamsToCompletableFutureConverter implements GenericConve return Mono.fromCompletableFuture((CompletableFuture)source); } else if (CompletableFuture.class.isAssignableFrom(targetType.getResolvableType().getRawClass())) { - return reactor.core.converter.CompletableFutureConverter.fromSingle((Publisher) source); + return Mono.from((Publisher)source).toCompletableFuture(); } return null; } From 999dfe39253c12718ef6ed7414f4e8ad6684de43 Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Sat, 27 Feb 2016 23:18:22 +0000 Subject: [PATCH 234/498] Sync BaseSubscriber --- .../server/reactive/UndertowHttpHandlerAdapter.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java index c01e34d0d1..d391f0c5f0 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java @@ -320,8 +320,8 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle } } - private static class ResponseBodySubscriber extends BaseSubscriber - implements ChannelListener { + private static class ResponseBodySubscriber + implements ChannelListener, BaseSubscriber{ private final HttpServerExchange exchange; @@ -342,14 +342,14 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle @Override public void onSubscribe(Subscription subscription) { - super.onSubscribe(subscription); + BaseSubscriber.super.onSubscribe(subscription); this.subscription = subscription; this.subscription.request(1); } @Override public void onNext(DataBuffer dataBuffer) { - super.onNext(dataBuffer); + BaseSubscriber.super.onNext(dataBuffer); ByteBuffer buffer = dataBuffer.asByteBuffer(); @@ -439,7 +439,7 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle @Override public void onError(Throwable ex) { - super.onError(ex); + BaseSubscriber.super.onError(ex); logger.error("ResponseBodySubscriber error", ex); if (!exchange.isResponseStarted() && exchange.getStatusCode() < 500) { exchange.setStatusCode(500); @@ -448,7 +448,6 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle @Override public void onComplete() { - super.onComplete(); if (this.responseChannel != null) { this.closing.set(true); closeIfDone(); From fc1b2e96f791cf0536109a823c12594dad506e3c Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Sun, 28 Feb 2016 00:56:10 +0000 Subject: [PATCH 235/498] Introduce Mono#justOrEmpty(T) --- .../method/annotation/RequestParamArgumentResolver.java | 3 +-- .../web/reactive/view/ViewResolverResultHandlerTests.java | 7 ++----- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestParamArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestParamArgumentResolver.java index 0d0dff9b2a..8eceba031f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestParamArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestParamArgumentResolver.java @@ -46,8 +46,7 @@ public class RequestParamArgumentResolver implements HandlerMethodArgumentResolv RequestParam annotation = param.getParameterAnnotation(RequestParam.class); String name = (annotation.value().length() != 0 ? annotation.value() : param.getParameterName()); UriComponents uriComponents = UriComponentsBuilder.fromUri(exchange.getRequest().getURI()).build(); - String value = uriComponents.getQueryParams().getFirst(name); - return (value != null ? Mono.just(value) : Mono.empty()); + return Mono.justOrEmpty(uriComponents.getQueryParams().getFirst(name)); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/view/ViewResolverResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/view/ViewResolverResultHandlerTests.java index 36a787e349..0780ce5f35 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/view/ViewResolverResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/view/ViewResolverResultHandlerTests.java @@ -57,10 +57,7 @@ import org.springframework.web.server.session.DefaultWebSessionManager; import org.springframework.web.server.session.WebSessionManager; import static org.hamcrest.CoreMatchers.endsWith; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; import static org.mockito.Mockito.mock; @@ -226,7 +223,7 @@ public class ViewResolverResultHandlerTests { @Override public Mono resolveViewName(String viewName, Locale locale) { View view = this.views.get(viewName); - return (view != null ? Mono.just(view) : Mono.empty()); + return Mono.justOrEmpty(view); } } From 423a4852c57bbf9beb65835b35a9be296c92fb2f Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Sun, 28 Feb 2016 11:29:46 +0000 Subject: [PATCH 236/498] use EmptySubscription --- .../server/reactive/WriteWithOperator.java | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/WriteWithOperator.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/WriteWithOperator.java index f2c2354a75..72cd43cb57 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/WriteWithOperator.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/WriteWithOperator.java @@ -22,7 +22,7 @@ import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import reactor.core.subscriber.SubscriberBarrier; import reactor.core.util.Assert; - +import reactor.core.util.EmptySubscription; /** * Given a write function that accepts a source {@code Publisher} to write @@ -36,10 +36,10 @@ import reactor.core.util.Assert; */ public class WriteWithOperator implements Function, Subscriber> { - private final java.util.function.Function, Publisher> writeFunction; + private final Function, Publisher> writeFunction; - public WriteWithOperator(java.util.function.Function, Publisher> writeFunction) { + public WriteWithOperator(Function, Publisher> writeFunction) { this.writeFunction = writeFunction; } @@ -156,7 +156,7 @@ public class WriteWithOperator implements Function, this.writeSubscriber = writeSubscriber; if (this.error != null || this.completed) { - this.writeSubscriber.onSubscribe(NO_OP_SUBSCRIPTION); + this.writeSubscriber.onSubscribe(EmptySubscription.INSTANCE); emitCachedSignals(); } else { @@ -234,15 +234,4 @@ public class WriteWithOperator implements Function, } } - private final static Subscription NO_OP_SUBSCRIPTION = new Subscription() { - - @Override - public void request(long n) { - } - - @Override - public void cancel() { - } - }; - } From 4197f002d87c1c02455cc640e20f89b28b10d271 Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Sun, 28 Feb 2016 11:49:00 +0000 Subject: [PATCH 237/498] Arrange WriteWithOperator to save some allocation cost with the same technique used by RSC and Reactor --- .../reactive/AbstractServerHttpResponse.java | 7 ++---- .../server/reactive/WriteWithOperator.java | 16 +++++++----- .../annotation/ResponseBodyResultHandler.java | 2 +- .../reactive/WriteWithOperatorTests.java | 25 +++++++++++-------- 4 files changed, 27 insertions(+), 23 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java index bf79e54391..42dbf1c200 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java @@ -21,7 +21,6 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; import org.reactivestreams.Publisher; -import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.core.io.buffer.DataBuffer; @@ -58,10 +57,8 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse { @Override public Mono setBody(Publisher publisher) { - return Flux.from(publisher) - .lift(new WriteWithOperator<>(writePublisher -> - applyBeforeCommit().after(() -> setBodyInternal(writePublisher)))) - .after(); + return new WriteWithOperator<>(publisher, writePublisher -> + applyBeforeCommit().after(() -> setBodyInternal(writePublisher))); } private Mono applyBeforeCommit() { diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/WriteWithOperator.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/WriteWithOperator.java index 72cd43cb57..44d15830c8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/WriteWithOperator.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/WriteWithOperator.java @@ -20,10 +20,12 @@ import java.util.function.Function; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; +import reactor.core.publisher.MonoSource; import reactor.core.subscriber.SubscriberBarrier; -import reactor.core.util.Assert; import reactor.core.util.EmptySubscription; +import org.springframework.util.Assert; + /** * Given a write function that accepts a source {@code Publisher} to write * with and returns {@code Publisher} for the result, this operator helps @@ -33,22 +35,24 @@ import reactor.core.util.EmptySubscription; * through the result publisher. Otherwise the write function is invoked. * * @author Rossen Stoyanchev + * @author Stephane Maldini */ -public class WriteWithOperator implements Function, Subscriber> { +public class WriteWithOperator extends MonoSource { private final Function, Publisher> writeFunction; - public WriteWithOperator(Function, Publisher> writeFunction) { + public WriteWithOperator(Publisher source, + Function, Publisher> writeFunction) { + super(source); this.writeFunction = writeFunction; } @Override - public Subscriber apply(Subscriber subscriber) { - return new WriteWithBarrier(subscriber); + public void subscribe(Subscriber s) { + source.subscribe(new WriteWithBarrier(s)); } - private class WriteWithBarrier extends SubscriberBarrier implements Publisher { /** diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java index 8c0b57d969..0e5af148dd 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java @@ -145,7 +145,7 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered publisher = this.conversionService.convert(value.get(), Publisher.class); elementType = returnType.getGeneric(0); if (Void.class.equals(elementType.getRawClass())) { - return (Mono)Mono.from(publisher); + return Mono.from((Publisher)publisher); } } else { diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/WriteWithOperatorTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/WriteWithOperatorTests.java index 56a2e02655..9f6d8f9602 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/WriteWithOperatorTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/WriteWithOperatorTests.java @@ -28,6 +28,7 @@ import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import reactor.core.subscriber.SubscriberBarrier; import reactor.rx.Fluxion; import reactor.rx.Signal; @@ -36,25 +37,27 @@ import static org.junit.Assert.*; /** * @author Rossen Stoyanchev + * @author Stephane Maldini */ @SuppressWarnings("ThrowableResultOfMethodCallIgnored") public class WriteWithOperatorTests { private OneByOneAsyncWriter writer; - private WriteWithOperator operator; - @Before public void setUp() throws Exception { this.writer = new OneByOneAsyncWriter(); - this.operator = new WriteWithOperator<>(this.writer::writeWith); + } + + private Mono writeWithOperator(Publisher source){ + return new WriteWithOperator<>(source, writer::writeWith); } @Test public void errorBeforeFirstItem() throws Exception { IllegalStateException error = new IllegalStateException("boo"); - Publisher completion = Flux.error(error).lift(this.operator); + Mono completion = Mono.error(error).as(this::writeWithOperator); List> signals = Fluxion.from(completion).materialize().toList().get(); assertEquals(1, signals.size()); @@ -63,7 +66,7 @@ public class WriteWithOperatorTests { @Test public void completionBeforeFirstItem() throws Exception { - Publisher completion = Flux.empty().lift(this.operator); + Mono completion = Flux.empty().as(this::writeWithOperator); List> signals = Fluxion.from(completion).materialize().toList().get(); assertEquals(1, signals.size()); @@ -75,8 +78,8 @@ public class WriteWithOperatorTests { @Test public void writeOneItem() throws Exception { - Publisher completion = Flux.just("one").lift(this.operator); - List> signals = Fluxion.from(completion).materialize().toList().get(); + Mono completion = Flux.just("one").as(this::writeWithOperator); + List> signals =completion.as(Fluxion::from).materialize().toList().get(); assertEquals(1, signals.size()); assertTrue("Unexpected signal: " + signals.get(0), signals.get(0).isOnComplete()); @@ -90,8 +93,8 @@ public class WriteWithOperatorTests { @Test public void writeMultipleItems() throws Exception { List items = Arrays.asList("one", "two", "three"); - Publisher completion = Flux.fromIterable(items).lift(this.operator); - List> signals = Fluxion.from(completion).materialize().toList().get(); + Mono completion = Flux.fromIterable(items).as(this::writeWithOperator); + List> signals = completion.as(Fluxion::from).materialize().toList().get(); assertEquals(1, signals.size()); assertTrue("Unexpected signal: " + signals.get(0), signals.get(0).isOnComplete()); @@ -113,8 +116,8 @@ public class WriteWithOperatorTests { subscriber.onError(error); } }, subscriber -> new AtomicInteger()); - Publisher completion = publisher.lift(this.operator); - List> signals = Fluxion.from(completion).materialize().toList().get(); + Mono completion = publisher.as(this::writeWithOperator); + List> signals = completion.as(Fluxion::from).materialize().toList().get(); assertEquals(1, signals.size()); assertSame("Unexpected signal: " + signals.get(0), error, signals.get(0).getThrowable()); From 14948d4241f60f0ba43b43ecfd6aa49af2f41665 Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Tue, 1 Mar 2016 20:50:55 +0000 Subject: [PATCH 238/498] sync reactor --- .../http/server/reactive/AsyncIntegrationTests.java | 2 +- .../web/reactive/view/ViewResolverResultHandlerTests.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java index 5c121a2ecf..a0efb8968b 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java @@ -65,7 +65,7 @@ public class AsyncIntegrationTests extends AbstractHttpHandlerIntegrationTests { public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { return response.setBody(Fluxion.just("h", "e", "l", "l", "o") .useTimer(Timer.global()) - .throttleRequest(100) + .delay(1) .dispatchOn(asyncGroup) .collect(allocator::allocateBuffer, (buffer, str) -> buffer.write(str.getBytes()))); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/view/ViewResolverResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/view/ViewResolverResultHandlerTests.java index 0780ce5f35..6dd15aa541 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/view/ViewResolverResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/view/ViewResolverResultHandlerTests.java @@ -19,6 +19,7 @@ import java.lang.reflect.Method; import java.net.URI; import java.nio.ByteBuffer; import java.nio.charset.Charset; +import java.time.Duration; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -26,7 +27,6 @@ import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Optional; -import java.util.concurrent.TimeUnit; import org.junit.Before; import org.junit.Test; @@ -194,7 +194,7 @@ public class ViewResolverResultHandlerTests { HandlerResult result = new HandlerResult(new Object(), value, type, this.model); Mono mono = handler.handleResult(this.exchange, result); TestSubscriber subscriber = new TestSubscriber<>(); - return subscriber.bindTo(mono).await(1, TimeUnit.SECONDS); + return subscriber.bindTo(mono).await(Duration.ofSeconds(1)); } private static DataBuffer asDataBuffer(String value) { From dc52d4c31f03621c86f5ede93eee5542b81f625a Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Tue, 1 Mar 2016 22:07:54 +0100 Subject: [PATCH 239/498] Update for latest Reactor Core Snapshot --- .../client/reactive/WebClientIntegrationTests.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/web/client/reactive/WebClientIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/client/reactive/WebClientIntegrationTests.java index ac4bc368f5..24017c1b8f 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/client/reactive/WebClientIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/client/reactive/WebClientIntegrationTests.java @@ -69,7 +69,7 @@ public class WebClientIntegrationTests { TestSubscriber ts = new TestSubscriber(); result.subscribe(ts); - ts.awaitAndAssertValuesWith( + ts.awaitAndAssertNextValuesWith( httpHeaders -> { assertEquals(MediaType.TEXT_PLAIN, httpHeaders.getContentType()); assertEquals(13L, httpHeaders.getContentLength()); @@ -95,7 +95,7 @@ public class WebClientIntegrationTests { TestSubscriber ts = new TestSubscriber(); result.subscribe(ts); - ts.awaitAndAssertValues("Hello Spring!").assertComplete(); + ts.awaitAndAssertNextValues("Hello Spring!").assertComplete(); RecordedRequest request = server.takeRequest(); assertEquals("testvalue", request.getHeader("X-Test-Header")); @@ -116,7 +116,7 @@ public class WebClientIntegrationTests { TestSubscriber> ts = new TestSubscriber(); result.subscribe(ts); - ts.awaitAndAssertValuesWith(new Consumer>() { + ts.awaitAndAssertNextValuesWith(new Consumer>() { @Override public void accept(ResponseEntity response) { assertEquals(200, response.getStatusCode().value()); @@ -143,7 +143,7 @@ public class WebClientIntegrationTests { TestSubscriber ts = new TestSubscriber(); result.subscribe(ts); - ts.awaitAndAssertValuesWith(p -> assertEquals("barbar", p.getBar())).assertComplete(); + ts.awaitAndAssertNextValuesWith(p -> assertEquals("barbar", p.getBar())).assertComplete(); RecordedRequest request = server.takeRequest(); assertEquals("/pojo", request.getPath()); assertEquals("application/json", request.getHeader(HttpHeaders.ACCEPT)); @@ -163,7 +163,7 @@ public class WebClientIntegrationTests { TestSubscriber ts = new TestSubscriber(); result.subscribe(ts); - ts.awaitAndAssertValuesWith( + ts.awaitAndAssertNextValuesWith( p -> assertThat(p.getBar(), Matchers.is("bar1")), p -> assertThat(p.getBar(), Matchers.is("bar2")) ).assertValueCount(2).assertComplete(); @@ -186,7 +186,7 @@ public class WebClientIntegrationTests { TestSubscriber>> ts = new TestSubscriber(); result.subscribe(ts); - ts.awaitAndAssertValuesWith( + ts.awaitAndAssertNextValuesWith( response -> { assertEquals(200, response.getStatusCode().value()); assertEquals(MediaType.APPLICATION_JSON, response.getHeaders().getContentType()); @@ -213,7 +213,7 @@ public class WebClientIntegrationTests { TestSubscriber ts = new TestSubscriber(); result.subscribe(ts); - ts.awaitAndAssertValuesWith(p -> assertEquals("BARBAR", p.getBar())).assertComplete(); + ts.awaitAndAssertNextValuesWith(p -> assertEquals("BARBAR", p.getBar())).assertComplete(); RecordedRequest request = server.takeRequest(); assertEquals("/pojo/capitalize", request.getPath()); From 379ce6354e9af061123cc75177b0c79cc0b6bf6b Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Tue, 1 Mar 2016 22:19:54 +0100 Subject: [PATCH 240/498] Update for latest Reactor Core Snapshot --- .../http/server/reactive/AsyncIntegrationTests.java | 3 ++- .../method/annotation/RequestMappingIntegrationTests.java | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java index a0efb8968b..8b36948a9c 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java @@ -17,6 +17,7 @@ package org.springframework.http.server.reactive; import java.net.URI; +import java.time.Duration; import org.hamcrest.Matchers; import org.junit.Test; @@ -65,7 +66,7 @@ public class AsyncIntegrationTests extends AbstractHttpHandlerIntegrationTests { public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { return response.setBody(Fluxion.just("h", "e", "l", "l", "o") .useTimer(Timer.global()) - .delay(1) + .delay(Duration.ofMillis(100)) .dispatchOn(asyncGroup) .collect(allocator::allocateBuffer, (buffer, str) -> buffer.write(str.getBytes()))); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java index 39b6057fe4..5e06c13dca 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java @@ -18,6 +18,7 @@ package org.springframework.web.reactive.method.annotation; import java.net.URI; import java.nio.ByteBuffer; +import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -491,7 +492,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @RequestMapping("/stream-result") public Publisher stringStreamResponseBody() { - return Flux.interval(1).as(Fluxion::from).take(5); + return Flux.interval(Duration.ofSeconds(1)).as(Fluxion::from).take(5); } @RequestMapping("/raw-flux") From ce4a687cf24385a4b01f9f117419981a87bba666 Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Thu, 3 Mar 2016 23:57:03 +0000 Subject: [PATCH 241/498] remove reactor-stream and related artefacts, update tests --- spring-web-reactive/build.gradle | 9 ++- ...ctiveStreamsToReactorFluxionConverter.java | 65 ------------------- .../reactive/AsyncIntegrationTests.java | 12 ++-- .../reactive/WriteWithOperatorTests.java | 33 +++++----- .../reactive/DispatcherHandlerErrorTests.java | 32 ++++----- .../ResponseStatusExceptionHandlerTests.java | 20 +++--- .../SimpleHandlerResultHandlerTests.java | 6 +- .../method/InvocableHandlerMethodTests.java | 42 ++++++------ .../RequestMappingIntegrationTests.java | 51 ++------------- 9 files changed, 80 insertions(+), 190 deletions(-) delete mode 100644 spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToReactorFluxionConverter.java diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index a6d4226ad6..c96b0a7754 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -19,10 +19,10 @@ jar { group = 'org.springframework.reactive' repositories { - mavenCentral() - maven { url 'https://oss.jfrog.org/libs-snapshot' } // RxNetty 0.5.x snapshots - maven { url 'http://repo.spring.io/snapshot' } // Reactor snapshot - mavenLocal() + mavenLocal() + mavenCentral() + maven { url 'https://oss.jfrog.org/libs-snapshot' } // RxNetty 0.5.x snapshots + maven { url 'http://repo.spring.io/snapshot' } // Reactor snapshot } configurations.all { @@ -88,7 +88,6 @@ dependencies { optional 'io.reactivex:rxjava:1.1.0' optional "io.reactivex:rxnetty-http:0.5.0-SNAPSHOT" optional "com.fasterxml.jackson.core:jackson-databind:2.6.2" - optional "io.projectreactor:reactor-stream:${reactorVersion}" optional "io.projectreactor:reactor-net:${reactorVersion}" optional "io.projectreactor:reactor-io:${reactorVersion}" optional "org.apache.tomcat:tomcat-util:${tomcatVersion}" diff --git a/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToReactorFluxionConverter.java b/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToReactorFluxionConverter.java deleted file mode 100644 index 8540be91bf..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToReactorFluxionConverter.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2002-2015 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.convert.support; - -import java.util.LinkedHashSet; -import java.util.Set; - -import org.reactivestreams.Publisher; -import reactor.rx.Fluxion; -import reactor.rx.Promise; - -import org.springframework.core.convert.TypeDescriptor; -import org.springframework.core.convert.converter.GenericConverter; - -/** - * @author Stephane Maldini - * @author Sebastien Deleuze - */ -public final class ReactiveStreamsToReactorFluxionConverter implements GenericConverter { - - @Override - public Set getConvertibleTypes() { - Set pairs = new LinkedHashSet<>(); - pairs.add(new GenericConverter.ConvertiblePair(Publisher.class, Fluxion.class)); - pairs.add(new GenericConverter.ConvertiblePair(Fluxion.class, Publisher.class)); - pairs.add(new GenericConverter.ConvertiblePair(Publisher.class, Promise.class)); - pairs.add(new GenericConverter.ConvertiblePair(Promise.class, Publisher.class)); - return pairs; - } - - @Override - public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { - if (source == null) { - return null; - } - if (Fluxion.class.isAssignableFrom(source.getClass())) { - return source; - } - else if (Fluxion.class.isAssignableFrom(targetType.getResolvableType().getRawClass())) { - return Fluxion.from((Publisher)source); - } - else if (Promise.class.isAssignableFrom(source.getClass())) { - return source; - } - else if (Promise.class.isAssignableFrom(targetType.getResolvableType().getRawClass())) { - return Fluxion.from((Publisher)source).promise(); - } - return null; - } - -} diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java index 8b36948a9c..af58c684e1 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java @@ -21,10 +21,10 @@ import java.time.Duration; import org.hamcrest.Matchers; import org.junit.Test; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.SchedulerGroup; import reactor.core.timer.Timer; -import reactor.rx.Fluxion; import org.springframework.core.io.buffer.DataBufferAllocator; import org.springframework.core.io.buffer.DefaultDataBufferAllocator; @@ -64,11 +64,11 @@ public class AsyncIntegrationTests extends AbstractHttpHandlerIntegrationTests { @Override public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { - return response.setBody(Fluxion.just("h", "e", "l", "l", "o") - .useTimer(Timer.global()) - .delay(Duration.ofMillis(100)) - .dispatchOn(asyncGroup) - .collect(allocator::allocateBuffer, + return response.setBody(Flux.just("h", "e", "l", "l", "o") + .useTimer(Timer.global()) + .delay(Duration.ofMillis(100)) + .dispatchOn(asyncGroup) + .collect(allocator::allocateBuffer, (buffer, str) -> buffer.write(str.getBytes()))); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/WriteWithOperatorTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/WriteWithOperatorTests.java index 9f6d8f9602..965d6a94e2 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/WriteWithOperatorTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/WriteWithOperatorTests.java @@ -29,9 +29,8 @@ import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.core.publisher.Signal; import reactor.core.subscriber.SubscriberBarrier; -import reactor.rx.Fluxion; -import reactor.rx.Signal; import static org.junit.Assert.*; @@ -58,19 +57,19 @@ public class WriteWithOperatorTests { public void errorBeforeFirstItem() throws Exception { IllegalStateException error = new IllegalStateException("boo"); Mono completion = Mono.error(error).as(this::writeWithOperator); - List> signals = Fluxion.from(completion).materialize().toList().get(); + Signal signal = completion.materialize().get(); - assertEquals(1, signals.size()); - assertSame("Unexpected signal: " + signals.get(0), error, signals.get(0).getThrowable()); + assertNotNull(signal); + assertSame("Unexpected signal: " + signal, error, signal.getThrowable()); } @Test public void completionBeforeFirstItem() throws Exception { Mono completion = Flux.empty().as(this::writeWithOperator); - List> signals = Fluxion.from(completion).materialize().toList().get(); + Signal signal = completion.materialize().get(); - assertEquals(1, signals.size()); - assertTrue("Unexpected signal: " + signals.get(0), signals.get(0).isOnComplete()); + assertNotNull(signal); + assertTrue("Unexpected signal: " + signal, signal.isOnComplete()); assertEquals(0, this.writer.items.size()); assertTrue(this.writer.completed); @@ -79,10 +78,10 @@ public class WriteWithOperatorTests { @Test public void writeOneItem() throws Exception { Mono completion = Flux.just("one").as(this::writeWithOperator); - List> signals =completion.as(Fluxion::from).materialize().toList().get(); + Signal signal = completion.materialize().get(); - assertEquals(1, signals.size()); - assertTrue("Unexpected signal: " + signals.get(0), signals.get(0).isOnComplete()); + assertNotNull(signal); + assertTrue("Unexpected signal: " + signal, signal.isOnComplete()); assertEquals(1, this.writer.items.size()); assertEquals("one", this.writer.items.get(0)); @@ -94,10 +93,10 @@ public class WriteWithOperatorTests { public void writeMultipleItems() throws Exception { List items = Arrays.asList("one", "two", "three"); Mono completion = Flux.fromIterable(items).as(this::writeWithOperator); - List> signals = completion.as(Fluxion::from).materialize().toList().get(); + Signal signal = completion.materialize().get(); - assertEquals(1, signals.size()); - assertTrue("Unexpected signal: " + signals.get(0), signals.get(0).isOnComplete()); + assertNotNull(signal); + assertTrue("Unexpected signal: " + signal, signal.isOnComplete()); assertEquals(3, this.writer.items.size()); assertEquals("one", this.writer.items.get(0)); @@ -117,10 +116,10 @@ public class WriteWithOperatorTests { } }, subscriber -> new AtomicInteger()); Mono completion = publisher.as(this::writeWithOperator); - List> signals = completion.as(Fluxion::from).materialize().toList().get(); + Signal signal = completion.materialize().get(); - assertEquals(1, signals.size()); - assertSame("Unexpected signal: " + signals.get(0), error, signals.get(0).getThrowable()); + assertNotNull(signal); + assertSame("Unexpected signal: " + signal, error, signal.getThrowable()); assertEquals(3, this.writer.items.size()); assertEquals("1", this.writer.items.get(0)); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java index 843ca5c567..21adec20d7 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java @@ -24,8 +24,8 @@ import org.junit.Before; import org.junit.Test; import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; -import reactor.rx.Fluxion; -import reactor.rx.Signal; +import reactor.core.publisher.Signal; +import reactor.core.util.SignalKind; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; @@ -105,7 +105,7 @@ public class DispatcherHandlerErrorTests { public void noHandler() throws Exception { this.request.setUri(new URI("/does-not-exist")); - Publisher publisher = this.dispatcherHandler.handle(this.exchange); + Mono publisher = this.dispatcherHandler.handle(this.exchange); Throwable ex = awaitErrorSignal(publisher); assertEquals(ResponseStatusException.class, ex.getClass()); @@ -117,7 +117,7 @@ public class DispatcherHandlerErrorTests { public void noResolverForArgument() throws Exception { this.request.setUri(new URI("/unknown-argument-type")); - Publisher publisher = this.dispatcherHandler.handle(this.exchange); + Mono publisher = this.dispatcherHandler.handle(this.exchange); Throwable ex = awaitErrorSignal(publisher); assertEquals(IllegalStateException.class, ex.getClass()); @@ -128,7 +128,7 @@ public class DispatcherHandlerErrorTests { public void controllerMethodError() throws Exception { this.request.setUri(new URI("/error-signal")); - Publisher publisher = this.dispatcherHandler.handle(this.exchange); + Mono publisher = this.dispatcherHandler.handle(this.exchange); Throwable ex = awaitErrorSignal(publisher); assertSame(EXCEPTION, ex); @@ -138,7 +138,7 @@ public class DispatcherHandlerErrorTests { public void controllerMethodWithThrownException() throws Exception { this.request.setUri(new URI("/raise-exception")); - Publisher publisher = this.dispatcherHandler.handle(this.exchange); + Mono publisher = this.dispatcherHandler.handle(this.exchange); Throwable ex = awaitErrorSignal(publisher); assertSame(EXCEPTION, ex); @@ -148,7 +148,7 @@ public class DispatcherHandlerErrorTests { public void noHandlerResultHandler() throws Exception { this.request.setUri(new URI("/unknown-return-type")); - Publisher publisher = this.dispatcherHandler.handle(this.exchange); + Mono publisher = this.dispatcherHandler.handle(this.exchange); Throwable ex = awaitErrorSignal(publisher); assertEquals(IllegalStateException.class, ex.getClass()); @@ -163,7 +163,7 @@ public class DispatcherHandlerErrorTests { .write("body".getBytes("UTF-8")); this.request.setBody(Mono.just(buffer)); - Publisher publisher = this.dispatcherHandler.handle(this.exchange); + Mono publisher = this.dispatcherHandler.handle(this.exchange); Throwable ex = awaitErrorSignal(publisher); assertEquals(ResponseStatusException.class, ex.getClass()); @@ -176,7 +176,7 @@ public class DispatcherHandlerErrorTests { this.request.setUri(new URI("/request-body")); this.request.setBody(Mono.error(EXCEPTION)); - Publisher publisher = this.dispatcherHandler.handle(this.exchange); + Mono publisher = this.dispatcherHandler.handle(this.exchange); Throwable ex = awaitErrorSignal(publisher); ex.printStackTrace(); @@ -190,9 +190,9 @@ public class DispatcherHandlerErrorTests { WebExceptionHandler exceptionHandler = new ServerError500ExceptionHandler(); WebHandler webHandler = new ExceptionHandlingWebHandler(this.dispatcherHandler, exceptionHandler); - Publisher publisher = webHandler.handle(this.exchange); + Mono publisher = webHandler.handle(this.exchange); - Fluxion.from(publisher).toList().get(); + publisher.get(); assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, this.response.getStatus()); } @@ -202,16 +202,16 @@ public class DispatcherHandlerErrorTests { WebHandler webHandler = new FilteringWebHandler(this.dispatcherHandler, new TestWebFilter()); webHandler = new ExceptionHandlingWebHandler(webHandler, new ServerError500ExceptionHandler()); - Publisher publisher = webHandler.handle(this.exchange); + Mono publisher = webHandler.handle(this.exchange); - Fluxion.from(publisher).toList().get(); + publisher.get(); assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, this.response.getStatus()); } - private Throwable awaitErrorSignal(Publisher publisher) throws Exception { - Signal signal = Fluxion.from(publisher).materialize().toList().get().get(0); - assertEquals("Unexpected signal: " + signal, Signal.Type.ERROR, signal.getType()); + private Throwable awaitErrorSignal(Mono mono) throws Exception { + Signal signal = mono.materialize().get(); + assertEquals("Unexpected signal: " + signal, SignalKind.onError, signal.getType()); return signal.getThrowable(); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java index bc64ab2277..37866c5311 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java @@ -16,13 +16,11 @@ package org.springframework.web.reactive; import java.net.URI; -import java.util.List; import org.junit.Before; import org.junit.Test; -import org.reactivestreams.Publisher; -import reactor.rx.Fluxion; -import reactor.rx.Signal; +import reactor.core.publisher.Mono; +import reactor.core.publisher.Signal; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; @@ -61,21 +59,21 @@ public class ResponseStatusExceptionHandlerTests { @Test public void handleException() throws Exception { Throwable ex = new ResponseStatusException(HttpStatus.BAD_REQUEST); - Publisher publisher = this.handler.handle(this.exchange, ex); + Mono publisher = this.handler.handle(this.exchange, ex); - Fluxion.from(publisher).toList().get(); + publisher.get(); assertEquals(HttpStatus.BAD_REQUEST, this.response.getStatus()); } @Test public void unresolvedException() throws Exception { Throwable ex = new IllegalStateException(); - Publisher publisher = this.handler.handle(this.exchange, ex); + Mono publisher = this.handler.handle(this.exchange, ex); - List> signals = Fluxion.from(publisher).materialize().toList().get(); - assertEquals(1, signals.size()); - assertTrue(signals.get(0).hasError()); - assertSame(ex, signals.get(0).getThrowable()); + Signal signal = publisher.materialize().get(); + assertNotNull(signal); + assertTrue(signal.hasError()); + assertSame(ex, signal.getThrowable()); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandlerTests.java index a0ba187223..eb540c4a4b 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandlerTests.java @@ -20,13 +20,12 @@ import java.util.concurrent.CompletableFuture; import org.junit.Test; import org.reactivestreams.Publisher; -import reactor.rx.Fluxion; +import reactor.core.publisher.Flux; import rx.Observable; import org.springframework.core.ResolvableType; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter; -import org.springframework.core.convert.support.ReactiveStreamsToReactorFluxionConverter; import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter; import org.springframework.ui.ExtendedModelMap; import org.springframework.web.method.HandlerMethod; @@ -77,7 +76,6 @@ public class SimpleHandlerResultHandlerTests { GenericConversionService conversionService = new GenericConversionService(); conversionService.addConverter(new ReactiveStreamsToCompletableFutureConverter()); - conversionService.addConverter(new ReactiveStreamsToReactorFluxionConverter()); conversionService.addConverter(new ReactiveStreamsToRxJava1Converter()); SimpleHandlerResultHandler resultHandler = new SimpleHandlerResultHandler(conversionService); TestController controller = new TestController(); @@ -126,7 +124,7 @@ public class SimpleHandlerResultHandlerTests { return null; } - public Fluxion streamVoid() { + public Flux streamVoid() { return null; } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java index ff628be7ef..cf4b5da07a 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java @@ -19,15 +19,14 @@ import java.lang.reflect.Method; import java.net.URI; import java.util.Arrays; import java.util.Collections; -import java.util.List; import org.junit.Before; import org.junit.Test; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import reactor.rx.Fluxion; -import reactor.rx.Signal; +import reactor.core.publisher.Signal; +import reactor.core.util.SignalKind; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; @@ -42,6 +41,7 @@ import org.springframework.web.server.adapter.DefaultServerWebExchange; import org.springframework.web.server.session.WebSessionManager; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -72,11 +72,11 @@ public class InvocableHandlerMethodTests { public void noArgsMethod() throws Exception { InvocableHandlerMethod hm = createHandlerMethod("noArgs"); - Publisher publisher = hm.invokeForRequest(this.exchange, this.model); - List values = Fluxion.from(publisher).toList().get(); + Mono mono = hm.invokeForRequest(this.exchange, this.model); + HandlerResult value = mono.get(); - assertEquals(1, values.size()); - assertEquals("success", values.get(0).getReturnValue().get()); + assertNotNull(value); + assertEquals("success", value.getReturnValue().get()); } @Test @@ -85,11 +85,11 @@ public class InvocableHandlerMethodTests { InvocableHandlerMethod hm = createHandlerMethod("singleArg", String.class); hm.setHandlerMethodArgumentResolvers(Collections.singletonList(new RequestParamArgumentResolver())); - Publisher publisher = hm.invokeForRequest(this.exchange, this.model); - List values = Fluxion.from(publisher).toList().get(); + Mono mono = hm.invokeForRequest(this.exchange, this.model); + HandlerResult value = mono.get(); - assertEquals(1, values.size()); - assertEquals("success:null", values.get(0).getReturnValue().get()); + assertNotNull(value); + assertEquals("success:null", value.getReturnValue().get()); } @Test @@ -97,11 +97,11 @@ public class InvocableHandlerMethodTests { InvocableHandlerMethod hm = createHandlerMethod("singleArg", String.class); addResolver(hm, Mono.just("value1")); - Publisher publisher = hm.invokeForRequest(this.exchange, this.model); - List values = Fluxion.from(publisher).toList().get(); + Mono mono = hm.invokeForRequest(this.exchange, this.model); + HandlerResult value = mono.get(); - assertEquals(1, values.size()); - assertEquals("success:value1", values.get(0).getReturnValue().get()); + assertNotNull(value); + assertEquals("success:value1", value.getReturnValue().get()); } @Test @@ -109,11 +109,11 @@ public class InvocableHandlerMethodTests { InvocableHandlerMethod hm = createHandlerMethod("singleArg", String.class); addResolver(hm, Flux.fromIterable(Arrays.asList("value1", "value2", "value3"))); - Publisher publisher = hm.invokeForRequest(this.exchange, this.model); - List values = Fluxion.from(publisher).toList().get(); + Mono mono = hm.invokeForRequest(this.exchange, this.model); + HandlerResult value = mono.get(); - assertEquals(1, values.size()); - assertEquals("success:value1", values.get(0).getReturnValue().get()); + assertNotNull(value); + assertEquals("success:value1", value.getReturnValue().get()); } @Test @@ -200,8 +200,8 @@ public class InvocableHandlerMethodTests { } private Throwable awaitErrorSignal(Publisher publisher) throws Exception { - Signal signal = Fluxion.from(publisher).materialize().toList().get().get(0); - assertEquals("Unexpected signal: " + signal, Signal.Type.ERROR, signal.getType()); + Signal signal = Flux.from(publisher).materialize().toList().get().get(0); + assertEquals("Unexpected signal: " + signal, SignalKind.onError, signal.getType()); return signal.getThrowable(); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java index 5e06c13dca..3abde708ed 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java @@ -30,8 +30,6 @@ import org.junit.Test; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import reactor.rx.Fluxion; -import reactor.rx.Promise; import rx.Observable; import rx.Single; @@ -48,7 +46,6 @@ import org.springframework.core.codec.support.StringEncoder; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter; -import org.springframework.core.convert.support.ReactiveStreamsToReactorFluxionConverter; import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferAllocator; @@ -205,11 +202,6 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati serializeAsPojo("http://localhost:" + port + "/single"); } - @Test - public void serializeAsPromise() throws Exception { - serializeAsPojo("http://localhost:" + port + "/promise"); - } - @Test public void serializeAsList() throws Exception { serializeAsCollection("http://localhost:" + port + "/list"); @@ -250,11 +242,6 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati capitalizeCollection("http://localhost:" + port + "/observable-capitalize"); } - @Test - public void streamCapitalize() throws Exception { - capitalizeCollection("http://localhost:" + port + "/stream-capitalize"); - } - @Test public void personCapitalize() throws Exception { capitalizePojo("http://localhost:" + port + "/person-capitalize"); @@ -275,11 +262,6 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati capitalizePojo("http://localhost:" + port + "/single-capitalize"); } - @Test - public void promiseCapitalize() throws Exception { - capitalizePojo("http://localhost:" + this.port + "/promise-capitalize"); - } - @Test public void publisherCreate() throws Exception { create("http://localhost:" + this.port + "/publisher-create"); @@ -290,11 +272,6 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati create("http://localhost:" + this.port + "/flux-create"); } - @Test - public void streamCreate() throws Exception { - create("http://localhost:" + this.port + "/stream-create"); - } - @Test public void observableCreate() throws Exception { create("http://localhost:" + this.port + "/observable-create"); @@ -399,7 +376,6 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati // TODO: test failures with DefaultConversionService GenericConversionService service = new GenericConversionService(); service.addConverter(new ReactiveStreamsToCompletableFutureConverter()); - service.addConverter(new ReactiveStreamsToReactorFluxionConverter()); service.addConverter(new ReactiveStreamsToRxJava1Converter()); return service; } @@ -492,7 +468,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @RequestMapping("/stream-result") public Publisher stringStreamResponseBody() { - return Flux.interval(Duration.ofSeconds(1)).as(Fluxion::from).take(5); + return Flux.interval(Duration.ofSeconds(1)).take(5); } @RequestMapping("/raw-flux") @@ -515,11 +491,6 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati return Single.just(new Person("Robert")); } - @RequestMapping("/promise") - public Promise promiseResponseBody() { - return Promise.success(new Person("Robert")); - } - @RequestMapping("/list") public List listResponseBody() { return Arrays.asList(new Person("Robert"), new Person("Marie")); @@ -541,8 +512,8 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati } @RequestMapping("/stream") - public Fluxion reactorStreamResponseBody() { - return Fluxion.just(new Person("Robert"), new Person("Marie")); + public Flux reactorStreamResponseBody() { + return Flux.just(new Person("Robert"), new Person("Marie")); } @RequestMapping("/publisher-capitalize") @@ -562,9 +533,9 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati return persons.map(person -> new Person(person.getName().toUpperCase())); } - @RequestMapping("/stream-capitalize") - public Fluxion streamCapitalize(@RequestBody Fluxion persons) { - return persons.map(person -> new Person(person.getName().toUpperCase())); + @RequestMapping("/stream-create") + public Publisher streamCreate(@RequestBody Flux personStream) { + return personStream.toList().doOnSuccess(persons::addAll).after(); } @RequestMapping("/person-capitalize") @@ -588,11 +559,6 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati return personFuture.map(person -> new Person(person.getName().toUpperCase())); } - @RequestMapping("/promise-capitalize") - public Promise promiseCapitalize(@RequestBody Promise personFuture) { - return Fluxion.from(personFuture.map(person -> new Person(person.getName().toUpperCase()))).promise(); - } - @RequestMapping("/publisher-create") public Publisher publisherCreate(@RequestBody Publisher personStream) { return Flux.from(personStream).doOnNext(persons::add).after(); @@ -603,11 +569,6 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati return personStream.doOnNext(persons::add).after(); } - @RequestMapping("/stream-create") - public Publisher streamCreate(@RequestBody Fluxion personStream) { - return personStream.toList().doOnSuccess(persons::addAll).after(); - } - @RequestMapping("/observable-create") public Observable observableCreate(@RequestBody Observable personStream) { return personStream.toList().doOnNext(persons::addAll).flatMap(document -> Observable.empty()); From a8c777b35facc210caf8bf32d19f01f296aa9bea Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Fri, 4 Mar 2016 00:05:35 +0000 Subject: [PATCH 242/498] Remove reference to Fluxion --- .../web/reactive/handler/SimpleHandlerResultHandlerTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandlerTests.java index eb540c4a4b..0056fc6858 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandlerTests.java @@ -59,7 +59,7 @@ public class SimpleHandlerResultHandlerTests { hm = new HandlerMethod(controller, TestController.class.getMethod("streamVoid")); type = ResolvableType.forMethodParameter(hm.getReturnType()); - // Reactor Fluxion is a Publisher + // Reactor Flux is a Publisher assertTrue(resultHandler.supports(createHandlerResult(hm, type))); hm = new HandlerMethod(controller, TestController.class.getMethod("observableVoid")); From dbc6478b8ab30513b2c65b8e0a54c6214aec6656 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 3 Mar 2016 21:30:06 -0500 Subject: [PATCH 243/498] Refactor HTTP Cookie support There is now an HttpCookie (simple name-value pair) and separately a ServerHttpCookie sub-class with additional attributes that a server can send to clients. HttpHeaders is no longer the place to access cookies. Instead there is a read-only HttpCookie map on ServerHttpRequest and a mutable ServerHttpCookie map on ServerHttpResponse. Cookies are stored in a map that preserves their order. --- .../org/springframework/http/HttpCookie.java | 200 +-- .../org/springframework/http/HttpHeaders.java | 1158 ----------------- .../http/ServerHttpCookie.java | 228 ++++ .../reactive/AbstractServerHttpRequest.java | 97 +- .../reactive/AbstractServerHttpResponse.java | 17 + .../reactive/ReactorServerHttpRequest.java | 14 +- .../reactive/ReactorServerHttpResponse.java | 21 +- .../reactive/RxNettyServerHttpRequest.java | 14 +- .../reactive/RxNettyServerHttpResponse.java | 14 +- .../server/reactive/ServerHttpRequest.java | 10 + .../server/reactive/ServerHttpResponse.java | 11 + .../reactive/ServletServerHttpRequest.java | 13 +- .../reactive/ServletServerHttpResponse.java | 14 +- .../reactive/UndertowServerHttpRequest.java | 14 +- .../reactive/UndertowServerHttpResponse.java | 14 +- .../session/CookieWebSessionIdResolver.java | 19 +- .../reactive/CookieIntegrationTests.java | 7 +- .../reactive/MockServerHttpRequest.java | 10 +- .../reactive/MockServerHttpResponse.java | 10 + .../reactive/ServerHttpResponseTests.java | 16 +- 20 files changed, 382 insertions(+), 1519 deletions(-) delete mode 100644 spring-web-reactive/src/main/java/org/springframework/http/HttpHeaders.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/http/ServerHttpCookie.java diff --git a/spring-web-reactive/src/main/java/org/springframework/http/HttpCookie.java b/spring-web-reactive/src/main/java/org/springframework/http/HttpCookie.java index 81a762f352..3f1675a4a9 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/HttpCookie.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/HttpCookie.java @@ -15,54 +15,28 @@ */ package org.springframework.http; -import java.time.Duration; - import org.springframework.util.Assert; -import org.springframework.util.ObjectUtils; /** - * Representation for an HTTP Cookie. + * Represents an HTTP Cookie with a name and value. * - *

    Use the {@link #clientCookie} factory method to create a client-to-server, - * name-value pair cookie and the {@link #serverCookie} factory method to build - * a server-to-client cookie with additional attributes. + *

    The {@link ServerHttpCookie} sub-class exposes the extra attributes that + * a server can include in a Set-Cookie response header. * * @author Rossen Stoyanchev * @see RFC 6265 */ -public final class HttpCookie { +public class HttpCookie { private final String name; private final String value; - private final Duration maxAge; - - private final String domain; - - private final String path; - - private final boolean secure; - - private final boolean httpOnly; - - - private HttpCookie(String name, String value) { - this(name, value, Duration.ofSeconds(-1), null, null, false, false); - } - - private HttpCookie(String name, String value, Duration maxAge, String domain, String path, - boolean secure, boolean httpOnly) { + public HttpCookie(String name, String value) { Assert.hasLength(name, "'name' is required and must not be empty."); - Assert.notNull(maxAge); this.name = name; this.value = (value != null ? value : ""); - this.maxAge = maxAge; - this.domain = domain; - this.path = path; - this.secure = secure; - this.httpOnly = httpOnly; } /** @@ -73,59 +47,16 @@ public final class HttpCookie { } /** - * Return the cookie value. + * Return the cookie value or an empty string, never {@code null}. */ public String getValue() { return this.value; } - /** - * Return the cookie "Max-Age" attribute in seconds. - * - *

    A positive value indicates when the cookie expires relative to the - * current time. A value of 0 means the cookie should expire immediately. - * A negative value means no "Max-Age" attribute in which case the cookie - * is removed when the browser is closed. - */ - public Duration getMaxAge() { - return this.maxAge; - } - - /** - * Return the cookie "Domain" attribute. - */ - public String getDomain() { - return this.domain; - } - - /** - * Return the cookie "Path" attribute. - */ - public String getPath() { - return this.path; - } - - /** - * Return {@code true} if the cookie has the "Secure" attribute. - */ - public boolean isSecure() { - return this.secure; - } - - /** - * Return {@code true} if the cookie has the "HttpOnly" attribute. - * @see http://www.owasp.org/index.php/HTTPOnly - */ - public boolean isHttpOnly() { - return this.httpOnly; - } @Override public int hashCode() { - int result = this.name.hashCode(); - result = 31 * result + ObjectUtils.nullSafeHashCode(this.domain); - result = 31 * result + ObjectUtils.nullSafeHashCode(this.path); - return result; + return this.name.hashCode(); } @Override @@ -137,122 +68,7 @@ public final class HttpCookie { return false; } HttpCookie otherCookie = (HttpCookie) other; - return (this.name.equalsIgnoreCase(otherCookie.getName()) && - ObjectUtils.nullSafeEquals(this.path, otherCookie.getPath()) && - ObjectUtils.nullSafeEquals(this.domain, otherCookie.getDomain())); - } - - /** - * Factory method to create a cookie sent from a client to a server. - * Client cookies are name-value pairs only without attributes. - * @param name the cookie name - * @param value the cookie value - * @return the created cookie instance - */ - public static HttpCookie clientCookie(String name, String value) { - return new HttpCookie(name, value); - } - - /** - * Factory method to obtain a builder for a server-defined cookie that starts - * with a name-value pair and may also include attributes. - * @param name the cookie name - * @param value the cookie value - * @return the created cookie instance - */ - public static HttpCookieBuilder serverCookie(final String name, final String value) { - - return new HttpCookieBuilder() { - - private Duration maxAge = Duration.ofSeconds(-1); - - private String domain; - - private String path; - - private boolean secure; - - private boolean httpOnly; - - - @Override - public HttpCookieBuilder maxAge(Duration maxAge) { - this.maxAge = maxAge; - return this; - } - - @Override - public HttpCookieBuilder domain(String domain) { - this.domain = domain; - return this; - } - - @Override - public HttpCookieBuilder path(String path) { - this.path = path; - return this; - } - - @Override - public HttpCookieBuilder secure() { - this.secure = true; - return this; - } - - @Override - public HttpCookieBuilder httpOnly() { - this.httpOnly = true; - return this; - } - - @Override - public HttpCookie build() { - return new HttpCookie(name, value, this.maxAge, this.domain, this.path, - this.secure, this.httpOnly); - } - }; - } - - /** - * A builder for a server-defined HttpCookie with attributes. - */ - public interface HttpCookieBuilder { - - /** - * Set the cookie "Max-Age" attribute. - * - *

    A positive value indicates when the cookie should expire relative - * to the current time. A value of 0 means the cookie should expire - * immediately. A negative value results in no "Max-Age" attribute in - * which case the cookie is removed when the browser is closed. - */ - HttpCookieBuilder maxAge(Duration maxAge); - - /** - * Set the cookie "Path" attribute. - */ - HttpCookieBuilder path(String path); - - /** - * Set the cookie "Domain" attribute. - */ - HttpCookieBuilder domain(String domain); - - /** - * Add the "Secure" attribute to the cookie. - */ - HttpCookieBuilder secure(); - - /** - * Add the "HttpOnly" attribute to the cookie. - * @see http://www.owasp.org/index.php/HTTPOnly - */ - HttpCookieBuilder httpOnly(); - - /** - * Create the HttpCookie. - */ - HttpCookie build(); + return (this.name.equalsIgnoreCase(otherCookie.getName())); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/HttpHeaders.java b/spring-web-reactive/src/main/java/org/springframework/http/HttpHeaders.java deleted file mode 100644 index 419e94b947..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/http/HttpHeaders.java +++ /dev/null @@ -1,1158 +0,0 @@ -/* - * Copyright 2002-2015 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; - -import java.io.Serializable; -import java.net.URI; -import java.nio.charset.Charset; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Date; -import java.util.EnumSet; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import java.util.TimeZone; - -import org.springframework.util.Assert; -import org.springframework.util.LinkedCaseInsensitiveMap; -import org.springframework.util.MultiValueMap; -import org.springframework.util.StringUtils; - -// A copy of HttpHeaders with additional support for: -// - HTTP cookies - -// To be merged into HttpHeaders from spring-web - -public class HttpHeaders implements MultiValueMap, Serializable { - - private static final long serialVersionUID = -8578554704772377436L; - - /** - * The HTTP {@code Accept} header field name. - * @see Section 5.3.2 of RFC 7231 - */ - public static final String ACCEPT = "Accept"; - /** - * The HTTP {@code Accept-Charset} header field name. - * @see Section 5.3.3 of RFC 7231 - */ - public static final String ACCEPT_CHARSET = "Accept-Charset"; - /** - * The HTTP {@code Accept-Encoding} header field name. - * @see Section 5.3.4 of RFC 7231 - */ - public static final String ACCEPT_ENCODING = "Accept-Encoding"; - /** - * The HTTP {@code Accept-Language} header field name. - * @see Section 5.3.5 of RFC 7231 - */ - public static final String ACCEPT_LANGUAGE = "Accept-Language"; - /** - * The HTTP {@code Accept-Ranges} header field name. - * @see Section 5.3.5 of RFC 7233 - */ - public static final String ACCEPT_RANGES = "Accept-Ranges"; - /** - * The CORS {@code Access-Control-Allow-Credentials} response header field name. - * @see CORS W3C recommandation - */ - public static final String ACCESS_CONTROL_ALLOW_CREDENTIALS = "Access-Control-Allow-Credentials"; - /** - * The CORS {@code Access-Control-Allow-Headers} response header field name. - * @see CORS W3C recommandation - */ - public static final String ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers"; - /** - * The CORS {@code Access-Control-Allow-Methods} response header field name. - * @see CORS W3C recommandation - */ - public static final String ACCESS_CONTROL_ALLOW_METHODS = "Access-Control-Allow-Methods"; - /** - * The CORS {@code Access-Control-Allow-Origin} response header field name. - * @see CORS W3C recommandation - */ - public static final String ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin"; - /** - * The CORS {@code Access-Control-Expose-Headers} response header field name. - * @see CORS W3C recommandation - */ - public static final String ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-Headers"; - /** - * The CORS {@code Access-Control-Max-Age} response header field name. - * @see CORS W3C recommandation - */ - public static final String ACCESS_CONTROL_MAX_AGE = "Access-Control-Max-Age"; - /** - * The CORS {@code Access-Control-Request-Headers} request header field name. - * @see CORS W3C recommandation - */ - public static final String ACCESS_CONTROL_REQUEST_HEADERS = "Access-Control-Request-Headers"; - /** - * The CORS {@code Access-Control-Request-Method} request header field name. - * @see CORS W3C recommandation - */ - public static final String ACCESS_CONTROL_REQUEST_METHOD = "Access-Control-Request-Method"; - /** - * The HTTP {@code Age} header field name. - * @see Section 5.1 of RFC 7234 - */ - public static final String AGE = "Age"; - /** - * The HTTP {@code Allow} header field name. - * @see Section 7.4.1 of RFC 7231 - */ - public static final String ALLOW = "Allow"; - /** - * The HTTP {@code Authorization} header field name. - * @see Section 4.2 of RFC 7235 - */ - public static final String AUTHORIZATION = "Authorization"; - /** - * The HTTP {@code Cache-Control} header field name. - * @see Section 5.2 of RFC 7234 - */ - public static final String CACHE_CONTROL = "Cache-Control"; - /** - * The HTTP {@code Connection} header field name. - * @see Section 6.1 of RFC 7230 - */ - public static final String CONNECTION = "Connection"; - /** - * The HTTP {@code Content-Encoding} header field name. - * @see Section 3.1.2.2 of RFC 7231 - */ - public static final String CONTENT_ENCODING = "Content-Encoding"; - /** - * The HTTP {@code Content-Disposition} header field name - * @see RFC 6266 - */ - public static final String CONTENT_DISPOSITION = "Content-Disposition"; - /** - * The HTTP {@code Content-Language} header field name. - * @see Section 3.1.3.2 of RFC 7231 - */ - public static final String CONTENT_LANGUAGE = "Content-Language"; - /** - * The HTTP {@code Content-Length} header field name. - * @see Section 3.3.2 of RFC 7230 - */ - public static final String CONTENT_LENGTH = "Content-Length"; - /** - * The HTTP {@code Content-Location} header field name. - * @see Section 3.1.4.2 of RFC 7231 - */ - public static final String CONTENT_LOCATION = "Content-Location"; - /** - * The HTTP {@code Content-Range} header field name. - * @see Section 4.2 of RFC 7233 - */ - public static final String CONTENT_RANGE = "Content-Range"; - /** - * The HTTP {@code Content-Type} header field name. - * @see Section 3.1.1.5 of RFC 7231 - */ - public static final String CONTENT_TYPE = "Content-Type"; - /** - * The HTTP {@code Cookie} header field name. - * @see Section 4.3.4 of RFC 2109 - */ - public static final String COOKIE = "Cookie"; - /** - * The HTTP {@code Date} header field name. - * @see Section 7.1.1.2 of RFC 7231 - */ - public static final String DATE = "Date"; - /** - * The HTTP {@code ETag} header field name. - * @see Section 2.3 of RFC 7232 - */ - public static final String ETAG = "ETag"; - /** - * The HTTP {@code Expect} header field name. - * @see Section 5.1.1 of RFC 7231 - */ - public static final String EXPECT = "Expect"; - /** - * The HTTP {@code Expires} header field name. - * @see Section 5.3 of RFC 7234 - */ - public static final String EXPIRES = "Expires"; - /** - * The HTTP {@code From} header field name. - * @see Section 5.5.1 of RFC 7231 - */ - public static final String FROM = "From"; - /** - * The HTTP {@code Host} header field name. - * @see Section 5.4 of RFC 7230 - */ - public static final String HOST = "Host"; - /** - * The HTTP {@code If-Match} header field name. - * @see Section 3.1 of RFC 7232 - */ - public static final String IF_MATCH = "If-Match"; - /** - * The HTTP {@code If-Modified-Since} header field name. - * @see Section 3.3 of RFC 7232 - */ - public static final String IF_MODIFIED_SINCE = "If-Modified-Since"; - /** - * The HTTP {@code If-None-Match} header field name. - * @see Section 3.2 of RFC 7232 - */ - public static final String IF_NONE_MATCH = "If-None-Match"; - /** - * The HTTP {@code If-Range} header field name. - * @see Section 3.2 of RFC 7233 - */ - public static final String IF_RANGE = "If-Range"; - /** - * The HTTP {@code If-Unmodified-Since} header field name. - * @see Section 3.4 of RFC 7232 - */ - public static final String IF_UNMODIFIED_SINCE = "If-Unmodified-Since"; - /** - * The HTTP {@code Last-Modified} header field name. - * @see Section 2.2 of RFC 7232 - */ - public static final String LAST_MODIFIED = "Last-Modified"; - /** - * The HTTP {@code Link} header field name. - * @see RFC 5988 - */ - public static final String LINK = "Link"; - /** - * The HTTP {@code Location} header field name. - * @see Section 7.1.2 of RFC 7231 - */ - public static final String LOCATION = "Location"; - /** - * The HTTP {@code Max-Forwards} header field name. - * @see Section 5.1.2 of RFC 7231 - */ - public static final String MAX_FORWARDS = "Max-Forwards"; - /** - * The HTTP {@code Origin} header field name. - * @see RFC 6454 - */ - public static final String ORIGIN = "Origin"; - /** - * The HTTP {@code Pragma} header field name. - * @see Section 5.4 of RFC 7234 - */ - public static final String PRAGMA = "Pragma"; - /** - * The HTTP {@code Proxy-Authenticate} header field name. - * @see Section 4.3 of RFC 7235 - */ - public static final String PROXY_AUTHENTICATE = "Proxy-Authenticate"; - /** - * The HTTP {@code Proxy-Authorization} header field name. - * @see Section 4.4 of RFC 7235 - */ - public static final String PROXY_AUTHORIZATION = "Proxy-Authorization"; - /** - * The HTTP {@code Range} header field name. - * @see Section 3.1 of RFC 7233 - */ - public static final String RANGE = "Range"; - /** - * The HTTP {@code Referer} header field name. - * @see Section 5.5.2 of RFC 7231 - */ - public static final String REFERER = "Referer"; - /** - * The HTTP {@code Retry-After} header field name. - * @see Section 7.1.3 of RFC 7231 - */ - public static final String RETRY_AFTER = "Retry-After"; - /** - * The HTTP {@code Server} header field name. - * @see Section 7.4.2 of RFC 7231 - */ - public static final String SERVER = "Server"; - /** - * The HTTP {@code Set-Cookie} header field name. - * @see Section 4.2.2 of RFC 2109 - */ - public static final String SET_COOKIE = "Set-Cookie"; - /** - * The HTTP {@code Set-Cookie2} header field name. - * @see RFC 2965 - */ - public static final String SET_COOKIE2 = "Set-Cookie2"; - /** - * The HTTP {@code TE} header field name. - * @see Section 4.3 of RFC 7230 - */ - public static final String TE = "TE"; - /** - * The HTTP {@code Trailer} header field name. - * @see Section 4.4 of RFC 7230 - */ - public static final String TRAILER = "Trailer"; - /** - * The HTTP {@code Transfer-Encoding} header field name. - * @see Section 3.3.1 of RFC 7230 - */ - public static final String TRANSFER_ENCODING = "Transfer-Encoding"; - /** - * The HTTP {@code Upgrade} header field name. - * @see Section 6.7 of RFC 7230 - */ - public static final String UPGRADE = "Upgrade"; - /** - * The HTTP {@code User-Agent} header field name. - * @see Section 5.5.3 of RFC 7231 - */ - public static final String USER_AGENT = "User-Agent"; - /** - * The HTTP {@code Vary} header field name. - * @see Section 7.1.4 of RFC 7231 - */ - public static final String VARY = "Vary"; - /** - * The HTTP {@code Via} header field name. - * @see Section 5.7.1 of RFC 7230 - */ - public static final String VIA = "Via"; - /** - * The HTTP {@code Warning} header field name. - * @see Section 5.5 of RFC 7234 - */ - public static final String WARNING = "Warning"; - /** - * The HTTP {@code WWW-Authenticate} header field name. - * @see Section 4.1 of RFC 7235 - */ - public static final String WWW_AUTHENTICATE = "WWW-Authenticate"; - - /** - * Date formats as specified in the HTTP RFC - * @see Section 7.1.1.1 of RFC 7231 - */ - private static final String[] DATE_FORMATS = new String[] { - "EEE, dd MMM yyyy HH:mm:ss zzz", - "EEE, dd-MMM-yy HH:mm:ss zzz", - "EEE MMM dd HH:mm:ss yyyy" - }; - - private static TimeZone GMT = TimeZone.getTimeZone("GMT"); - - - private final Map> headers; - - private final Map> cookies; - - - /** - * Constructs a new, empty instance of the {@code HttpHeaders} object. - */ - public HttpHeaders() { - this(new LinkedCaseInsensitiveMap>(8, Locale.ENGLISH), null, false); - } - - /** - * Constructor with a map of HTTP input cookies (e.g. cookies sent by client) - * that enables lazy initialization on first access of the map. - * @param inputCookies input cookies - */ - public HttpHeaders(Map> inputCookies) { - this(new LinkedCaseInsensitiveMap>(8, Locale.ENGLISH), inputCookies, false); - Assert.notNull(cookies, "'inputCookies' is required."); - } - - /** - * Private constructor that can create read-only {@code HttpHeader} instances. - */ - private HttpHeaders(Map> headers, Map> cookies, - boolean readOnly) { - - Assert.notNull(headers, "'headers' must not be null"); - if (readOnly) { - Map> map = - new LinkedCaseInsensitiveMap>(headers.size(), Locale.ENGLISH); - for (Entry> entry : headers.entrySet()) { - List values = Collections.unmodifiableList(entry.getValue()); - map.put(entry.getKey(), values); - } - this.headers = Collections.unmodifiableMap(map); - this.cookies = (cookies != null ? Collections.unmodifiableMap(cookies) : Collections.emptyMap()); - } - else { - this.headers = headers; - this.cookies = (cookies != null ? cookies : new LinkedCaseInsensitiveMap<>()); - } - } - - /** - * Set the list of acceptable {@linkplain MediaType media types}, - * as specified by the {@code Accept} header. - */ - public void setAccept(List acceptableMediaTypes) { - set(ACCEPT, MediaType.toString(acceptableMediaTypes)); - } - - /** - * Return the list of acceptable {@linkplain MediaType media types}, - * as specified by the {@code Accept} header. - *

    Returns an empty list when the acceptable media types are unspecified. - */ - public List getAccept() { - String value = getFirst(ACCEPT); - List result = (value != null ? MediaType.parseMediaTypes(value) : Collections.emptyList()); - - // Some containers parse 'Accept' into multiple values - if (result.size() == 1) { - List acceptHeader = get(ACCEPT); - if (acceptHeader.size() > 1) { - value = StringUtils.collectionToCommaDelimitedString(acceptHeader); - result = MediaType.parseMediaTypes(value); - } - } - - return result; - } - - /** - * Set the (new) value of the {@code Access-Control-Allow-Credentials} response header. - */ - public void setAccessControlAllowCredentials(boolean allowCredentials) { - set(ACCESS_CONTROL_ALLOW_CREDENTIALS, Boolean.toString(allowCredentials)); - } - - /** - * Returns the value of the {@code Access-Control-Allow-Credentials} response header. - */ - public boolean getAccessControlAllowCredentials() { - return new Boolean(getFirst(ACCESS_CONTROL_ALLOW_CREDENTIALS)); - } - - /** - * Set the (new) value of the {@code Access-Control-Allow-Headers} response header. - */ - public void setAccessControlAllowHeaders(List allowedHeaders) { - set(ACCESS_CONTROL_ALLOW_HEADERS, toCommaDelimitedString(allowedHeaders)); - } - - /** - * Returns the value of the {@code Access-Control-Allow-Headers} response header. - */ - public List getAccessControlAllowHeaders() { - return getFirstValueAsList(ACCESS_CONTROL_ALLOW_HEADERS); - } - - /** - * Set the (new) value of the {@code Access-Control-Allow-Methods} response header. - */ - public void setAccessControlAllowMethods(List allowedMethods) { - set(ACCESS_CONTROL_ALLOW_METHODS, StringUtils.collectionToCommaDelimitedString(allowedMethods)); - } - - /** - * Returns the value of the {@code Access-Control-Allow-Methods} response header. - */ - public List getAccessControlAllowMethods() { - List result = new ArrayList(); - String value = getFirst(ACCESS_CONTROL_ALLOW_METHODS); - if (value != null) { - String[] tokens = value.split(",\\s*"); - for (String token : tokens) { - result.add(HttpMethod.valueOf(token)); - } - } - return result; - } - - /** - * Set the (new) value of the {@code Access-Control-Allow-Origin} response header. - */ - public void setAccessControlAllowOrigin(String allowedOrigin) { - set(ACCESS_CONTROL_ALLOW_ORIGIN, allowedOrigin); - } - - /** - * Returns the value of the {@code Access-Control-Allow-Origin} response header. - */ - public String getAccessControlAllowOrigin() { - return getFirst(ACCESS_CONTROL_ALLOW_ORIGIN); - } - - /** - * Set the (new) value of the {@code Access-Control-Expose-Headers} response header. - */ - public void setAccessControlExposeHeaders(List exposedHeaders) { - set(ACCESS_CONTROL_EXPOSE_HEADERS, toCommaDelimitedString(exposedHeaders)); - } - - /** - * Returns the value of the {@code Access-Control-Expose-Headers} response header. - */ - public List getAccessControlExposeHeaders() { - return getFirstValueAsList(ACCESS_CONTROL_EXPOSE_HEADERS); - } - - /** - * Set the (new) value of the {@code Access-Control-Max-Age} response header. - */ - public void setAccessControlMaxAge(long maxAge) { - set(ACCESS_CONTROL_MAX_AGE, Long.toString(maxAge)); - } - - /** - * Returns the value of the {@code Access-Control-Max-Age} response header. - *

    Returns -1 when the max age is unknown. - */ - public long getAccessControlMaxAge() { - String value = getFirst(ACCESS_CONTROL_MAX_AGE); - return (value != null ? Long.parseLong(value) : -1); - } - - /** - * Set the (new) value of the {@code Access-Control-Request-Headers} request header. - */ - public void setAccessControlRequestHeaders(List requestHeaders) { - set(ACCESS_CONTROL_REQUEST_HEADERS, toCommaDelimitedString(requestHeaders)); - } - - /** - * Returns the value of the {@code Access-Control-Request-Headers} request header. - */ - public List getAccessControlRequestHeaders() { - return getFirstValueAsList(ACCESS_CONTROL_REQUEST_HEADERS); - } - - /** - * Set the (new) value of the {@code Access-Control-Request-Method} request header. - */ - public void setAccessControlRequestMethod(HttpMethod requestedMethod) { - set(ACCESS_CONTROL_REQUEST_METHOD, requestedMethod.name()); - } - - /** - * Returns the value of the {@code Access-Control-Request-Method} request header. - */ - public HttpMethod getAccessControlRequestMethod() { - String value = getFirst(ACCESS_CONTROL_REQUEST_METHOD); - return (value != null ? HttpMethod.valueOf(value) : null); - } - - /** - * Set the list of acceptable {@linkplain Charset charsets}, - * as specified by the {@code Accept-Charset} header. - */ - public void setAcceptCharset(List acceptableCharsets) { - StringBuilder builder = new StringBuilder(); - for (Iterator iterator = acceptableCharsets.iterator(); iterator.hasNext();) { - Charset charset = iterator.next(); - builder.append(charset.name().toLowerCase(Locale.ENGLISH)); - if (iterator.hasNext()) { - builder.append(", "); - } - } - set(ACCEPT_CHARSET, builder.toString()); - } - - /** - * Return the list of acceptable {@linkplain Charset charsets}, - * as specified by the {@code Accept-Charset} header. - */ - public List getAcceptCharset() { - List result = new ArrayList(); - String value = getFirst(ACCEPT_CHARSET); - if (value != null) { - String[] tokens = value.split(",\\s*"); - for (String token : tokens) { - int paramIdx = token.indexOf(';'); - String charsetName; - if (paramIdx == -1) { - charsetName = token; - } - else { - charsetName = token.substring(0, paramIdx); - } - if (!charsetName.equals("*")) { - result.add(Charset.forName(charsetName)); - } - } - } - return result; - } - - /** - * Set the set of allowed {@link HttpMethod HTTP methods}, - * as specified by the {@code Allow} header. - */ - public void setAllow(Set allowedMethods) { - set(ALLOW, StringUtils.collectionToCommaDelimitedString(allowedMethods)); - } - - /** - * Return the set of allowed {@link HttpMethod HTTP methods}, - * as specified by the {@code Allow} header. - *

    Returns an empty set when the allowed methods are unspecified. - */ - public Set getAllow() { - String value = getFirst(ALLOW); - if (!StringUtils.isEmpty(value)) { - List allowedMethod = new ArrayList(5); - String[] tokens = value.split(",\\s*"); - for (String token : tokens) { - allowedMethod.add(HttpMethod.valueOf(token)); - } - return EnumSet.copyOf(allowedMethod); - } - else { - return EnumSet.noneOf(HttpMethod.class); - } - } - - /** - * Set the (new) value of the {@code Cache-Control} header. - */ - public void setCacheControl(String cacheControl) { - set(CACHE_CONTROL, cacheControl); - } - - /** - * Returns the value of the {@code Cache-Control} header. - */ - public String getCacheControl() { - return getFirst(CACHE_CONTROL); - } - - /** - * Set the (new) value of the {@code Connection} header. - */ - public void setConnection(String connection) { - set(CONNECTION, connection); - } - - /** - * Set the (new) value of the {@code Connection} header. - */ - public void setConnection(List connection) { - set(CONNECTION, toCommaDelimitedString(connection)); - } - - /** - * Returns the value of the {@code Connection} header. - */ - public List getConnection() { - return getFirstValueAsList(CONNECTION); - } - - /** - * Set the (new) value of the {@code Content-Disposition} header - * for {@code form-data}. - * @param name the control name - * @param filename the filename (may be {@code null}) - */ - public void setContentDispositionFormData(String name, String filename) { - Assert.notNull(name, "'name' must not be null"); - StringBuilder builder = new StringBuilder("form-data; name=\""); - builder.append(name).append('\"'); - if (filename != null) { - builder.append("; filename=\""); - builder.append(filename).append('\"'); - } - set(CONTENT_DISPOSITION, builder.toString()); - } - - /** - * Set the length of the body in bytes, as specified by the - * {@code Content-Length} header. - */ - public void setContentLength(long contentLength) { - set(CONTENT_LENGTH, Long.toString(contentLength)); - } - - /** - * Return the length of the body in bytes, as specified by the - * {@code Content-Length} header. - *

    Returns -1 when the content-length is unknown. - */ - public long getContentLength() { - String value = getFirst(CONTENT_LENGTH); - return (value != null ? Long.parseLong(value) : -1); - } - - /** - * Set the {@linkplain MediaType media type} of the body, - * as specified by the {@code Content-Type} header. - */ - public void setContentType(MediaType mediaType) { - Assert.isTrue(!mediaType.isWildcardType(), "'Content-Type' cannot contain wildcard type '*'"); - Assert.isTrue(!mediaType.isWildcardSubtype(), "'Content-Type' cannot contain wildcard subtype '*'"); - set(CONTENT_TYPE, mediaType.toString()); - } - - /** - * Return the {@linkplain MediaType media type} of the body, as specified - * by the {@code Content-Type} header. - *

    Returns {@code null} when the content-type is unknown. - */ - public MediaType getContentType() { - String value = getFirst(CONTENT_TYPE); - return (StringUtils.hasLength(value) ? MediaType.parseMediaType(value) : null); - } - - /** - * Add an HTTP cookie. - */ - public void addCookie(HttpCookie cookie) { - String name = cookie.getName(); - List set = this.cookies.get(name); - if (set == null) { - set = new ArrayList<>(); - this.cookies.put(name, set); - } - set.add(cookie); - } - - /** - * Return a map with {@link HttpCookie}s. When reading input cookies this map - * cannot be modified. When writing output cookies, this map is mutable. - */ - public Map> getCookies() { - return this.cookies; - } - - /** - * Set the date and time at which the message was created, as specified - * by the {@code Date} header. - *

    The date should be specified as the number of milliseconds since - * January 1, 1970 GMT. - */ - public void setDate(long date) { - setDate(DATE, date); - } - - /** - * Return the date and time at which the message was created, as specified - * by the {@code Date} header. - *

    The date is returned as the number of milliseconds since - * January 1, 1970 GMT. Returns -1 when the date is unknown. - * @throws IllegalArgumentException if the value can't be converted to a date - */ - public long getDate() { - return getFirstDate(DATE); - } - - /** - * Set the (new) entity tag of the body, as specified by the {@code ETag} header. - */ - public void setETag(String eTag) { - if (eTag != null) { - Assert.isTrue(eTag.startsWith("\"") || eTag.startsWith("W/"), - "Invalid eTag, does not start with W/ or \""); - Assert.isTrue(eTag.endsWith("\""), "Invalid eTag, does not end with \""); - } - set(ETAG, eTag); - } - - /** - * Return the entity tag of the body, as specified by the {@code ETag} header. - */ - public String getETag() { - return getFirst(ETAG); - } - - /** - * Set the date and time at which the message is no longer valid, - * as specified by the {@code Expires} header. - *

    The date should be specified as the number of milliseconds since - * January 1, 1970 GMT. - */ - public void setExpires(long expires) { - setDate(EXPIRES, expires); - } - - /** - * Return the date and time at which the message is no longer valid, - * as specified by the {@code Expires} header. - *

    The date is returned as the number of milliseconds since - * January 1, 1970 GMT. Returns -1 when the date is unknown. - */ - public long getExpires() { - try { - return getFirstDate(EXPIRES); - } - catch (IllegalArgumentException ex) { - return -1; - } - } - - /** - * Set the (new) value of the {@code If-Modified-Since} header. - *

    The date should be specified as the number of milliseconds since - * January 1, 1970 GMT. - */ - public void setIfModifiedSince(long ifModifiedSince) { - setDate(IF_MODIFIED_SINCE, ifModifiedSince); - } - - /** - * Return the value of the {@code If-Modified-Since} header. - *

    The date is returned as the number of milliseconds since - * January 1, 1970 GMT. Returns -1 when the date is unknown. - */ - public long getIfModifiedSince() { - return getFirstDate(IF_MODIFIED_SINCE); - } - - /** - * Set the (new) value of the {@code If-None-Match} header. - */ - public void setIfNoneMatch(String ifNoneMatch) { - set(IF_NONE_MATCH, ifNoneMatch); - } - - /** - * Set the (new) values of the {@code If-None-Match} header. - */ - public void setIfNoneMatch(List ifNoneMatchList) { - set(IF_NONE_MATCH, toCommaDelimitedString(ifNoneMatchList)); - } - - protected String toCommaDelimitedString(List list) { - StringBuilder builder = new StringBuilder(); - for (Iterator iterator = list.iterator(); iterator.hasNext();) { - String ifNoneMatch = iterator.next(); - builder.append(ifNoneMatch); - if (iterator.hasNext()) { - builder.append(", "); - } - } - return builder.toString(); - } - - /** - * Return the value of the {@code If-None-Match} header. - */ - public List getIfNoneMatch() { - return getFirstValueAsList(IF_NONE_MATCH); - } - - protected List getFirstValueAsList(String header) { - List result = new ArrayList(); - String value = getFirst(header); - if (value != null) { - String[] tokens = value.split(",\\s*"); - for (String token : tokens) { - result.add(token); - } - } - return result; - } - - /** - * Set the time the resource was last changed, as specified by the - * {@code Last-Modified} header. - *

    The date should be specified as the number of milliseconds since - * January 1, 1970 GMT. - */ - public void setLastModified(long lastModified) { - setDate(LAST_MODIFIED, lastModified); - } - - /** - * Return the time the resource was last changed, as specified by the - * {@code Last-Modified} header. - *

    The date is returned as the number of milliseconds since - * January 1, 1970 GMT. Returns -1 when the date is unknown. - */ - public long getLastModified() { - return getFirstDate(LAST_MODIFIED); - } - - /** - * Set the (new) location of a resource, - * as specified by the {@code Location} header. - */ - public void setLocation(URI location) { - set(LOCATION, location.toASCIIString()); - } - - /** - * Return the (new) location of a resource - * as specified by the {@code Location} header. - *

    Returns {@code null} when the location is unknown. - */ - public URI getLocation() { - String value = getFirst(LOCATION); - return (value != null ? URI.create(value) : null); - } - - /** - * Set the (new) value of the {@code Origin} header. - */ - public void setOrigin(String origin) { - set(ORIGIN, origin); - } - - /** - * Return the value of the {@code Origin} header. - */ - public String getOrigin() { - return getFirst(ORIGIN); - } - - /** - * Set the (new) value of the {@code Pragma} header. - */ - public void setPragma(String pragma) { - set(PRAGMA, pragma); - } - - /** - * Return the value of the {@code Pragma} header. - */ - public String getPragma() { - return getFirst(PRAGMA); - } - - /** - * Sets the (new) value of the {@code Range} header. - */ - public void setRange(List ranges) { - String value = HttpRange.toString(ranges); - set(RANGE, value); - } - - /** - * Returns the value of the {@code Range} header. - *

    Returns an empty list when the range is unknown. - */ - public List getRange() { - String value = getFirst(RANGE); - return HttpRange.parseRanges(value); - } - - /** - * Set the (new) value of the {@code Upgrade} header. - */ - public void setUpgrade(String upgrade) { - set(UPGRADE, upgrade); - } - - /** - * Returns the value of the {@code Upgrade} header. - */ - public String getUpgrade() { - return getFirst(UPGRADE); - } - - /** - * Parse the first header value for the given header name as a date, - * return -1 if there is no value, or raise {@link IllegalArgumentException} - * if the value cannot be parsed as a date. - */ - public long getFirstDate(String headerName) { - String headerValue = getFirst(headerName); - if (headerValue == null) { - return -1; - } - for (String dateFormat : DATE_FORMATS) { - SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateFormat, Locale.US); - simpleDateFormat.setTimeZone(GMT); - try { - return simpleDateFormat.parse(headerValue).getTime(); - } - catch (ParseException ex) { - // ignore - } - } - throw new IllegalArgumentException("Cannot parse date value \"" + headerValue + - "\" for \"" + headerName + "\" header"); - } - - /** - * Set the given date under the given header name after formatting it as a string - * using the pattern {@code "EEE, dd MMM yyyy HH:mm:ss zzz"}. The equivalent of - * {@link #set(String, String)} but for date headers. - */ - public void setDate(String headerName, long date) { - SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMATS[0], Locale.US); - dateFormat.setTimeZone(GMT); - set(headerName, dateFormat.format(new Date(date))); - } - - /** - * Return the first header value for the given header name, if any. - * @param headerName the header name - * @return the first header value, or {@code null} if none - */ - @Override - public String getFirst(String headerName) { - List headerValues = this.headers.get(headerName); - return (headerValues != null ? headerValues.get(0) : null); - } - - /** - * Add the given, single header value under the given name. - * @param headerName the header name - * @param headerValue the header value - * @throws UnsupportedOperationException if adding headers is not supported - * @see #put(String, List) - * @see #set(String, String) - */ - @Override - public void add(String headerName, String headerValue) { - List headerValues = this.headers.get(headerName); - if (headerValues == null) { - headerValues = new LinkedList(); - this.headers.put(headerName, headerValues); - } - headerValues.add(headerValue); - } - - /** - * Set the given, single header value under the given name. - * @param headerName the header name - * @param headerValue the header value - * @throws UnsupportedOperationException if adding headers is not supported - * @see #put(String, List) - * @see #add(String, String) - */ - @Override - public void set(String headerName, String headerValue) { - List headerValues = new LinkedList(); - headerValues.add(headerValue); - this.headers.put(headerName, headerValues); - } - - @Override - public void setAll(Map values) { - for (Entry entry : values.entrySet()) { - set(entry.getKey(), entry.getValue()); - } - } - - @Override - public Map toSingleValueMap() { - LinkedHashMap singleValueMap = new LinkedHashMap(this.headers.size()); - for (Entry> entry : this.headers.entrySet()) { - singleValueMap.put(entry.getKey(), entry.getValue().get(0)); - } - return singleValueMap; - } - - - // Map implementation - - @Override - public int size() { - return this.headers.size(); - } - - @Override - public boolean isEmpty() { - return this.headers.isEmpty(); - } - - @Override - public boolean containsKey(Object key) { - return this.headers.containsKey(key); - } - - @Override - public boolean containsValue(Object value) { - return this.headers.containsValue(value); - } - - @Override - public List get(Object key) { - return this.headers.get(key); - } - - @Override - public List put(String key, List value) { - return this.headers.put(key, value); - } - - @Override - public List remove(Object key) { - return this.headers.remove(key); - } - - @Override - public void putAll(Map> map) { - this.headers.putAll(map); - } - - @Override - public void clear() { - this.headers.clear(); - } - - @Override - public Set keySet() { - return this.headers.keySet(); - } - - @Override - public Collection> values() { - return this.headers.values(); - } - - @Override - public Set>> entrySet() { - return this.headers.entrySet(); - } - - - @Override - public boolean equals(Object other) { - if (this == other) { - return true; - } - if (!(other instanceof HttpHeaders)) { - return false; - } - HttpHeaders otherHeaders = (HttpHeaders) other; - return this.headers.equals(otherHeaders.headers); - } - - @Override - public int hashCode() { - return this.headers.hashCode(); - } - - @Override - public String toString() { - return this.headers.toString(); - } - - - /** - * Return a {@code HttpHeaders} object that can only be read, not written to. - */ - public static HttpHeaders readOnlyHttpHeaders(HttpHeaders headers) { - return new HttpHeaders(headers, headers.getCookies(), true); - } - -} diff --git a/spring-web-reactive/src/main/java/org/springframework/http/ServerHttpCookie.java b/spring-web-reactive/src/main/java/org/springframework/http/ServerHttpCookie.java new file mode 100644 index 0000000000..4d7d698d6e --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/http/ServerHttpCookie.java @@ -0,0 +1,228 @@ +/* + * Copyright 2002-2015 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; + +import java.time.Duration; +import java.util.Optional; + +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * Represents a server-side cookie with extra attributes that a server can + * include in a Set-Cookie response header. + * + *

    Use {@link #with} to create a {@code ServerHttpCookie}. + * + * @author Rossen Stoyanchev + * @see RFC 6265 + */ +public final class ServerHttpCookie extends HttpCookie { + + private final Duration maxAge; + + private final Optional domain; + + private final Optional path; + + private final boolean secure; + + private final boolean httpOnly; + + + /** + * Private constructor. See {@link #with(String, String)}. + */ + private ServerHttpCookie(String name, String value, Duration maxAge, String domain, + String path, boolean secure, boolean httpOnly) { + + super(name, value); + Assert.notNull(maxAge); + this.maxAge = maxAge; + this.domain = Optional.ofNullable(domain); + this.path = Optional.ofNullable(path); + this.secure = secure; + this.httpOnly = httpOnly; + } + + + /** + * Return the cookie "Max-Age" attribute in seconds. + * + *

    A positive value indicates when the cookie expires relative to the + * current time. A value of 0 means the cookie should expire immediately. + * A negative value means no "Max-Age" attribute in which case the cookie + * is removed when the browser is closed. + */ + public Duration getMaxAge() { + return this.maxAge; + } + + /** + * Return the cookie "Domain" attribute. + */ + public Optional getDomain() { + return this.domain; + } + + /** + * Return the cookie "Path" attribute. + */ + public Optional getPath() { + return this.path; + } + + /** + * Return {@code true} if the cookie has the "Secure" attribute. + */ + public boolean isSecure() { + return this.secure; + } + + /** + * Return {@code true} if the cookie has the "HttpOnly" attribute. + * @see http://www.owasp.org/index.php/HTTPOnly + */ + public boolean isHttpOnly() { + return this.httpOnly; + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + ObjectUtils.nullSafeHashCode(this.domain); + result = 31 * result + ObjectUtils.nullSafeHashCode(this.path); + return result; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof ServerHttpCookie)) { + return false; + } + ServerHttpCookie otherCookie = (ServerHttpCookie) other; + return (getName().equalsIgnoreCase(otherCookie.getName()) && + ObjectUtils.nullSafeEquals(this.path, otherCookie.getPath()) && + ObjectUtils.nullSafeEquals(this.domain, otherCookie.getDomain())); + } + + + /** + * Factory method to obtain a builder for a server-defined cookie that starts + * with a name-value pair and may also include attributes. + * @param name the cookie name + * @param value the cookie value + * @return the created cookie instance + */ + public static ServerHttpCookieBuilder with(final String name, final String value) { + + return new ServerHttpCookieBuilder() { + + private Duration maxAge = Duration.ofSeconds(-1); + + private String domain; + + private String path; + + private boolean secure; + + private boolean httpOnly; + + + @Override + public ServerHttpCookieBuilder maxAge(Duration maxAge) { + this.maxAge = maxAge; + return this; + } + + @Override + public ServerHttpCookieBuilder domain(String domain) { + this.domain = domain; + return this; + } + + @Override + public ServerHttpCookieBuilder path(String path) { + this.path = path; + return this; + } + + @Override + public ServerHttpCookieBuilder secure() { + this.secure = true; + return this; + } + + @Override + public ServerHttpCookieBuilder httpOnly() { + this.httpOnly = true; + return this; + } + + @Override + public ServerHttpCookie build() { + return new ServerHttpCookie(name, value, this.maxAge, this.domain, this.path, + this.secure, this.httpOnly); + } + }; + } + + /** + * A builder for a server-defined HttpCookie with attributes. + */ + public interface ServerHttpCookieBuilder { + + /** + * Set the cookie "Max-Age" attribute. + * + *

    A positive value indicates when the cookie should expire relative + * to the current time. A value of 0 means the cookie should expire + * immediately. A negative value results in no "Max-Age" attribute in + * which case the cookie is removed when the browser is closed. + */ + ServerHttpCookieBuilder maxAge(Duration maxAge); + + /** + * Set the cookie "Path" attribute. + */ + ServerHttpCookieBuilder path(String path); + + /** + * Set the cookie "Domain" attribute. + */ + ServerHttpCookieBuilder domain(String domain); + + /** + * Add the "Secure" attribute to the cookie. + */ + ServerHttpCookieBuilder secure(); + + /** + * Add the "HttpOnly" attribute to the cookie. + * @see http://www.owasp.org/index.php/HTTPOnly + */ + ServerHttpCookieBuilder httpOnly(); + + /** + * Create the HttpCookie. + */ + ServerHttpCookie build(); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java index ebc5641cb1..f9301eab6d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java @@ -17,14 +17,14 @@ package org.springframework.http.server.reactive; import java.net.URI; import java.net.URISyntaxException; -import java.util.Collection; import java.util.List; import java.util.Map; -import java.util.Set; import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; import org.springframework.util.LinkedCaseInsensitiveMap; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; /** * Common base class for {@link ServerHttpRequest} implementations. @@ -37,6 +37,8 @@ public abstract class AbstractServerHttpRequest implements ServerHttpRequest { private HttpHeaders headers; + private MultiValueMap cookies; + @Override public URI getURI() { @@ -61,7 +63,7 @@ public abstract class AbstractServerHttpRequest implements ServerHttpRequest { @Override public HttpHeaders getHeaders() { if (this.headers == null) { - this.headers = new HttpHeaders(new HttpCookieInputMap()); + this.headers = new HttpHeaders(); initHeaders(this.headers); } return this.headers; @@ -74,89 +76,20 @@ public abstract class AbstractServerHttpRequest implements ServerHttpRequest { */ protected abstract void initHeaders(HttpHeaders headers); + @Override + public MultiValueMap getCookies() { + if (this.cookies == null) { + this.cookies = new LinkedMultiValueMap(); + initCookies(this.cookies); + } + return this.cookies; + } + /** * Initialize the cookies from the underlying request. Invoked lazily on the * first access to cookies via {@link #getHeaders()} and then cached. * @param cookies the map to add cookies to */ - protected abstract void initCookies(Map> cookies); - - - /** - * Read-only map of input cookies with lazy initialization. - */ - private class HttpCookieInputMap implements Map> { - - private Map> cookies; - - - private Map> getCookies() { - if (this.cookies == null) { - this.cookies = new LinkedCaseInsensitiveMap<>(); - initCookies(this.cookies); - } - return this.cookies; - } - - @Override - public int size() { - return getCookies().size(); - } - - @Override - public boolean isEmpty() { - return getCookies().isEmpty(); - } - - @Override - public boolean containsKey(Object key) { - return getCookies().containsKey(key); - } - - @Override - public boolean containsValue(Object value) { - return getCookies().containsValue(value); - } - - @Override - public List get(Object key) { - return getCookies().get(key); - } - - @Override - public Set keySet() { - return getCookies().keySet(); - } - - @Override - public Collection> values() { - return getCookies().values(); - } - - @Override - public Set>> entrySet() { - return getCookies().entrySet(); - } - - @Override - public List put(String key, List value) { - throw new UnsupportedOperationException("Can't modify client sent cookies."); - } - - @Override - public List remove(Object key) { - throw new UnsupportedOperationException("Can't modify client sent cookies."); - } - - @Override - public void putAll(Map> map) { - throw new UnsupportedOperationException("Can't modify client sent cookies."); - } - - @Override - public void clear() { - throw new UnsupportedOperationException("Can't modify client sent cookies."); - } - } + protected abstract void initCookies(MultiValueMap cookies); } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java index 42dbf1c200..e4eb3e4feb 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java @@ -17,6 +17,7 @@ package org.springframework.http.server.reactive; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; @@ -24,8 +25,13 @@ import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; +import org.springframework.http.ServerHttpCookie; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; /** @@ -37,6 +43,8 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse { private final HttpHeaders headers; + private final MultiValueMap cookies; + private AtomicReference state = new AtomicReference<>(State.NEW); private final List>> beforeCommitActions = new ArrayList<>(4); @@ -44,6 +52,7 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse { protected AbstractServerHttpResponse() { this.headers = new HttpHeaders(); + this.cookies = new LinkedMultiValueMap(); } @@ -55,6 +64,14 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse { return this.headers; } + @Override + public MultiValueMap getCookies() { + if (State.COMITTED.equals(this.state.get())) { + return CollectionUtils.unmodifiableMultiValueMap(this.cookies); + } + return this.cookies; + } + @Override public Mono setBody(Publisher publisher) { return new WriteWithOperator<>(publisher, writePublisher -> diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java index 8f40ed958d..db3bb6186e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java @@ -18,9 +18,6 @@ package org.springframework.http.server.reactive; import java.net.URI; import java.net.URISyntaxException; import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; import reactor.core.publisher.Flux; import reactor.io.buffer.Buffer; @@ -33,6 +30,7 @@ import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.util.Assert; +import org.springframework.util.MultiValueMap; /** * Adapt {@link ServerHttpRequest} to the Reactor Net {@link HttpChannel}. @@ -76,15 +74,11 @@ public class ReactorServerHttpRequest extends AbstractServerHttpRequest { } @Override - protected void initCookies(Map> cookies) { + protected void initCookies(MultiValueMap cookies) { for (String name : this.channel.cookies().keySet()) { - List list = cookies.get(name); - if (list == null) { - list = new ArrayList<>(); - cookies.put(name, list); - } for (Cookie cookie : this.channel.cookies().get(name)) { - list.add(HttpCookie.clientCookie(name, cookie.value())); + HttpCookie httpCookie = new HttpCookie(name, cookie.value()); + cookies.add(name, httpCookie); } } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java index 011b3d599a..37ec2b309e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java @@ -16,6 +16,7 @@ package org.springframework.http.server.reactive; import java.time.Duration; +import java.util.Optional; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -26,8 +27,8 @@ import reactor.io.net.http.model.Cookie; import reactor.io.net.http.model.Status; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.http.HttpCookie; import org.springframework.http.HttpStatus; +import org.springframework.http.ServerHttpCookie; import org.springframework.util.Assert; /** @@ -73,10 +74,10 @@ public class ReactorServerHttpResponse extends AbstractServerHttpResponse { @Override protected void writeCookies() { - for (String name : getHeaders().getCookies().keySet()) { - for (HttpCookie httpCookie : getHeaders().getCookies().get(name)) { - Cookie reactorCookie = new ReactorCookie(httpCookie); - this.channel.addResponseCookie(name, reactorCookie); + for (String name : getCookies().keySet()) { + for (ServerHttpCookie httpCookie : getCookies().get(name)) { + Cookie cookie = new ReactorCookie(httpCookie); + this.channel.addResponseCookie(name, cookie); } } } @@ -87,10 +88,10 @@ public class ReactorServerHttpResponse extends AbstractServerHttpResponse { */ private final static class ReactorCookie extends Cookie { - private final HttpCookie httpCookie; + private final ServerHttpCookie httpCookie; - public ReactorCookie(HttpCookie httpCookie) { + public ReactorCookie(ServerHttpCookie httpCookie) { this.httpCookie = httpCookie; } @@ -117,12 +118,14 @@ public class ReactorServerHttpResponse extends AbstractServerHttpResponse { @Override public String domain() { - return this.httpCookie.getDomain(); + Optional domain = this.httpCookie.getDomain(); + return (domain.isPresent() ? domain.get() : null); } @Override public String path() { - return this.httpCookie.getPath(); + Optional path = this.httpCookie.getPath(); + return (path.isPresent() ? path.get() : null); } @Override diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java index 7fd9e01d2a..e3b1562005 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java @@ -18,9 +18,6 @@ package org.springframework.http.server.reactive; import java.net.URI; import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; import io.netty.buffer.ByteBuf; import io.netty.handler.codec.http.cookie.Cookie; @@ -35,6 +32,7 @@ import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.util.Assert; +import org.springframework.util.MultiValueMap; /** * Adapt {@link ServerHttpRequest} to the RxNetty {@link HttpServerRequest}. @@ -79,15 +77,11 @@ public class RxNettyServerHttpRequest extends AbstractServerHttpRequest { } @Override - protected void initCookies(Map> map) { + protected void initCookies(MultiValueMap cookies) { for (String name : this.request.getCookies().keySet()) { - List list = map.get(name); - if (list == null) { - list = new ArrayList<>(); - map.put(name, list); - } for (Cookie cookie : this.request.getCookies().get(name)) { - list.add(HttpCookie.clientCookie(name, cookie.value())); + HttpCookie httpCookie = new HttpCookie(name, cookie.value()); + cookies.add(name, httpCookie); } } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java index ca064ad2b5..9cb04d6719 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java @@ -29,8 +29,8 @@ import rx.Observable; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.NettyDataBuffer; -import org.springframework.http.HttpCookie; import org.springframework.http.HttpStatus; +import org.springframework.http.ServerHttpCookie; import org.springframework.util.Assert; /** @@ -85,14 +85,18 @@ public class RxNettyServerHttpResponse extends AbstractServerHttpResponse { @Override protected void writeCookies() { - for (String name : getHeaders().getCookies().keySet()) { - for (HttpCookie httpCookie : getHeaders().getCookies().get(name)) { + for (String name : getCookies().keySet()) { + for (ServerHttpCookie httpCookie : getCookies().get(name)) { Cookie cookie = new DefaultCookie(name, httpCookie.getValue()); if (!httpCookie.getMaxAge().isNegative()) { cookie.setMaxAge(httpCookie.getMaxAge().getSeconds()); } - cookie.setDomain(httpCookie.getDomain()); - cookie.setPath(httpCookie.getPath()); + if (httpCookie.getDomain().isPresent()) { + cookie.setDomain(httpCookie.getDomain().get()); + } + if (httpCookie.getPath().isPresent()) { + cookie.setPath(httpCookie.getPath().get()); + } cookie.setSecure(httpCookie.isSecure()); cookie.setHttpOnly(httpCookie.isHttpOnly()); this.response.addCookie(cookie); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java index 90a52dd13c..695e944b0a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java @@ -16,8 +16,13 @@ package org.springframework.http.server.reactive; +import java.util.List; +import java.util.Map; + +import org.springframework.http.HttpCookie; import org.springframework.http.HttpRequest; import org.springframework.http.ReactiveHttpInputMessage; +import org.springframework.util.MultiValueMap; /** * Represents a reactive server-side HTTP request @@ -26,4 +31,9 @@ import org.springframework.http.ReactiveHttpInputMessage; */ public interface ServerHttpRequest extends HttpRequest, ReactiveHttpInputMessage { + /** + * Return a read-only map of cookies sent by the client. + */ + MultiValueMap getCookies(); + } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java index c7449eca34..c6d53276e8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java @@ -16,10 +16,16 @@ package org.springframework.http.server.reactive; +import java.util.List; +import java.util.Map; + import reactor.core.publisher.Mono; +import org.springframework.http.HttpCookie; import org.springframework.http.HttpStatus; import org.springframework.http.ReactiveHttpOutputMessage; +import org.springframework.http.ServerHttpCookie; +import org.springframework.util.MultiValueMap; /** * Represents a reactive server-side HTTP response. @@ -34,6 +40,11 @@ public interface ServerHttpResponse extends ReactiveHttpOutputMessage { */ void setStatusCode(HttpStatus status); + /** + * Return a mutable map with cookies to be sent to the client. + */ + MultiValueMap getCookies(); + /** * Indicate that request handling is complete, allowing for any cleanup or * end-of-processing tasks to be performed such as applying header changes diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java index 2374d86231..289d20b8c8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java @@ -20,9 +20,7 @@ import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.Charset; -import java.util.ArrayList; import java.util.Enumeration; -import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicLong; import javax.servlet.ReadListener; @@ -45,6 +43,7 @@ import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.util.Assert; import org.springframework.util.LinkedCaseInsensitiveMap; +import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; /** @@ -127,17 +126,13 @@ public class ServletServerHttpRequest extends AbstractServerHttpRequest { } @Override - protected void initCookies(Map> map) { + protected void initCookies(MultiValueMap httpCookies) { Cookie[] cookies = this.request.getCookies(); if (cookies != null) { for (Cookie cookie : cookies) { String name = cookie.getName(); - List list = map.get(name); - if (list == null) { - list = new ArrayList<>(); - map.put(name, list); - } - list.add(HttpCookie.clientCookie(name, cookie.getValue())); + HttpCookie httpCookie = new HttpCookie(name, cookie.getValue()); + httpCookies.add(name, httpCookie); } } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java index 2ee70f42a9..3c0a5fd9cf 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java @@ -34,9 +34,9 @@ import org.reactivestreams.Subscription; import reactor.core.publisher.Mono; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.http.HttpCookie; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.http.ServerHttpCookie; import org.springframework.util.Assert; /** @@ -98,17 +98,17 @@ public class ServletServerHttpResponse extends AbstractServerHttpResponse { @Override protected void writeCookies() { - for (String name : getHeaders().getCookies().keySet()) { - for (HttpCookie httpCookie : getHeaders().getCookies().get(name)) { + for (String name : getCookies().keySet()) { + for (ServerHttpCookie httpCookie : getCookies().get(name)) { Cookie cookie = new Cookie(name, httpCookie.getValue()); if (!httpCookie.getMaxAge().isNegative()) { cookie.setMaxAge((int) httpCookie.getMaxAge().getSeconds()); } - if (httpCookie.getDomain() != null) { - cookie.setDomain(httpCookie.getDomain()); + if (httpCookie.getDomain().isPresent()) { + cookie.setDomain(httpCookie.getDomain().get()); } - if (httpCookie.getPath() != null) { - cookie.setPath(httpCookie.getPath()); + if (httpCookie.getPath().isPresent()) { + cookie.setPath(httpCookie.getPath().get()); } cookie.setSecure(httpCookie.isSecure()); cookie.setHttpOnly(httpCookie.isHttpOnly()); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java index da38edd041..cebcc776ed 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java @@ -18,9 +18,6 @@ package org.springframework.http.server.reactive; import java.net.URI; import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; import io.undertow.server.HttpServerExchange; import io.undertow.server.handlers.Cookie; @@ -33,6 +30,7 @@ import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.util.Assert; +import org.springframework.util.MultiValueMap; /** * Adapt {@link ServerHttpRequest} to the Underow {@link HttpServerExchange}. @@ -79,15 +77,11 @@ public class UndertowServerHttpRequest extends AbstractServerHttpRequest { } @Override - protected void initCookies(Map> map) { + protected void initCookies(MultiValueMap cookies) { for (String name : this.exchange.getRequestCookies().keySet()) { - List list = map.get(name); - if (list == null) { - list = new ArrayList<>(); - map.put(name, list); - } Cookie cookie = this.exchange.getRequestCookies().get(name); - list.add(HttpCookie.clientCookie(name, cookie.getValue())); + HttpCookie httpCookie = new HttpCookie(name, cookie.getValue()); + cookies.add(name, httpCookie); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java index dcaa12d233..091bf80fdd 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java @@ -28,8 +28,8 @@ import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.http.HttpCookie; import org.springframework.http.HttpStatus; +import org.springframework.http.ServerHttpCookie; import org.springframework.util.Assert; /** @@ -80,14 +80,18 @@ public class UndertowServerHttpResponse extends AbstractServerHttpResponse { @Override protected void writeCookies() { - for (String name : getHeaders().getCookies().keySet()) { - for (HttpCookie httpCookie : getHeaders().getCookies().get(name)) { + for (String name : getCookies().keySet()) { + for (ServerHttpCookie httpCookie : getCookies().get(name)) { Cookie cookie = new CookieImpl(name, httpCookie.getValue()); if (!httpCookie.getMaxAge().isNegative()) { cookie.setMaxAge((int) httpCookie.getMaxAge().getSeconds()); } - cookie.setDomain(httpCookie.getDomain()); - cookie.setPath(httpCookie.getPath()); + if (httpCookie.getDomain().isPresent()) { + cookie.setDomain(httpCookie.getDomain().get()); + } + if (httpCookie.getPath().isPresent()) { + cookie.setPath(httpCookie.getPath().get()); + } cookie.setSecure(httpCookie.isSecure()); cookie.setHttpOnly(httpCookie.isHttpOnly()); this.exchange.getResponseCookies().putIfAbsent(name, cookie); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/session/CookieWebSessionIdResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/server/session/CookieWebSessionIdResolver.java index f325be9e67..51aeaae895 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/session/CookieWebSessionIdResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/session/CookieWebSessionIdResolver.java @@ -18,12 +18,14 @@ package org.springframework.web.server.session; import java.time.Duration; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Optional; import org.springframework.http.HttpCookie; -import org.springframework.http.HttpHeaders; +import org.springframework.http.ServerHttpCookie; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; +import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; import org.springframework.web.server.ServerWebExchange; @@ -58,7 +60,7 @@ public class CookieWebSessionIdResolver implements WebSessionIdResolver { /** * Set the value for the "Max-Age" attribute of the cookie that holds the - * session id. For the range of values see {@link HttpCookie#getMaxAge()}. + * session id. For the range of values see {@link ServerHttpCookie#getMaxAge()}. *

    By default set to -1. * @param maxAge the maxAge duration value */ @@ -76,18 +78,17 @@ public class CookieWebSessionIdResolver implements WebSessionIdResolver { @Override public Optional resolveSessionId(ServerWebExchange exchange) { - HttpHeaders headers = exchange.getRequest().getHeaders(); - List cookies = headers.getCookies().get(getCookieName()); - return (CollectionUtils.isEmpty(cookies) ? - Optional.empty() : Optional.of(cookies.get(0).getValue())); + MultiValueMap cookieMap = exchange.getRequest().getCookies(); + HttpCookie cookie = cookieMap.getFirst(getCookieName()); + return (cookie != null ? Optional.of(cookie.getValue()) : Optional.empty()); } @Override public void setSessionId(ServerWebExchange exchange, String id) { Duration maxAge = (StringUtils.hasText(id) ? getCookieMaxAge() : Duration.ofSeconds(0)); - HttpCookie cookie = HttpCookie.serverCookie(getCookieName(), id).maxAge(maxAge).build(); - HttpHeaders headers = exchange.getResponse().getHeaders(); - headers.getCookies().put(getCookieName(), Collections.singletonList(cookie)); + ServerHttpCookie cookie = ServerHttpCookie.with(getCookieName(), id).maxAge(maxAge).build(); + MultiValueMap cookieMap = exchange.getResponse().getCookies(); + cookieMap.set(getCookieName(), cookie); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/CookieIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/CookieIntegrationTests.java index 3a20401b74..547a6f0457 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/CookieIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/CookieIntegrationTests.java @@ -28,6 +28,7 @@ import reactor.core.publisher.Mono; import org.springframework.http.HttpCookie; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; +import org.springframework.http.ServerHttpCookie; import org.springframework.web.client.RestTemplate; import static org.hamcrest.CoreMatchers.equalTo; @@ -98,12 +99,12 @@ public class CookieIntegrationTests extends AbstractHttpHandlerIntegrationTests @Override public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { - this.requestCookies = request.getHeaders().getCookies(); + this.requestCookies = request.getCookies(); this.requestCookies.size(); // Cause lazy loading - response.getHeaders().addCookie(HttpCookie.serverCookie("SID", "31d4d96e407aad42") + response.getCookies().add("SID", ServerHttpCookie.with("SID", "31d4d96e407aad42") .path("/").secure().httpOnly().build()); - response.getHeaders().addCookie(HttpCookie.serverCookie("lang", "en-US") + response.getCookies().add("lang", ServerHttpCookie.with("lang", "en-US") .domain("example.com").path("/").build()); return response.setComplete(); diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpRequest.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpRequest.java index 01612d73d1..7678536a38 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpRequest.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpRequest.java @@ -21,8 +21,11 @@ import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; /** * @author Rossen Stoyanchev @@ -35,6 +38,8 @@ public class MockServerHttpRequest implements ServerHttpRequest { private HttpHeaders headers = new HttpHeaders(); + private MultiValueMap cookies = new LinkedMultiValueMap<>(); + private Flux body; @@ -74,8 +79,9 @@ public class MockServerHttpRequest implements ServerHttpRequest { return this.headers; } - public void setHeaders(HttpHeaders headers) { - this.headers = headers; + @Override + public MultiValueMap getCookies() { + return this.cookies; } @Override diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java index 4fd1d1e2b2..995846464b 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java @@ -24,6 +24,9 @@ import reactor.core.publisher.Mono; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.ServerHttpCookie; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; /** * @author Rossen Stoyanchev @@ -34,6 +37,8 @@ public class MockServerHttpResponse implements ServerHttpResponse { private HttpHeaders headers = new HttpHeaders(); + private MultiValueMap cookies = new LinkedMultiValueMap<>(); + private Publisher body; @@ -51,6 +56,11 @@ public class MockServerHttpResponse implements ServerHttpResponse { return this.headers; } + @Override + public MultiValueMap getCookies() { + return this.cookies; + } + public Publisher getBody() { return this.body; } diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java index af049f0f29..260e18bdb2 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java @@ -27,8 +27,8 @@ import reactor.core.publisher.Mono; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DefaultDataBufferAllocator; -import org.springframework.http.HttpCookie; import org.springframework.http.HttpStatus; +import org.springframework.http.ServerHttpCookie; import static junit.framework.TestCase.assertTrue; import static org.junit.Assert.assertEquals; @@ -81,17 +81,17 @@ public class ServerHttpResponseTests { @Test public void beforeCommitWithSetBody() throws Exception { - HttpCookie cookie = HttpCookie.serverCookie("ID", "123").build(); + ServerHttpCookie cookie = ServerHttpCookie.with("ID", "123").build(); TestServerHttpResponse response = new TestServerHttpResponse(); response.beforeCommit(() -> { - response.getHeaders().addCookie(cookie); + response.getCookies().add(cookie.getName(), cookie); return Mono.empty(); }); response.setBody(Flux.just(wrap("a"), wrap("b"), wrap("c"))).get(); assertTrue(response.headersWritten); assertTrue(response.cookiesWritten); - assertSame(cookie, response.getHeaders().getCookies().get("ID").get(0)); + assertSame(cookie, response.getCookies().getFirst("ID")); assertEquals(3, response.content.size()); assertEquals("a", new String(response.content.get(0).asByteBuffer().array(), UTF_8)); @@ -108,7 +108,7 @@ public class ServerHttpResponseTests { assertTrue("beforeCommit action errors should be ignored", response.headersWritten); assertTrue("beforeCommit action errors should be ignored", response.cookiesWritten); - assertNull(response.getHeaders().getCookies().get("ID")); + assertNull(response.getCookies().get("ID")); assertEquals(3, response.content.size()); assertEquals("a", new String(response.content.get(0).asByteBuffer().array(), UTF_8)); @@ -118,10 +118,10 @@ public class ServerHttpResponseTests { @Test public void beforeCommitActionWithSetComplete() throws Exception { - HttpCookie cookie = HttpCookie.serverCookie("ID", "123").build(); + ServerHttpCookie cookie = ServerHttpCookie.with("ID", "123").build(); TestServerHttpResponse response = new TestServerHttpResponse(); response.beforeCommit(() -> { - response.getHeaders().addCookie(cookie); + response.getCookies().add(cookie.getName(), cookie); return Mono.empty(); }); response.setComplete().get(); @@ -129,7 +129,7 @@ public class ServerHttpResponseTests { assertTrue(response.headersWritten); assertTrue(response.cookiesWritten); assertTrue(response.content.isEmpty()); - assertSame(cookie, response.getHeaders().getCookies().get("ID").get(0)); + assertSame(cookie, response.getCookies().getFirst("ID")); } From ca7aeaae35469bfaa6ddcd02336f23948c6fa30e Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 3 Mar 2016 22:32:03 -0500 Subject: [PATCH 244/498] WebSessionIdResolver now returns List of ids Issue: #64 --- .../session/CookieWebSessionIdResolver.java | 13 +++++----- .../session/DefaultWebSessionManager.java | 12 ++++----- .../server/session/WebSessionIdResolver.java | 8 +++--- .../DefaultWebSessionManagerTests.java | 26 ++++++++++--------- 4 files changed, 31 insertions(+), 28 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/session/CookieWebSessionIdResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/server/session/CookieWebSessionIdResolver.java index 51aeaae895..8530117ccc 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/session/CookieWebSessionIdResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/session/CookieWebSessionIdResolver.java @@ -18,13 +18,11 @@ package org.springframework.web.server.session; import java.time.Duration; import java.util.Collections; import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.stream.Collectors; import org.springframework.http.HttpCookie; import org.springframework.http.ServerHttpCookie; import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; import org.springframework.web.server.ServerWebExchange; @@ -77,10 +75,13 @@ public class CookieWebSessionIdResolver implements WebSessionIdResolver { @Override - public Optional resolveSessionId(ServerWebExchange exchange) { + public List resolveSessionId(ServerWebExchange exchange) { MultiValueMap cookieMap = exchange.getRequest().getCookies(); - HttpCookie cookie = cookieMap.getFirst(getCookieName()); - return (cookie != null ? Optional.of(cookie.getValue()) : Optional.empty()); + List cookies = cookieMap.get(getCookieName()); + if (cookies == null) { + return Collections.emptyList(); + } + return cookies.stream().map(HttpCookie::getValue).collect(Collectors.toList()); } @Override diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/session/DefaultWebSessionManager.java b/spring-web-reactive/src/main/java/org/springframework/web/server/session/DefaultWebSessionManager.java index 8c1756c313..5bcb233b95 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/session/DefaultWebSessionManager.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/session/DefaultWebSessionManager.java @@ -17,9 +17,10 @@ package org.springframework.web.server.session; import java.time.Clock; import java.time.Instant; -import java.util.Optional; +import java.util.List; import java.util.UUID; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.util.Assert; @@ -98,9 +99,8 @@ public class DefaultWebSessionManager implements WebSessionManager { @Override public Mono getSession(ServerWebExchange exchange) { - return Mono.fromCallable(() -> getSessionIdResolver().resolveSessionId(exchange)) - .where(Optional::isPresent) - .map(Optional::get) + return Flux.fromIterable(getSessionIdResolver().resolveSessionId(exchange)) + .next() .then(this.sessionStore::retrieveSession) .then(session -> validateSession(exchange, session)) .otherwiseIfEmpty(createSession(exchange)) @@ -147,8 +147,8 @@ public class DefaultWebSessionManager implements WebSessionManager { // Force explicit start session.start(); - Optional requestedId = getSessionIdResolver().resolveSessionId(exchange); - if (!requestedId.isPresent() || !session.getId().equals(requestedId.get())) { + List requestedIds = getSessionIdResolver().resolveSessionId(exchange); + if (requestedIds.isEmpty() || !session.getId().equals(requestedIds.get(0))) { this.sessionIdResolver.setSessionId(exchange, session.getId()); } return this.sessionStore.storeSession(session); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/session/WebSessionIdResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/server/session/WebSessionIdResolver.java index 151952716b..a57a1cfa7e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/session/WebSessionIdResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/session/WebSessionIdResolver.java @@ -15,7 +15,7 @@ */ package org.springframework.web.server.session; -import java.util.Optional; +import java.util.List; import org.springframework.web.server.ServerWebExchange; @@ -31,11 +31,11 @@ import org.springframework.web.server.ServerWebExchange; public interface WebSessionIdResolver { /** - * Resolve the session id associated with the request. + * Resolve the session id's associated with the request. * @param exchange the current exchange - * @return the session id if present + * @return the session id's or an empty list */ - Optional resolveSessionId(ServerWebExchange exchange); + List resolveSessionId(ServerWebExchange exchange); /** * Send the given session id to the client or if the session id is "null" diff --git a/spring-web-reactive/src/test/java/org/springframework/web/server/session/DefaultWebSessionManagerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/server/session/DefaultWebSessionManagerTests.java index 5342f97f1a..c7c17b8a6a 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/server/session/DefaultWebSessionManagerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/server/session/DefaultWebSessionManagerTests.java @@ -19,7 +19,9 @@ import java.net.URI; import java.time.Clock; import java.time.Duration; import java.time.Instant; -import java.util.Optional; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import org.junit.Before; import org.junit.Test; @@ -27,9 +29,9 @@ import org.junit.Test; import org.springframework.http.HttpMethod; import org.springframework.http.server.reactive.MockServerHttpRequest; import org.springframework.http.server.reactive.MockServerHttpResponse; -import org.springframework.web.server.adapter.DefaultServerWebExchange; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebSession; +import org.springframework.web.server.adapter.DefaultServerWebExchange; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -64,7 +66,7 @@ public class DefaultWebSessionManagerTests { @Test public void getSessionPassive() throws Exception { - this.idResolver.setIdToResolve(Optional.empty()); + this.idResolver.setIdsToResolve(Collections.emptyList()); WebSession session = this.manager.getSession(this.exchange).get(); assertNotNull(session); @@ -79,7 +81,7 @@ public class DefaultWebSessionManagerTests { @Test public void getSessionForceCreate() throws Exception { - this.idResolver.setIdToResolve(Optional.empty()); + this.idResolver.setIdsToResolve(Collections.emptyList()); WebSession session = this.manager.getSession(this.exchange).get(); session.start(); session.save(); @@ -92,7 +94,7 @@ public class DefaultWebSessionManagerTests { @Test public void getSessionAddAttribute() throws Exception { - this.idResolver.setIdToResolve(Optional.empty()); + this.idResolver.setIdsToResolve(Collections.emptyList()); WebSession session = this.manager.getSession(this.exchange).get(); session.getAttributes().put("foo", "bar"); session.save(); @@ -105,7 +107,7 @@ public class DefaultWebSessionManagerTests { DefaultWebSession existing = new DefaultWebSession("1", Clock.systemDefaultZone()); this.manager.getSessionStore().storeSession(existing); - this.idResolver.setIdToResolve(Optional.of("1")); + this.idResolver.setIdsToResolve(Collections.singletonList("1")); WebSession actual = this.manager.getSession(this.exchange).get(); assertSame(existing, actual); } @@ -118,7 +120,7 @@ public class DefaultWebSessionManagerTests { existing.setLastAccessTime(Instant.now(clock).minus(Duration.ofMinutes(31))); this.manager.getSessionStore().storeSession(existing); - this.idResolver.setIdToResolve(Optional.of("1")); + this.idResolver.setIdsToResolve(Collections.singletonList("1")); WebSession actual = this.manager.getSession(this.exchange).get(); assertNotSame(existing, actual); } @@ -126,13 +128,13 @@ public class DefaultWebSessionManagerTests { private static class TestWebSessionIdResolver implements WebSessionIdResolver { - private Optional idToResolve = Optional.empty(); + private List idsToResolve = new ArrayList<>(); private String id = null; - public void setIdToResolve(Optional idToResolve) { - this.idToResolve = idToResolve; + public void setIdsToResolve(List idsToResolve) { + this.idsToResolve = idsToResolve; } public String getId() { @@ -140,8 +142,8 @@ public class DefaultWebSessionManagerTests { } @Override - public Optional resolveSessionId(ServerWebExchange exchange) { - return this.idToResolve; + public List resolveSessionId(ServerWebExchange exchange) { + return this.idsToResolve; } @Override From 1eadee5655e9d69d04afb83984438db7bc1363b9 Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Mon, 7 Mar 2016 13:57:27 +0000 Subject: [PATCH 245/498] integration build with Reactor Core 2.5.0.M2 --- spring-web-reactive/build.gradle | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index c96b0a7754..4c76fb02e0 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -22,6 +22,7 @@ repositories { mavenLocal() mavenCentral() maven { url 'https://oss.jfrog.org/libs-snapshot' } // RxNetty 0.5.x snapshots + maven { url 'http://repo.spring.io/milestone' } // Reactor milestone maven { url 'http://repo.spring.io/snapshot' } // Reactor snapshot } @@ -32,13 +33,14 @@ configurations.all { ext { springVersion = '4.2.3.RELEASE' - reactorVersion = '2.5.0.BUILD-SNAPSHOT' + reactorVersion = '2.5.0.M2' + reactorNettyVersion = '2.5.0.BUILD-SNAPSHOT' tomcatVersion = '8.0.28' jettyVersion = '9.3.5.v20151012' javadocLinks = [ "http://docs.oracle.com/javase/8/docs/api/", - "http://next.projectreactor.io/core/docs/api/", + "http://projectreactor.io/core/docs/api/", "http://docs.spring.io/spring/docs/${springVersion}/javadoc-api/", "http://www.reactive-streams.org/reactive-streams-1.0.0-javadoc/" ] as String[] @@ -88,8 +90,7 @@ dependencies { optional 'io.reactivex:rxjava:1.1.0' optional "io.reactivex:rxnetty-http:0.5.0-SNAPSHOT" optional "com.fasterxml.jackson.core:jackson-databind:2.6.2" - optional "io.projectreactor:reactor-net:${reactorVersion}" - optional "io.projectreactor:reactor-io:${reactorVersion}" + optional "io.projectreactor:reactor-net:${reactorNettyVersion}" optional "org.apache.tomcat:tomcat-util:${tomcatVersion}" optional "org.apache.tomcat.embed:tomcat-embed-core:${tomcatVersion}" optional 'io.undertow:undertow-core:1.3.5.Final' From b2648f84addf424256221369892548f1fb9e45d2 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 7 Mar 2016 08:32:24 -0500 Subject: [PATCH 246/498] DefaultWebSessionManager supports multiple sessions Issue: #64 --- .../session/CookieWebSessionIdResolver.java | 2 +- .../session/DefaultWebSessionManager.java | 6 +-- .../server/session/WebSessionIdResolver.java | 2 +- .../DefaultWebSessionManagerTests.java | 50 +++++++++++-------- .../session/WebSessionIntegrationTests.java | 14 +++--- 5 files changed, 40 insertions(+), 34 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/session/CookieWebSessionIdResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/server/session/CookieWebSessionIdResolver.java index 8530117ccc..1d1fe75bad 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/session/CookieWebSessionIdResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/session/CookieWebSessionIdResolver.java @@ -75,7 +75,7 @@ public class CookieWebSessionIdResolver implements WebSessionIdResolver { @Override - public List resolveSessionId(ServerWebExchange exchange) { + public List resolveSessionIds(ServerWebExchange exchange) { MultiValueMap cookieMap = exchange.getRequest().getCookies(); List cookies = cookieMap.get(getCookieName()); if (cookies == null) { diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/session/DefaultWebSessionManager.java b/spring-web-reactive/src/main/java/org/springframework/web/server/session/DefaultWebSessionManager.java index 5bcb233b95..1eb0e84871 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/session/DefaultWebSessionManager.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/session/DefaultWebSessionManager.java @@ -99,9 +99,9 @@ public class DefaultWebSessionManager implements WebSessionManager { @Override public Mono getSession(ServerWebExchange exchange) { - return Flux.fromIterable(getSessionIdResolver().resolveSessionId(exchange)) + return Flux.fromIterable(getSessionIdResolver().resolveSessionIds(exchange)) + .concatMap(this.sessionStore::retrieveSession) .next() - .then(this.sessionStore::retrieveSession) .then(session -> validateSession(exchange, session)) .otherwiseIfEmpty(createSession(exchange)) .map(session -> extendSession(exchange, session)); @@ -147,7 +147,7 @@ public class DefaultWebSessionManager implements WebSessionManager { // Force explicit start session.start(); - List requestedIds = getSessionIdResolver().resolveSessionId(exchange); + List requestedIds = getSessionIdResolver().resolveSessionIds(exchange); if (requestedIds.isEmpty() || !session.getId().equals(requestedIds.get(0))) { this.sessionIdResolver.setSessionId(exchange, session.getId()); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/session/WebSessionIdResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/server/session/WebSessionIdResolver.java index a57a1cfa7e..77884e7370 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/session/WebSessionIdResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/session/WebSessionIdResolver.java @@ -35,7 +35,7 @@ public interface WebSessionIdResolver { * @param exchange the current exchange * @return the session id's or an empty list */ - List resolveSessionId(ServerWebExchange exchange); + List resolveSessionIds(ServerWebExchange exchange); /** * Send the given session id to the client or if the session id is "null" diff --git a/spring-web-reactive/src/test/java/org/springframework/web/server/session/DefaultWebSessionManagerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/server/session/DefaultWebSessionManagerTests.java index c7c17b8a6a..13efe415b6 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/server/session/DefaultWebSessionManagerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/server/session/DefaultWebSessionManagerTests.java @@ -20,6 +20,7 @@ import java.time.Clock; import java.time.Duration; import java.time.Instant; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -45,17 +46,15 @@ import static org.junit.Assert.assertSame; */ public class DefaultWebSessionManagerTests { - private DefaultWebSessionManager manager; + private final DefaultWebSessionManager manager = new DefaultWebSessionManager(); - private TestWebSessionIdResolver idResolver; + private final TestWebSessionIdResolver idResolver = new TestWebSessionIdResolver(); private DefaultServerWebExchange exchange; @Before public void setUp() throws Exception { - this.idResolver = new TestWebSessionIdResolver(); - this.manager = new DefaultWebSessionManager(); this.manager.setSessionIdResolver(this.idResolver); MockServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/path")); @@ -65,66 +64,73 @@ public class DefaultWebSessionManagerTests { @Test - public void getSessionPassive() throws Exception { + public void getSessionWithoutStarting() throws Exception { this.idResolver.setIdsToResolve(Collections.emptyList()); WebSession session = this.manager.getSession(this.exchange).get(); - - assertNotNull(session); - assertFalse(session.isStarted()); - assertFalse(session.isExpired()); - session.save(); - assertNull(this.idResolver.getId()); + assertFalse(session.isStarted()); + assertFalse(session.isExpired()); + assertNull(this.idResolver.getSavedId()); assertNull(this.manager.getSessionStore().retrieveSession(session.getId()).get()); } @Test - public void getSessionForceCreate() throws Exception { + public void startSessionExplicitly() throws Exception { this.idResolver.setIdsToResolve(Collections.emptyList()); WebSession session = this.manager.getSession(this.exchange).get(); session.start(); session.save(); String id = session.getId(); - assertNotNull(this.idResolver.getId()); - assertEquals(id, this.idResolver.getId()); + assertNotNull(this.idResolver.getSavedId()); + assertEquals(id, this.idResolver.getSavedId()); assertSame(session, this.manager.getSessionStore().retrieveSession(id).get()); } @Test - public void getSessionAddAttribute() throws Exception { + public void startSessionImplicitly() throws Exception { this.idResolver.setIdsToResolve(Collections.emptyList()); WebSession session = this.manager.getSession(this.exchange).get(); session.getAttributes().put("foo", "bar"); session.save(); - assertNotNull(this.idResolver.getId()); + assertNotNull(this.idResolver.getSavedId()); } @Test - public void getSessionExisting() throws Exception { + public void existingSession() throws Exception { DefaultWebSession existing = new DefaultWebSession("1", Clock.systemDefaultZone()); this.manager.getSessionStore().storeSession(existing); - this.idResolver.setIdsToResolve(Collections.singletonList("1")); + WebSession actual = this.manager.getSession(this.exchange).get(); assertSame(existing, actual); } @Test - public void getSessionExistingExpired() throws Exception { + public void existingSessionIsExpired() throws Exception { Clock clock = Clock.systemDefaultZone(); DefaultWebSession existing = new DefaultWebSession("1", clock); existing.start(); existing.setLastAccessTime(Instant.now(clock).minus(Duration.ofMinutes(31))); this.manager.getSessionStore().storeSession(existing); - this.idResolver.setIdsToResolve(Collections.singletonList("1")); + WebSession actual = this.manager.getSession(this.exchange).get(); assertNotSame(existing, actual); } + @Test + public void multipleSessions() throws Exception { + DefaultWebSession existing = new DefaultWebSession("3", Clock.systemDefaultZone()); + this.manager.getSessionStore().storeSession(existing); + this.idResolver.setIdsToResolve(Arrays.asList("1", "2", "3")); + + WebSession actual = this.manager.getSession(this.exchange).get(); + assertSame(existing, actual); + } + private static class TestWebSessionIdResolver implements WebSessionIdResolver { @@ -137,12 +143,12 @@ public class DefaultWebSessionManagerTests { this.idsToResolve = idsToResolve; } - public String getId() { + public String getSavedId() { return this.id; } @Override - public List resolveSessionId(ServerWebExchange exchange) { + public List resolveSessionIds(ServerWebExchange exchange) { return this.idsToResolve; } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/server/session/WebSessionIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/server/session/WebSessionIntegrationTests.java index cc5cfdf437..45cde4ff18 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/server/session/WebSessionIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/server/session/WebSessionIntegrationTests.java @@ -43,8 +43,9 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; - /** + * Integration tests for with a server-side session. + * * @author Rossen Stoyanchev */ public class WebSessionIntegrationTests extends AbstractHttpHandlerIntegrationTests { @@ -95,6 +96,8 @@ public class WebSessionIntegrationTests extends AbstractHttpHandlerIntegrationTe @Test public void expiredSession() throws Exception { + + // First request: no session yet, new session created RequestEntity request = RequestEntity.get(createUri("/")).build(); ResponseEntity response = this.restTemplate.exchange(request, Void.class); @@ -103,11 +106,11 @@ public class WebSessionIntegrationTests extends AbstractHttpHandlerIntegrationTe assertNotNull(id); assertEquals(1, this.handler.getCount()); - // Set clock back 31 minutes + // Set (server-side) clock back 31 minutes Clock clock = this.sessionManager.getClock(); this.sessionManager.setClock(Clock.offset(clock, Duration.ofMinutes(-31))); - // Access again to update lastAccessTime + // Second request: lastAccessTime updated (with offset clock) request = RequestEntity.get(createUri("/")).header("Cookie", "SESSION=" + id).build(); response = this.restTemplate.exchange(request, Void.class); @@ -115,7 +118,7 @@ public class WebSessionIntegrationTests extends AbstractHttpHandlerIntegrationTe assertNull(response.getHeaders().get("Set-Cookie")); assertEquals(2, this.handler.getCount()); - // Now it should be expired + // Third request: new session replaces expired session request = RequestEntity.get(createUri("/")).header("Cookie", "SESSION=" + id).build(); response = this.restTemplate.exchange(request, Void.class); @@ -125,9 +128,6 @@ public class WebSessionIntegrationTests extends AbstractHttpHandlerIntegrationTe assertEquals("Expected new session attribute", 1, this.handler.getCount()); } - - // No client side HttpCookie support yet - private String extractSessionId(HttpHeaders headers) { List headerValues = headers.get("Set-Cookie"); assertNotNull(headerValues); From e97a55ad54b952c2489d448b6c22664caf3d3330 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 7 Mar 2016 17:07:12 -0500 Subject: [PATCH 247/498] WebClient supports Void response body WebResponseExtractor uses Mono.when with the response status, headers, and the decoded body. However when the response body stream is empty then when completes empty too. This change adds defaultIfEmpty handling for en empty response body. --- .../reactive/WebResponseExtractors.java | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebResponseExtractors.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebResponseExtractors.java index 9c24c76d35..ac4331b6cb 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebResponseExtractors.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebResponseExtractors.java @@ -42,6 +42,9 @@ public class WebResponseExtractors { private static final Object[] HINTS = new Object[] {UTF_8}; + private static final Object EMPTY_BODY = new Object(); + + /** * Extract the response body and decode it, returning it as a {@code Mono} */ @@ -68,18 +71,21 @@ public class WebResponseExtractors { * Extract the full response body as a {@code ResponseEntity} * with its body decoded as a single type {@code T} */ - public static WebResponseExtractor>> response(Class sourceClass) { + public static WebResponseExtractor>> response(Class bodyClass) { - ResolvableType resolvableType = ResolvableType.forClass(sourceClass); + ResolvableType bodyType = ResolvableType.forClass(bodyClass); return webResponse -> webResponse.getClientResponse() - .then(response -> - Mono.when( - decodeResponseBody(response, resolvableType, webResponse.getMessageDecoders()).next(), - Mono.just(response.getHeaders()), - Mono.just(response.getStatusCode()))) + .then(response -> { + List> decoders = webResponse.getMessageDecoders(); + return Mono.when( + decodeResponseBody(response, bodyType, decoders).next().defaultIfEmpty(EMPTY_BODY), + Mono.just(response.getHeaders()), + Mono.just(response.getStatusCode())); + }) .map(tuple -> { + Object body = (tuple.getT1() != EMPTY_BODY ? tuple.getT1() : null); //noinspection unchecked - return new ResponseEntity<>((T) tuple.getT1(), tuple.getT2(), tuple.getT3()); + return new ResponseEntity<>((T) body, tuple.getT2(), tuple.getT3()); }); } From f02492e15faa3884a9f8bb574482b40f58427841 Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Fri, 11 Mar 2016 20:02:34 +0000 Subject: [PATCH 248/498] sync with reactor-core and reactor-io --- spring-web-reactive/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index 4c76fb02e0..cffddc1c5e 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -90,7 +90,7 @@ dependencies { optional 'io.reactivex:rxjava:1.1.0' optional "io.reactivex:rxnetty-http:0.5.0-SNAPSHOT" optional "com.fasterxml.jackson.core:jackson-databind:2.6.2" - optional "io.projectreactor:reactor-net:${reactorNettyVersion}" + optional "io.projectreactor:reactor-netty:${reactorNettyVersion}" optional "org.apache.tomcat:tomcat-util:${tomcatVersion}" optional "org.apache.tomcat.embed:tomcat-embed-core:${tomcatVersion}" optional 'io.undertow:undertow-core:1.3.5.Final' From 13ddfb4aabe5ca31e8f3a882bce3528b4a7ccbf3 Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Fri, 11 Mar 2016 22:15:11 +0000 Subject: [PATCH 249/498] update package --- .../http/client/reactive/ReactorClientHttpRequest.java | 4 ++-- .../http/client/reactive/ReactorClientHttpResponse.java | 4 ++-- .../client/reactive/ReactorHttpClientRequestFactory.java | 4 ++-- .../http/server/reactive/ReactorHttpHandlerAdapter.java | 6 +++--- .../http/server/reactive/ReactorServerHttpRequest.java | 4 ++-- .../http/server/reactive/ReactorServerHttpResponse.java | 6 +++--- .../http/server/reactive/boot/ReactorHttpServer.java | 4 ++-- 7 files changed, 16 insertions(+), 16 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java index e80a56ae5c..81ec72a3d4 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java @@ -22,8 +22,8 @@ import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.io.buffer.Buffer; -import reactor.io.net.http.HttpClient; -import reactor.io.net.http.model.Method; +import reactor.io.netty.http.HttpClient; +import reactor.io.netty.http.model.Method; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferAllocator; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpResponse.java index 4eaac99918..937d26d07b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpResponse.java @@ -20,7 +20,7 @@ import java.nio.ByteBuffer; import reactor.core.publisher.Flux; import reactor.io.buffer.Buffer; -import reactor.io.net.http.HttpChannel; +import reactor.io.netty.http.HttpChannel; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferAllocator; @@ -31,7 +31,7 @@ import org.springframework.http.HttpStatus; * {@link ClientHttpResponse} implementation for the Reactor Net HTTP client * * @author Brian Clozel - * @see reactor.io.net.http.HttpClient + * @see reactor.io.netty.http.HttpClient */ public class ReactorClientHttpResponse implements ClientHttpResponse { diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorHttpClientRequestFactory.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorHttpClientRequestFactory.java index f3f83b95ae..e759e3b85f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorHttpClientRequestFactory.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorHttpClientRequestFactory.java @@ -18,8 +18,8 @@ package org.springframework.http.client.reactive; import java.net.URI; -import reactor.io.net.ReactiveNet; -import reactor.io.net.http.HttpClient; +import reactor.io.netty.ReactiveNet; +import reactor.io.netty.http.HttpClient; import org.springframework.core.io.buffer.DataBufferAllocator; import org.springframework.core.io.buffer.DefaultDataBufferAllocator; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorHttpHandlerAdapter.java index 15a78c126e..cf7136a4fa 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorHttpHandlerAdapter.java @@ -17,8 +17,8 @@ package org.springframework.http.server.reactive; import reactor.core.publisher.Mono; import reactor.io.buffer.Buffer; -import reactor.io.net.ReactiveChannelHandler; -import reactor.io.net.http.HttpChannel; +import reactor.io.ipc.ChannelFluxHandler; +import reactor.io.netty.http.HttpChannel; import org.springframework.core.io.buffer.DataBufferAllocator; import org.springframework.util.Assert; @@ -27,7 +27,7 @@ import org.springframework.util.Assert; * @author Stephane Maldini */ public class ReactorHttpHandlerAdapter - implements ReactiveChannelHandler> { + implements ChannelFluxHandler> { private final HttpHandler httpHandler; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java index db3bb6186e..cb4f58fc50 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java @@ -21,8 +21,8 @@ import java.nio.ByteBuffer; import reactor.core.publisher.Flux; import reactor.io.buffer.Buffer; -import reactor.io.net.http.HttpChannel; -import reactor.io.net.http.model.Cookie; +import reactor.io.netty.http.HttpChannel; +import reactor.io.netty.http.model.Cookie; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferAllocator; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java index 37ec2b309e..0e57f1e2c7 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java @@ -22,9 +22,9 @@ import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.io.buffer.Buffer; -import reactor.io.net.http.HttpChannel; -import reactor.io.net.http.model.Cookie; -import reactor.io.net.http.model.Status; +import reactor.io.netty.http.HttpChannel; +import reactor.io.netty.http.model.Cookie; +import reactor.io.netty.http.model.Status; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpStatus; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java index 9e61091331..5cbd81bfea 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java @@ -19,7 +19,7 @@ package org.springframework.http.server.reactive.boot; import reactor.core.flow.Loopback; import reactor.core.state.Completable; import reactor.io.buffer.Buffer; -import reactor.io.net.ReactiveNet; +import reactor.io.netty.ReactiveNet; import org.springframework.core.io.buffer.DataBufferAllocator; import org.springframework.core.io.buffer.DefaultDataBufferAllocator; @@ -34,7 +34,7 @@ public class ReactorHttpServer extends HttpServerSupport private ReactorHttpHandlerAdapter reactorHandler; - private reactor.io.net.http.HttpServer reactorServer; + private reactor.io.netty.http.HttpServer reactorServer; private DataBufferAllocator allocator = new DefaultDataBufferAllocator(); From 530c817287a63004b827535f6a9702821131aaad Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Sat, 12 Mar 2016 01:16:49 +0000 Subject: [PATCH 250/498] sync reactor-netty --- .../client/reactive/ReactorHttpClientRequestFactory.java | 3 +-- .../http/server/reactive/boot/ReactorHttpServer.java | 5 ++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorHttpClientRequestFactory.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorHttpClientRequestFactory.java index e759e3b85f..ce0cd81812 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorHttpClientRequestFactory.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorHttpClientRequestFactory.java @@ -18,7 +18,6 @@ package org.springframework.http.client.reactive; import java.net.URI; -import reactor.io.netty.ReactiveNet; import reactor.io.netty.http.HttpClient; import org.springframework.core.io.buffer.DataBufferAllocator; @@ -43,7 +42,7 @@ public class ReactorHttpClientRequestFactory implements ClientHttpRequestFactory } public ReactorHttpClientRequestFactory(DataBufferAllocator allocator) { - this(allocator, ReactiveNet.httpClient()); + this(allocator, reactor.io.netty.http.HttpClient.create()); } protected ReactorHttpClientRequestFactory(DataBufferAllocator allocator, HttpClient httpClient) { diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java index 5cbd81bfea..4887a6741d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java @@ -19,7 +19,6 @@ package org.springframework.http.server.reactive.boot; import reactor.core.flow.Loopback; import reactor.core.state.Completable; import reactor.io.buffer.Buffer; -import reactor.io.netty.ReactiveNet; import org.springframework.core.io.buffer.DataBufferAllocator; import org.springframework.core.io.buffer.DefaultDataBufferAllocator; @@ -50,8 +49,8 @@ public class ReactorHttpServer extends HttpServerSupport Assert.notNull(getHttpHandler()); this.reactorHandler = new ReactorHttpHandlerAdapter(getHttpHandler(), allocator); - this.reactorServer = (getPort() != -1 ? ReactiveNet.httpServer(getPort()) : - ReactiveNet.httpServer()); + this.reactorServer = (getPort() != -1 ? reactor.io.netty.http.HttpServer.create(getPort()) : + reactor.io.netty.http.HttpServer.create()); } @Override From 10a04b0769866e0d022658eaf57c9c705a088951 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Thu, 17 Mar 2016 10:21:04 +0100 Subject: [PATCH 251/498] Test improvements. --- .../method/annotation/RequestMappingIntegrationTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java index 3abde708ed..bd75c45429 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java @@ -468,7 +468,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @RequestMapping("/stream-result") public Publisher stringStreamResponseBody() { - return Flux.interval(Duration.ofSeconds(1)).take(5); + return Flux.interval(Duration.ofMillis(100)).take(5); } @RequestMapping("/raw-flux") From 62753102dcc3acfc1bd5053b59163859aa2fed00 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Thu, 17 Mar 2016 10:11:12 -0500 Subject: [PATCH 252/498] Ensure a consistent netty version Currently there are clashes between io.netty:netty-common:4.1.0.Beta7 and io.netty:netty-all:4.1.0.CR3 which can cause errors in the build related to "VerifyError: Bad type on operand stack". One solution would be to exclude the jars that duplicate the classes. However, this can be fragile since additional dependencies may be added that bring in the dependency transitively. This commit locks the version for any artifiact with the group "io.nettty" to ensure the correct version of netty is used. --- spring-web-reactive/build.gradle | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index cffddc1c5e..3d7e230b9a 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -26,17 +26,13 @@ repositories { maven { url 'http://repo.spring.io/snapshot' } // Reactor snapshot } -configurations.all { - // check for updates every build - resolutionStrategy.cacheChangingModulesFor 0, 'seconds' -} - ext { springVersion = '4.2.3.RELEASE' reactorVersion = '2.5.0.M2' reactorNettyVersion = '2.5.0.BUILD-SNAPSHOT' tomcatVersion = '8.0.28' jettyVersion = '9.3.5.v20151012' + nettyVersion = '4.1.0.CR3' javadocLinks = [ "http://docs.oracle.com/javase/8/docs/api/", @@ -46,6 +42,18 @@ ext { ] as String[] } +configurations.all { + // check for updates every build + resolutionStrategy.cacheChangingModulesFor 0, 'seconds' + resolutionStrategy.eachDependency { DependencyResolveDetails details -> + // consistent netty version to avoid issues with clashes in netty-all vs + // netty-common for example + if (details.requested.group == 'io.netty') { + details.useVersion nettyVersion + } + } +} + uploadArchives { repositories { mavenDeployer { From ececec21f7b084bfeb57b737f52e1bbadaa2be16 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Thu, 17 Mar 2016 10:11:28 -0500 Subject: [PATCH 253/498] Update to Gradle 2.12 --- spring-web-reactive/gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-web-reactive/gradle/wrapper/gradle-wrapper.properties b/spring-web-reactive/gradle/wrapper/gradle-wrapper.properties index d785e9887a..329a105dc7 100644 --- a/spring-web-reactive/gradle/wrapper/gradle-wrapper.properties +++ b/spring-web-reactive/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.12-all.zip From e19abf9c90cb5f0d093dcd40e490ff161bd199d3 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 17 Mar 2016 15:28:55 -0400 Subject: [PATCH 254/498] Rename ServerCookie to ResponseCookie HttpCookie-ResponseCookie follows HttpEntity-ResponseEntity and also avoids use of "Server" outside of server sub-package. --- .../org/springframework/http/HttpCookie.java | 7 ++- ...verHttpCookie.java => ResponseCookie.java} | 49 +++++++++---------- .../reactive/AbstractServerHttpResponse.java | 10 ++-- .../reactive/ReactorServerHttpResponse.java | 8 +-- .../reactive/RxNettyServerHttpResponse.java | 4 +- .../server/reactive/ServerHttpResponse.java | 8 +-- .../reactive/ServletServerHttpResponse.java | 4 +- .../reactive/UndertowServerHttpResponse.java | 4 +- .../session/CookieWebSessionIdResolver.java | 8 +-- .../reactive/CookieIntegrationTests.java | 6 +-- .../reactive/MockServerHttpResponse.java | 6 +-- .../reactive/ServerHttpResponseTests.java | 6 +-- 12 files changed, 56 insertions(+), 64 deletions(-) rename spring-web-reactive/src/main/java/org/springframework/http/{ServerHttpCookie.java => ResponseCookie.java} (77%) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/HttpCookie.java b/spring-web-reactive/src/main/java/org/springframework/http/HttpCookie.java index 3f1675a4a9..34f57ae8be 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/HttpCookie.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/HttpCookie.java @@ -18,10 +18,9 @@ package org.springframework.http; import org.springframework.util.Assert; /** - * Represents an HTTP Cookie with a name and value. - * - *

    The {@link ServerHttpCookie} sub-class exposes the extra attributes that - * a server can include in a Set-Cookie response header. + * Represents an HTTP cookie as a name-value pair consistent with the content of + * the "Cookie" request header. The {@link ResponseCookie} sub-class has the + * additional attributes expected in the "Set-Cookie" response header. * * @author Rossen Stoyanchev * @see RFC 6265 diff --git a/spring-web-reactive/src/main/java/org/springframework/http/ServerHttpCookie.java b/spring-web-reactive/src/main/java/org/springframework/http/ResponseCookie.java similarity index 77% rename from spring-web-reactive/src/main/java/org/springframework/http/ServerHttpCookie.java rename to spring-web-reactive/src/main/java/org/springframework/http/ResponseCookie.java index 4d7d698d6e..13d9adbc45 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/ServerHttpCookie.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/ResponseCookie.java @@ -22,15 +22,14 @@ import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; /** - * Represents a server-side cookie with extra attributes that a server can - * include in a Set-Cookie response header. - * - *

    Use {@link #with} to create a {@code ServerHttpCookie}. + * An {@code HttpCookie} sub-class with the additional attributes allowed in + * the "Set-Cookie" response header. To build an instance use the {@link #from} + * static method. * * @author Rossen Stoyanchev * @see RFC 6265 */ -public final class ServerHttpCookie extends HttpCookie { +public final class ResponseCookie extends HttpCookie { private final Duration maxAge; @@ -44,9 +43,9 @@ public final class ServerHttpCookie extends HttpCookie { /** - * Private constructor. See {@link #with(String, String)}. + * Private constructor. See {@link #from(String, String)}. */ - private ServerHttpCookie(String name, String value, Duration maxAge, String domain, + private ResponseCookie(String name, String value, Duration maxAge, String domain, String path, boolean secure, boolean httpOnly) { super(name, value); @@ -113,10 +112,10 @@ public final class ServerHttpCookie extends HttpCookie { if (this == other) { return true; } - if (!(other instanceof ServerHttpCookie)) { + if (!(other instanceof ResponseCookie)) { return false; } - ServerHttpCookie otherCookie = (ServerHttpCookie) other; + ResponseCookie otherCookie = (ResponseCookie) other; return (getName().equalsIgnoreCase(otherCookie.getName()) && ObjectUtils.nullSafeEquals(this.path, otherCookie.getPath()) && ObjectUtils.nullSafeEquals(this.domain, otherCookie.getDomain())); @@ -130,9 +129,9 @@ public final class ServerHttpCookie extends HttpCookie { * @param value the cookie value * @return the created cookie instance */ - public static ServerHttpCookieBuilder with(final String name, final String value) { + public static ResponseCookieBuilder from(final String name, final String value) { - return new ServerHttpCookieBuilder() { + return new ResponseCookieBuilder() { private Duration maxAge = Duration.ofSeconds(-1); @@ -146,38 +145,38 @@ public final class ServerHttpCookie extends HttpCookie { @Override - public ServerHttpCookieBuilder maxAge(Duration maxAge) { + public ResponseCookieBuilder maxAge(Duration maxAge) { this.maxAge = maxAge; return this; } @Override - public ServerHttpCookieBuilder domain(String domain) { + public ResponseCookieBuilder domain(String domain) { this.domain = domain; return this; } @Override - public ServerHttpCookieBuilder path(String path) { + public ResponseCookieBuilder path(String path) { this.path = path; return this; } @Override - public ServerHttpCookieBuilder secure() { + public ResponseCookieBuilder secure() { this.secure = true; return this; } @Override - public ServerHttpCookieBuilder httpOnly() { + public ResponseCookieBuilder httpOnly() { this.httpOnly = true; return this; } @Override - public ServerHttpCookie build() { - return new ServerHttpCookie(name, value, this.maxAge, this.domain, this.path, + public ResponseCookie build() { + return new ResponseCookie(name, value, this.maxAge, this.domain, this.path, this.secure, this.httpOnly); } }; @@ -186,7 +185,7 @@ public final class ServerHttpCookie extends HttpCookie { /** * A builder for a server-defined HttpCookie with attributes. */ - public interface ServerHttpCookieBuilder { + public interface ResponseCookieBuilder { /** * Set the cookie "Max-Age" attribute. @@ -196,33 +195,33 @@ public final class ServerHttpCookie extends HttpCookie { * immediately. A negative value results in no "Max-Age" attribute in * which case the cookie is removed when the browser is closed. */ - ServerHttpCookieBuilder maxAge(Duration maxAge); + ResponseCookieBuilder maxAge(Duration maxAge); /** * Set the cookie "Path" attribute. */ - ServerHttpCookieBuilder path(String path); + ResponseCookieBuilder path(String path); /** * Set the cookie "Domain" attribute. */ - ServerHttpCookieBuilder domain(String domain); + ResponseCookieBuilder domain(String domain); /** * Add the "Secure" attribute to the cookie. */ - ServerHttpCookieBuilder secure(); + ResponseCookieBuilder secure(); /** * Add the "HttpOnly" attribute to the cookie. * @see http://www.owasp.org/index.php/HTTPOnly */ - ServerHttpCookieBuilder httpOnly(); + ResponseCookieBuilder httpOnly(); /** * Create the HttpCookie. */ - ServerHttpCookie build(); + ResponseCookie build(); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java index e4eb3e4feb..91eff4024e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java @@ -17,7 +17,6 @@ package org.springframework.http.server.reactive; import java.util.ArrayList; import java.util.List; -import java.util.Map; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; @@ -25,9 +24,8 @@ import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; -import org.springframework.http.ServerHttpCookie; +import org.springframework.http.ResponseCookie; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.LinkedMultiValueMap; @@ -43,7 +41,7 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse { private final HttpHeaders headers; - private final MultiValueMap cookies; + private final MultiValueMap cookies; private AtomicReference state = new AtomicReference<>(State.NEW); @@ -52,7 +50,7 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse { protected AbstractServerHttpResponse() { this.headers = new HttpHeaders(); - this.cookies = new LinkedMultiValueMap(); + this.cookies = new LinkedMultiValueMap(); } @@ -65,7 +63,7 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse { } @Override - public MultiValueMap getCookies() { + public MultiValueMap getCookies() { if (State.COMITTED.equals(this.state.get())) { return CollectionUtils.unmodifiableMultiValueMap(this.cookies); } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java index 0e57f1e2c7..e2f220c137 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java @@ -28,7 +28,7 @@ import reactor.io.netty.http.model.Status; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpStatus; -import org.springframework.http.ServerHttpCookie; +import org.springframework.http.ResponseCookie; import org.springframework.util.Assert; /** @@ -75,7 +75,7 @@ public class ReactorServerHttpResponse extends AbstractServerHttpResponse { @Override protected void writeCookies() { for (String name : getCookies().keySet()) { - for (ServerHttpCookie httpCookie : getCookies().get(name)) { + for (ResponseCookie httpCookie : getCookies().get(name)) { Cookie cookie = new ReactorCookie(httpCookie); this.channel.addResponseCookie(name, cookie); } @@ -88,10 +88,10 @@ public class ReactorServerHttpResponse extends AbstractServerHttpResponse { */ private final static class ReactorCookie extends Cookie { - private final ServerHttpCookie httpCookie; + private final ResponseCookie httpCookie; - public ReactorCookie(ServerHttpCookie httpCookie) { + public ReactorCookie(ResponseCookie httpCookie) { this.httpCookie = httpCookie; } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java index 9cb04d6719..ebcf5fad1b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java @@ -30,7 +30,7 @@ import rx.Observable; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.NettyDataBuffer; import org.springframework.http.HttpStatus; -import org.springframework.http.ServerHttpCookie; +import org.springframework.http.ResponseCookie; import org.springframework.util.Assert; /** @@ -86,7 +86,7 @@ public class RxNettyServerHttpResponse extends AbstractServerHttpResponse { @Override protected void writeCookies() { for (String name : getCookies().keySet()) { - for (ServerHttpCookie httpCookie : getCookies().get(name)) { + for (ResponseCookie httpCookie : getCookies().get(name)) { Cookie cookie = new DefaultCookie(name, httpCookie.getValue()); if (!httpCookie.getMaxAge().isNegative()) { cookie.setMaxAge(httpCookie.getMaxAge().getSeconds()); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java index c6d53276e8..18ba0f172b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java @@ -16,15 +16,11 @@ package org.springframework.http.server.reactive; -import java.util.List; -import java.util.Map; - import reactor.core.publisher.Mono; -import org.springframework.http.HttpCookie; import org.springframework.http.HttpStatus; import org.springframework.http.ReactiveHttpOutputMessage; -import org.springframework.http.ServerHttpCookie; +import org.springframework.http.ResponseCookie; import org.springframework.util.MultiValueMap; /** @@ -43,7 +39,7 @@ public interface ServerHttpResponse extends ReactiveHttpOutputMessage { /** * Return a mutable map with cookies to be sent to the client. */ - MultiValueMap getCookies(); + MultiValueMap getCookies(); /** * Indicate that request handling is complete, allowing for any cleanup or diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java index 3c0a5fd9cf..465a73eff3 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java @@ -36,7 +36,7 @@ import reactor.core.publisher.Mono; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.http.ServerHttpCookie; +import org.springframework.http.ResponseCookie; import org.springframework.util.Assert; /** @@ -99,7 +99,7 @@ public class ServletServerHttpResponse extends AbstractServerHttpResponse { @Override protected void writeCookies() { for (String name : getCookies().keySet()) { - for (ServerHttpCookie httpCookie : getCookies().get(name)) { + for (ResponseCookie httpCookie : getCookies().get(name)) { Cookie cookie = new Cookie(name, httpCookie.getValue()); if (!httpCookie.getMaxAge().isNegative()) { cookie.setMaxAge((int) httpCookie.getMaxAge().getSeconds()); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java index 091bf80fdd..910f913f70 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java @@ -29,7 +29,7 @@ import reactor.core.publisher.Mono; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpStatus; -import org.springframework.http.ServerHttpCookie; +import org.springframework.http.ResponseCookie; import org.springframework.util.Assert; /** @@ -81,7 +81,7 @@ public class UndertowServerHttpResponse extends AbstractServerHttpResponse { @Override protected void writeCookies() { for (String name : getCookies().keySet()) { - for (ServerHttpCookie httpCookie : getCookies().get(name)) { + for (ResponseCookie httpCookie : getCookies().get(name)) { Cookie cookie = new CookieImpl(name, httpCookie.getValue()); if (!httpCookie.getMaxAge().isNegative()) { cookie.setMaxAge((int) httpCookie.getMaxAge().getSeconds()); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/session/CookieWebSessionIdResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/server/session/CookieWebSessionIdResolver.java index 1d1fe75bad..a992f0057d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/session/CookieWebSessionIdResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/session/CookieWebSessionIdResolver.java @@ -21,7 +21,7 @@ import java.util.List; import java.util.stream.Collectors; import org.springframework.http.HttpCookie; -import org.springframework.http.ServerHttpCookie; +import org.springframework.http.ResponseCookie; import org.springframework.util.Assert; import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; @@ -58,7 +58,7 @@ public class CookieWebSessionIdResolver implements WebSessionIdResolver { /** * Set the value for the "Max-Age" attribute of the cookie that holds the - * session id. For the range of values see {@link ServerHttpCookie#getMaxAge()}. + * session id. For the range of values see {@link ResponseCookie#getMaxAge()}. *

    By default set to -1. * @param maxAge the maxAge duration value */ @@ -87,8 +87,8 @@ public class CookieWebSessionIdResolver implements WebSessionIdResolver { @Override public void setSessionId(ServerWebExchange exchange, String id) { Duration maxAge = (StringUtils.hasText(id) ? getCookieMaxAge() : Duration.ofSeconds(0)); - ServerHttpCookie cookie = ServerHttpCookie.with(getCookieName(), id).maxAge(maxAge).build(); - MultiValueMap cookieMap = exchange.getResponse().getCookies(); + ResponseCookie cookie = ResponseCookie.from(getCookieName(), id).maxAge(maxAge).build(); + MultiValueMap cookieMap = exchange.getResponse().getCookies(); cookieMap.set(getCookieName(), cookie); } diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/CookieIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/CookieIntegrationTests.java index 547a6f0457..db9e79a3c0 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/CookieIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/CookieIntegrationTests.java @@ -28,7 +28,7 @@ import reactor.core.publisher.Mono; import org.springframework.http.HttpCookie; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; -import org.springframework.http.ServerHttpCookie; +import org.springframework.http.ResponseCookie; import org.springframework.web.client.RestTemplate; import static org.hamcrest.CoreMatchers.equalTo; @@ -102,9 +102,9 @@ public class CookieIntegrationTests extends AbstractHttpHandlerIntegrationTests this.requestCookies = request.getCookies(); this.requestCookies.size(); // Cause lazy loading - response.getCookies().add("SID", ServerHttpCookie.with("SID", "31d4d96e407aad42") + response.getCookies().add("SID", ResponseCookie.from("SID", "31d4d96e407aad42") .path("/").secure().httpOnly().build()); - response.getCookies().add("lang", ServerHttpCookie.with("lang", "en-US") + response.getCookies().add("lang", ResponseCookie.from("lang", "en-US") .domain("example.com").path("/").build()); return response.setComplete(); diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java index 995846464b..42a672ce8d 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java @@ -24,7 +24,7 @@ import reactor.core.publisher.Mono; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; -import org.springframework.http.ServerHttpCookie; +import org.springframework.http.ResponseCookie; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -37,7 +37,7 @@ public class MockServerHttpResponse implements ServerHttpResponse { private HttpHeaders headers = new HttpHeaders(); - private MultiValueMap cookies = new LinkedMultiValueMap<>(); + private MultiValueMap cookies = new LinkedMultiValueMap<>(); private Publisher body; @@ -57,7 +57,7 @@ public class MockServerHttpResponse implements ServerHttpResponse { } @Override - public MultiValueMap getCookies() { + public MultiValueMap getCookies() { return this.cookies; } diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java index 260e18bdb2..dc2f5bf1b3 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java @@ -28,7 +28,7 @@ import reactor.core.publisher.Mono; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DefaultDataBufferAllocator; import org.springframework.http.HttpStatus; -import org.springframework.http.ServerHttpCookie; +import org.springframework.http.ResponseCookie; import static junit.framework.TestCase.assertTrue; import static org.junit.Assert.assertEquals; @@ -81,7 +81,7 @@ public class ServerHttpResponseTests { @Test public void beforeCommitWithSetBody() throws Exception { - ServerHttpCookie cookie = ServerHttpCookie.with("ID", "123").build(); + ResponseCookie cookie = ResponseCookie.from("ID", "123").build(); TestServerHttpResponse response = new TestServerHttpResponse(); response.beforeCommit(() -> { response.getCookies().add(cookie.getName(), cookie); @@ -118,7 +118,7 @@ public class ServerHttpResponseTests { @Test public void beforeCommitActionWithSetComplete() throws Exception { - ServerHttpCookie cookie = ServerHttpCookie.with("ID", "123").build(); + ResponseCookie cookie = ResponseCookie.from("ID", "123").build(); TestServerHttpResponse response = new TestServerHttpResponse(); response.beforeCommit(() -> { response.getCookies().add(cookie.getName(), cookie); From 72dbe9012e00a29b03b925612766bc928b705e8e Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 17 Mar 2016 21:04:50 -0400 Subject: [PATCH 255/498] Add cookies to ClientHttpRequest/Response --- .../springframework/http/ResponseCookie.java | 23 ++++++++--- .../reactive/AbstractClientHttpRequest.java | 15 +++++++ .../client/reactive/ClientHttpRequest.java | 7 ++++ .../client/reactive/ClientHttpResponse.java | 7 ++++ .../reactive/ReactorClientHttpRequest.java | 41 +++++++++++++++---- .../reactive/ReactorClientHttpResponse.java | 24 ++++++++++- .../reactive/RxNettyClientHttpRequest.java | 8 ++++ .../reactive/RxNettyClientHttpResponse.java | 33 +++++++++++++++ .../server/reactive/ServerHttpResponse.java | 2 +- .../reactive/CookieIntegrationTests.java | 2 +- 10 files changed, 146 insertions(+), 16 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/ResponseCookie.java b/spring-web-reactive/src/main/java/org/springframework/http/ResponseCookie.java index 13d9adbc45..0254d5a435 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/ResponseCookie.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/ResponseCookie.java @@ -150,6 +150,12 @@ public final class ResponseCookie extends HttpCookie { return this; } + @Override + public ResponseCookieBuilder maxAge(long maxAgeSeconds) { + this.maxAge = maxAgeSeconds >= 0 ? Duration.ofSeconds(maxAgeSeconds) : Duration.ofSeconds(-1); + return this; + } + @Override public ResponseCookieBuilder domain(String domain) { this.domain = domain; @@ -163,14 +169,14 @@ public final class ResponseCookie extends HttpCookie { } @Override - public ResponseCookieBuilder secure() { - this.secure = true; + public ResponseCookieBuilder secure(boolean secure) { + this.secure = secure; return this; } @Override - public ResponseCookieBuilder httpOnly() { - this.httpOnly = true; + public ResponseCookieBuilder httpOnly(boolean httpOnly) { + this.httpOnly = httpOnly; return this; } @@ -197,6 +203,11 @@ public final class ResponseCookie extends HttpCookie { */ ResponseCookieBuilder maxAge(Duration maxAge); + /** + * Set the cookie "Max-Age" attribute in seconds. + */ + ResponseCookieBuilder maxAge(long maxAgeSeconds); + /** * Set the cookie "Path" attribute. */ @@ -210,13 +221,13 @@ public final class ResponseCookie extends HttpCookie { /** * Add the "Secure" attribute to the cookie. */ - ResponseCookieBuilder secure(); + ResponseCookieBuilder secure(boolean secure); /** * Add the "HttpOnly" attribute to the cookie. * @see http://www.owasp.org/index.php/HTTPOnly */ - ResponseCookieBuilder httpOnly(); + ResponseCookieBuilder httpOnly(boolean httpOnly); /** * Create the HttpCookie. diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/AbstractClientHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/AbstractClientHttpRequest.java index 850eea9e36..8018698e76 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/AbstractClientHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/AbstractClientHttpRequest.java @@ -23,8 +23,12 @@ import java.util.function.Supplier; import reactor.core.publisher.Mono; +import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; /** * Base class for {@link ClientHttpRequest} implementations. @@ -36,6 +40,8 @@ public abstract class AbstractClientHttpRequest implements ClientHttpRequest { private final HttpHeaders headers; + private final MultiValueMap cookies; + private AtomicReference state = new AtomicReference<>(State.NEW); private final List>> beforeCommitActions = new ArrayList<>(4); @@ -47,6 +53,7 @@ public abstract class AbstractClientHttpRequest implements ClientHttpRequest { else { this.headers = httpHeaders; } + this.cookies = new LinkedMultiValueMap<>(); } @Override @@ -57,6 +64,14 @@ public abstract class AbstractClientHttpRequest implements ClientHttpRequest { return this.headers; } + @Override + public MultiValueMap getCookies() { + if (State.COMITTED.equals(this.state.get())) { + return CollectionUtils.unmodifiableMultiValueMap(this.cookies); + } + return this.cookies; + } + protected Mono applyBeforeCommit() { Mono mono = Mono.empty(); if (this.state.compareAndSet(State.NEW, State.COMMITTING)) { diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ClientHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ClientHttpRequest.java index 32c38940ae..3e7098a043 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ClientHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ClientHttpRequest.java @@ -20,8 +20,10 @@ import java.net.URI; import reactor.core.publisher.Mono; +import org.springframework.http.HttpCookie; import org.springframework.http.HttpMethod; import org.springframework.http.ReactiveHttpOutputMessage; +import org.springframework.util.MultiValueMap; /** * Represents a reactive client-side HTTP request. @@ -41,6 +43,11 @@ public interface ClientHttpRequest extends ReactiveHttpOutputMessage { */ URI getURI(); + /** + * Return a mutable map of request cookies to send to the server. + */ + MultiValueMap getCookies(); + /** * Execute this request, resulting in a reactive stream of a single * {@link org.springframework.http.client.ClientHttpResponse}. diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ClientHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ClientHttpResponse.java index 34cad69c10..502102b227 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ClientHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ClientHttpResponse.java @@ -18,6 +18,8 @@ package org.springframework.http.client.reactive; import org.springframework.http.HttpStatus; import org.springframework.http.ReactiveHttpInputMessage; +import org.springframework.http.ResponseCookie; +import org.springframework.util.MultiValueMap; /** * Represents a reactive client-side HTTP response. @@ -31,4 +33,9 @@ public interface ClientHttpResponse extends ReactiveHttpInputMessage { */ HttpStatus getStatusCode(); + /** + * Return a read-only map of response cookies received from the server. + */ + MultiValueMap getCookies(); + } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java index 81ec72a3d4..5ff05bc20d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java @@ -17,16 +17,19 @@ package org.springframework.http.client.reactive; import java.net.URI; +import java.util.Collection; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.io.buffer.Buffer; import reactor.io.netty.http.HttpClient; +import reactor.io.netty.http.model.Cookie; import reactor.io.netty.http.model.Method; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferAllocator; +import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -96,13 +99,13 @@ public class ReactorClientHttpRequest extends AbstractClientHttpRequest { channel.headers().removeTransferEncodingChunked(); } return applyBeforeCommit() - .after(() -> - { - getHeaders().entrySet().stream() - .forEach(e -> channel.headers().set(e.getKey(), e.getValue())); - return Mono.empty(); - } - ) + .after(() -> { + getHeaders().entrySet().stream().forEach(e -> + channel.headers().set(e.getKey(), e.getValue())); + getCookies().values().stream().flatMap(Collection::stream).forEach(cookie -> + channel.addCookie(cookie.getName(), new ReactorCookie(cookie))); + return Mono.empty(); + }) .after(() -> { if (body != null) { return channel.writeBufferWith(body); @@ -115,5 +118,29 @@ public class ReactorClientHttpRequest extends AbstractClientHttpRequest { .map(httpChannel -> new ReactorClientHttpResponse(httpChannel, allocator)); } + + /** + * At present Reactor does not provide a {@link Cookie} implementation. + */ + private final static class ReactorCookie extends Cookie { + + private final HttpCookie httpCookie; + + + public ReactorCookie(HttpCookie httpCookie) { + this.httpCookie = httpCookie; + } + + @Override + public String name() { + return this.httpCookie.getName(); + } + + @Override + public String value() { + return this.httpCookie.getValue(); + } + } + } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpResponse.java index 937d26d07b..df2d58aded 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpResponse.java @@ -17,6 +17,7 @@ package org.springframework.http.client.reactive; import java.nio.ByteBuffer; +import java.util.Collection; import reactor.core.publisher.Flux; import reactor.io.buffer.Buffer; @@ -26,6 +27,10 @@ import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferAllocator; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseCookie; +import org.springframework.util.CollectionUtils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; /** * {@link ClientHttpResponse} implementation for the Reactor Net HTTP client @@ -62,10 +67,27 @@ public class ReactorClientHttpResponse implements ClientHttpResponse { return HttpStatus.valueOf(this.channel.responseStatus().getCode()); } + @Override + public MultiValueMap getCookies() { + MultiValueMap result = new LinkedMultiValueMap<>(); + this.channel.cookies().values().stream().flatMap(Collection::stream) + .forEach(cookie -> { + ResponseCookie responseCookie = ResponseCookie.from(cookie.name(), cookie.value()) + .domain(cookie.domain()) + .path(cookie.path()) + .maxAge(cookie.maxAge()) + .secure(cookie.secure()) + .httpOnly(cookie.httpOnly()) + .build(); + result.add(cookie.name(), responseCookie); + }); + return CollectionUtils.unmodifiableMultiValueMap(result); + } + @Override public String toString() { return "ReactorClientHttpResponse{" + - "request=" + this.channel.method() + " " + this.channel.uri().toString() + "," + + "request=" + this.channel.method() + " " + this.channel.uri() + "," + "status=" + getStatusCode() + '}'; } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpRequest.java index 74c894e7fa..7ab96b37c6 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpRequest.java @@ -17,10 +17,12 @@ package org.springframework.http.client.reactive; import java.net.URI; +import java.util.Collection; import java.util.List; import java.util.Map; import io.netty.buffer.ByteBuf; +import io.netty.handler.codec.http.cookie.DefaultCookie; import io.reactivex.netty.protocol.http.client.HttpClient; import io.reactivex.netty.protocol.http.client.HttpClientRequest; import org.reactivestreams.Publisher; @@ -31,6 +33,7 @@ import rx.Observable; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.NettyDataBufferAllocator; +import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -102,6 +105,11 @@ public class RxNettyClientHttpRequest extends AbstractClientHttpRequest { req = req.addHeader(entry.getKey(), value); } } + for (Map.Entry> entry : getCookies().entrySet()) { + for (HttpCookie cookie : entry.getValue()) { + req.addCookie(new DefaultCookie(cookie.getName(), cookie.getValue())); + } + } return req; }) .map(req -> { diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpResponse.java index a6a2efe3b2..5edd02ef9e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpResponse.java @@ -16,6 +16,8 @@ package org.springframework.http.client.reactive; +import java.util.Collection; + import io.netty.buffer.ByteBuf; import io.reactivex.netty.protocol.http.client.HttpClientResponse; import reactor.core.converter.RxJava1ObservableConverter; @@ -25,7 +27,11 @@ import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.NettyDataBufferAllocator; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseCookie; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; /** * {@link ClientHttpResponse} implementation for the RxNetty HTTP client @@ -38,8 +44,11 @@ public class RxNettyClientHttpResponse implements ClientHttpResponse { private final HttpHeaders headers; + private final MultiValueMap cookies; + private final NettyDataBufferAllocator allocator; + public RxNettyClientHttpResponse(HttpClientResponse response, NettyDataBufferAllocator allocator) { Assert.notNull("'request', request must not be null"); @@ -48,8 +57,26 @@ public class RxNettyClientHttpResponse implements ClientHttpResponse { this.response = response; this.headers = new HttpHeaders(); this.response.headerIterator().forEachRemaining(e -> this.headers.set(e.getKey(), e.getValue())); + this.cookies = initCookies(response); } + private static MultiValueMap initCookies(HttpClientResponse response) { + MultiValueMap result = new LinkedMultiValueMap<>(); + response.getCookies().values().stream().flatMap(Collection::stream) + .forEach(cookie -> { + ResponseCookie responseCookie = ResponseCookie.from(cookie.name(), cookie.value()) + .domain(cookie.domain()) + .path(cookie.path()) + .maxAge(cookie.maxAge()) + .secure(cookie.isSecure()) + .httpOnly(cookie.isHttpOnly()) + .build(); + result.add(cookie.name(), responseCookie); + }); + return CollectionUtils.unmodifiableMultiValueMap(result); + } + + @Override public HttpStatus getStatusCode() { return HttpStatus.valueOf(this.response.getStatus().code()); @@ -64,4 +91,10 @@ public class RxNettyClientHttpResponse implements ClientHttpResponse { public HttpHeaders getHeaders() { return this.headers; } + + @Override + public MultiValueMap getCookies() { + return this.cookies; + } + } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java index 18ba0f172b..4e641499f5 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java @@ -37,7 +37,7 @@ public interface ServerHttpResponse extends ReactiveHttpOutputMessage { void setStatusCode(HttpStatus status); /** - * Return a mutable map with cookies to be sent to the client. + * Return a mutable map with the cookies to send to the server. */ MultiValueMap getCookies(); diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/CookieIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/CookieIntegrationTests.java index db9e79a3c0..d39d60a45c 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/CookieIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/CookieIntegrationTests.java @@ -103,7 +103,7 @@ public class CookieIntegrationTests extends AbstractHttpHandlerIntegrationTests this.requestCookies.size(); // Cause lazy loading response.getCookies().add("SID", ResponseCookie.from("SID", "31d4d96e407aad42") - .path("/").secure().httpOnly().build()); + .path("/").secure(true).httpOnly(true).build()); response.getCookies().add("lang", ResponseCookie.from("lang", "en-US") .domain("example.com").path("/").build()); From 5519428d43e797742d60e76fcd10dab181cfcfe7 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Thu, 17 Mar 2016 17:08:19 +0100 Subject: [PATCH 256/498] Replaced DataBufferPublisherInputStream with JDK equivalent. --- .../DataBufferPublisherInputStream.java | 147 ------------------ .../io/buffer/support/DataBufferUtils.java | 31 +++- 2 files changed, 30 insertions(+), 148 deletions(-) delete mode 100644 spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferPublisherInputStream.java diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferPublisherInputStream.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferPublisherInputStream.java deleted file mode 100644 index 5405b9b65b..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferPublisherInputStream.java +++ /dev/null @@ -1,147 +0,0 @@ -/* - * 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.io.buffer.support; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Iterator; -import java.util.concurrent.atomic.AtomicBoolean; - -import org.reactivestreams.Publisher; -import org.reactivestreams.Subscription; -import reactor.core.publisher.Flux; - -import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.util.Assert; - -/** - * @author Arjen Poutsma - */ -class DataBufferPublisherInputStream extends InputStream { - - private final AtomicBoolean completed = new AtomicBoolean(); - - private final Iterator queue; - - private InputStream currentStream; - - /** - * Creates a new {@code ByteArrayPublisherInputStream} based on the given publisher. - * @param publisher the publisher to use - */ - public DataBufferPublisherInputStream(Publisher publisher) { - this(publisher, 1); - } - - /** - * Creates a new {@code ByteArrayPublisherInputStream} based on the given publisher. - * @param publisher the publisher to use - * @param requestSize the {@linkplain Subscription#request(long) request size} to use - * on the publisher bound to Integer MAX - */ - public DataBufferPublisherInputStream(Publisher publisher, - int requestSize) { - Assert.notNull(publisher, "'publisher' must not be null"); - - this.queue = Flux.from(publisher).toIterable(requestSize).iterator(); - } - - @Override - public int available() throws IOException { - if (completed.get()) { - return 0; - } - InputStream is = currentStream(); - return is != null ? is.available() : 0; - } - - @Override - public int read() throws IOException { - if (completed.get()) { - return -1; - } - InputStream is = currentStream(); - while (is != null) { - int ch = is.read(); - if (ch != -1) { - return ch; - } - else { - is = currentStream(); - } - } - return -1; - } - - @Override - public int read(byte[] b, int off, int len) throws IOException { - if (completed.get()) { - return -1; - } - InputStream is = currentStream(); - if (is == null) { - return -1; - } - else if (b == null) { - throw new NullPointerException(); - } - else if (off < 0 || len < 0 || len > b.length - off) { - throw new IndexOutOfBoundsException(); - } - else if (len == 0) { - return 0; - } - do { - int n = is.read(b, off, len); - if (n > 0) { - return n; - } - else { - is = currentStream(); - } - } - while (is != null); - - return -1; - } - - private InputStream currentStream() throws IOException { - try { - if (this.currentStream != null && this.currentStream.available() > 0) { - return this.currentStream; - } - else { - // if upstream Publisher has completed, then complete() and return null, - if (!this.queue.hasNext()) { - this.completed.set(true); - return null; - } - // next() blocks until next - // but that's OK since this is a *blocking* InputStream - DataBuffer signal = this.queue.next(); - this.currentStream = signal.asInputStream(); - return this.currentStream; - } - } - catch (Throwable error) { - this.completed.set(true); - throw new IOException(error); - } - } - - -} diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java index d4bcc1ad6c..5888ce8f34 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java @@ -17,6 +17,9 @@ package org.springframework.core.io.buffer.support; import java.io.InputStream; +import java.io.SequenceInputStream; +import java.util.Enumeration; +import java.util.Iterator; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -62,7 +65,33 @@ public abstract class DataBufferUtils { * @return the input stream */ public static InputStream toInputStream(Publisher publisher) { - return new DataBufferPublisherInputStream(publisher); + Iterable streams = Flux.from(publisher). + map(DataBuffer::asInputStream). + toIterable(); + + Enumeration enumeration = + new IteratorEnumeration(streams); + + return new SequenceInputStream(enumeration); + } + + private static class IteratorEnumeration implements Enumeration { + + private final Iterator delegate; + + public IteratorEnumeration(Iterable iterable) { + this.delegate = iterable.iterator(); + } + + @Override + public boolean hasMoreElements() { + return delegate.hasNext(); + } + + @Override + public T nextElement() { + return delegate.next(); + } } } From 2c52731417154651f05881e57b866c5d5c2bd42d Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Thu, 17 Mar 2016 15:42:30 +0100 Subject: [PATCH 257/498] Various test improvements --- .../http/server/reactive/EchoHandler.java | 30 ------ .../reactive/EchoHandlerIntegrationTests.java | 46 +++----- .../http/server/reactive/RandomHandler.java | 89 --------------- .../RandomHandlerIntegrationTests.java | 102 ++++++++++++++++-- .../http/server/reactive/XmlHandler.java | 86 --------------- .../reactive/XmlHandlerIntegrationTests.java | 93 +++++++++++++++- .../RequestMappingIntegrationTests.java | 11 +- 7 files changed, 205 insertions(+), 252 deletions(-) delete mode 100644 spring-web-reactive/src/test/java/org/springframework/http/server/reactive/EchoHandler.java delete mode 100644 spring-web-reactive/src/test/java/org/springframework/http/server/reactive/RandomHandler.java delete mode 100644 spring-web-reactive/src/test/java/org/springframework/http/server/reactive/XmlHandler.java diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/EchoHandler.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/EchoHandler.java deleted file mode 100644 index b5bf612848..0000000000 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/EchoHandler.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2002-2015 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.server.reactive; - -import reactor.core.publisher.Mono; - -/** - * @author Arjen Poutsma - */ -public class EchoHandler implements HttpHandler { - - @Override - public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { - return response.setBody(request.getBody()); - } -} diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/EchoHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/EchoHandlerIntegrationTests.java index 5a27960114..00c030795e 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/EchoHandlerIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/EchoHandlerIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -20,13 +20,13 @@ import java.net.URI; import java.util.Random; import org.junit.Test; +import reactor.core.publisher.Mono; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; import org.springframework.web.client.RestTemplate; import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; public class EchoHandlerIntegrationTests extends AbstractHttpHandlerIntegrationTests { @@ -43,7 +43,7 @@ public class EchoHandlerIntegrationTests extends AbstractHttpHandlerIntegrationT @Test - public void echoBytes() throws Exception { + public void echo() throws Exception { RestTemplate restTemplate = new RestTemplate(); byte[] body = randomBytes(); @@ -53,36 +53,6 @@ public class EchoHandlerIntegrationTests extends AbstractHttpHandlerIntegrationT assertArrayEquals(body, response.getBody()); } - @Test - public void echoString() throws Exception { - RestTemplate restTemplate = new RestTemplate(); - - String body = randomString(); - RequestEntity request = RequestEntity.post(new URI("http://localhost:" + port)).body(body); - ResponseEntity response = restTemplate.exchange(request, String.class); - - assertEquals(body, response.getBody()); - } - - private String randomString() { - StringBuilder builder = new StringBuilder(); - int i = 1; - while (builder.length() < REQUEST_SIZE) { - builder.append(randomChar()); - if (i % 5 == 0) { - builder.append(' '); - } - if (i % 80 == 0) { - builder.append('\n'); - } - i++; - } - return builder.toString(); - } - - private char randomChar() { - return (char) (rnd.nextInt(26) + 'a'); - } private byte[] randomBytes() { byte[] buffer = new byte[REQUEST_SIZE]; @@ -90,4 +60,14 @@ public class EchoHandlerIntegrationTests extends AbstractHttpHandlerIntegrationT return buffer; } + /** + * @author Arjen Poutsma + */ + public static class EchoHandler implements HttpHandler { + + @Override + public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { + return response.setBody(request.getBody()); + } + } } diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/RandomHandler.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/RandomHandler.java deleted file mode 100644 index 14b17ec1c2..0000000000 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/RandomHandler.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * 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.http.server.reactive; - -import java.util.Random; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; -import reactor.core.publisher.Mono; - -import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DefaultDataBufferAllocator; - -import static org.junit.Assert.assertEquals; - -/** - * @author Arjen Poutsma - */ -public class RandomHandler implements HttpHandler { - - private static final Log logger = LogFactory.getLog(RandomHandler.class); - - public static final int RESPONSE_SIZE = 4096 * 3; - - private final Random rnd = new Random(); - - @Override - public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { - - request.getBody().subscribe(new Subscriber() { - private Subscription s; - - private int requestSize = 0; - - @Override - public void onSubscribe(Subscription s) { - this.s = s; - s.request(Long.MAX_VALUE); - } - - @Override - public void onNext(DataBuffer bytes) { - requestSize += bytes.readableByteCount(); - } - - @Override - public void onError(Throwable t) { - logger.error(t); - - } - - @Override - public void onComplete() { - logger.debug("Complete"); - assertEquals(RandomHandlerIntegrationTests.REQUEST_SIZE, requestSize); - } - }); - - response.getHeaders().setContentLength(RESPONSE_SIZE); - byte[] randomBytes = randomBytes(); - DataBuffer buffer = - new DefaultDataBufferAllocator().allocateBuffer(randomBytes.length); - buffer.write(randomBytes); - return response.setBody(Mono.just(buffer)); - } - - private byte[] randomBytes() { - byte[] buffer = new byte[RESPONSE_SIZE]; - rnd.nextBytes(buffer); - return buffer; - } - -} diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/RandomHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/RandomHandlerIntegrationTests.java index d7925d3ca0..7e92d620df 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/RandomHandlerIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/RandomHandlerIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -20,29 +20,48 @@ import java.net.URI; import java.util.Random; import org.junit.Test; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferAllocator; +import org.springframework.core.io.buffer.DefaultDataBufferAllocator; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; +import org.springframework.http.server.reactive.boot.ReactorHttpServer; import org.springframework.web.client.RestTemplate; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assume.assumeFalse; public class RandomHandlerIntegrationTests extends AbstractHttpHandlerIntegrationTests { public static final int REQUEST_SIZE = 4096 * 3; - private Random rnd = new Random(); + public static final int RESPONSE_SIZE = 1024 * 4; + + private final Random rnd = new Random(); + + private final RandomHandler handler = new RandomHandler(); + + private final DataBufferAllocator allocator = new DefaultDataBufferAllocator(); @Override protected RandomHandler createHttpHandler() { - return new RandomHandler(); + return handler; } @Test - public void random() throws Exception { + public void random() throws Throwable { + // TODO: fix Reactor support + assumeFalse(server instanceof ReactorHttpServer); + RestTemplate restTemplate = new RestTemplate(); byte[] body = randomBytes(); @@ -50,9 +69,17 @@ public class RandomHandlerIntegrationTests extends AbstractHttpHandlerIntegratio ResponseEntity response = restTemplate.exchange(request, byte[].class); assertNotNull(response.getBody()); - assertEquals(RandomHandler.RESPONSE_SIZE, + assertEquals(RESPONSE_SIZE, response.getHeaders().getContentLength()); - assertEquals(RandomHandler.RESPONSE_SIZE, response.getBody().length); + assertEquals(RESPONSE_SIZE, response.getBody().length); + + while (!handler.requestComplete) { + Thread.sleep(100); + } + if (handler.requestError != null) { + throw handler.requestError; + } + assertEquals(REQUEST_SIZE, handler.requestSize); } @@ -62,4 +89,67 @@ public class RandomHandlerIntegrationTests extends AbstractHttpHandlerIntegratio return buffer; } + private class RandomHandler implements HttpHandler { + + public static final int CHUNKS = 16; + + private volatile boolean requestComplete; + + private int requestSize; + + private Throwable requestError; + + @Override + public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { + requestError = null; + + request.getBody().subscribe(new Subscriber() { + + @Override + public void onSubscribe(Subscription s) { + requestComplete = false; + requestSize = 0; + requestError = null; + s.request(Long.MAX_VALUE); + } + + @Override + public void onNext(DataBuffer bytes) { + requestSize += bytes.readableByteCount(); + } + + @Override + public void onError(Throwable t) { + requestComplete = true; + requestError = t; + } + + @Override + public void onComplete() { + requestComplete = true; + } + }); + + response.getHeaders().setContentLength(RESPONSE_SIZE); + return response.setBody(multipleChunks()); + } + + private Publisher singleChunk() { + return Mono.just(randomBuffer(RESPONSE_SIZE)); + } + + private Publisher multipleChunks() { + int chunkSize = RESPONSE_SIZE / CHUNKS; + return Flux.range(1, CHUNKS).map(integer -> randomBuffer(chunkSize)); + } + + private DataBuffer randomBuffer(int size) { + byte[] bytes = new byte[size]; + rnd.nextBytes(bytes); + DataBuffer buffer = allocator.allocateBuffer(size); + buffer.write(bytes); + return buffer; + } + + } } diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/XmlHandler.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/XmlHandler.java deleted file mode 100644 index 3b72aed9d9..0000000000 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/XmlHandler.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * 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.http.server.reactive; - -import java.io.InputStream; -import java.io.OutputStream; -import javax.xml.bind.JAXBContext; -import javax.xml.bind.Marshaller; -import javax.xml.bind.Unmarshaller; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DefaultDataBufferAllocator; -import org.springframework.core.io.buffer.support.DataBufferUtils; -import org.springframework.http.MediaType; - -import static org.junit.Assert.fail; - -/** - * @author Arjen Poutsma - */ -public class XmlHandler implements HttpHandler { - - private static final Log logger = LogFactory.getLog(XmlHandler.class); - - @Override - public Mono handle(ServerHttpRequest request, - ServerHttpResponse response) { - try { - JAXBContext jaxbContext = JAXBContext.newInstance(XmlHandlerIntegrationTests.Person.class); - Unmarshaller unmarshaller = jaxbContext.createUnmarshaller(); - Marshaller marshaller = jaxbContext.createMarshaller(); - - Runnable r = () -> { - try { - InputStream bis = DataBufferUtils.toInputStream(request.getBody()); - - XmlHandlerIntegrationTests.Person johnDoe = - (XmlHandlerIntegrationTests.Person) unmarshaller.unmarshal(bis); - - logger.info("Read: " + johnDoe); - - } - catch (Exception e) { - logger.error(e, e); - } - }; - - Thread t = new Thread(r); - t.start(); - - response.getHeaders().setContentType(MediaType.APPLICATION_XML); - XmlHandlerIntegrationTests.Person janeDoe = new XmlHandlerIntegrationTests.Person("Jane Doe"); - - DataBuffer buffer = new DefaultDataBufferAllocator().allocateBuffer(); - OutputStream bos = buffer.asOutputStream(); - marshaller.marshal(janeDoe, bos); - bos.close(); - - return response.setBody(Flux.just(buffer)); - } - catch (Exception ex) { - logger.error(ex, ex); - fail(ex.getMessage()); - return null; - } - } -} diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/XmlHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/XmlHandlerIntegrationTests.java index c805886bf1..b286a8273b 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/XmlHandlerIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/XmlHandlerIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -16,39 +16,123 @@ package org.springframework.http.server.reactive; +import java.io.InputStream; +import java.io.OutputStream; import java.net.URI; +import javax.xml.bind.JAXBContext; +import javax.xml.bind.Marshaller; +import javax.xml.bind.Unmarshaller; import javax.xml.bind.annotation.XmlRootElement; import org.junit.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DefaultDataBufferAllocator; +import org.springframework.core.io.buffer.support.DataBufferUtils; +import org.springframework.http.MediaType; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; +import org.springframework.http.server.reactive.boot.ReactorHttpServer; +import org.springframework.http.server.reactive.boot.RxNettyHttpServer; import org.springframework.web.client.RestTemplate; +import static org.junit.Assert.assertEquals; +import static org.junit.Assume.assumeFalse; + /** * @author Arjen Poutsma */ public class XmlHandlerIntegrationTests extends AbstractHttpHandlerIntegrationTests { + private final XmlHandler handler = new XmlHandler(); + @Override protected HttpHandler createHttpHandler() { - return new XmlHandler(); + return handler; } @Test public void xml() throws Exception { + // TODO: fix Reactor and RxNetty support + assumeFalse(server instanceof ReactorHttpServer || + server instanceof RxNettyHttpServer); + RestTemplate restTemplate = new RestTemplate(); Person johnDoe = new Person("John Doe"); + Person janeDoe = new Person("Jane Doe"); RequestEntity request = RequestEntity.post(new URI("http://localhost:" + port)).body( johnDoe); ResponseEntity response = restTemplate.exchange(request, Person.class); - System.out.println(response.getBody()); + assertEquals(janeDoe, response.getBody()); + + while (!handler.requestComplete) { + Thread.sleep(100); + } + if (handler.requestError != null) { + throw handler.requestError; + } + assertEquals(johnDoe, handler.requestPerson); + + } + + private static class XmlHandler implements HttpHandler { + + private volatile boolean requestComplete = false; + + private Person requestPerson; + + private Exception requestError; + + + @Override + public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { + requestError = null; + try { + JAXBContext jaxbContext = JAXBContext.newInstance(Person.class); + Marshaller marshaller = jaxbContext.createMarshaller(); + + Runnable r = () -> { + try { + InputStream bis = + DataBufferUtils.toInputStream(request.getBody()); + + Unmarshaller unmarshaller = jaxbContext.createUnmarshaller(); + requestPerson = (Person) unmarshaller.unmarshal(bis); + + } + catch (Exception ex) { + requestError = ex; + } + finally { + requestComplete = true; + } + }; + + Thread t = new Thread(r); + t.start(); + + response.getHeaders().setContentType(MediaType.APPLICATION_XML); + Person janeDoe = new Person("Jane Doe"); + + DataBuffer buffer = new DefaultDataBufferAllocator().allocateBuffer(); + OutputStream bos = buffer.asOutputStream(); + marshaller.marshal(janeDoe, bos); + bos.close(); + + return response.setBody(Flux.just(buffer)); + } + catch (Exception ex) { + return Mono.error(ex); + } + } } @XmlRootElement - static class Person { + private static class Person { private String name; @@ -82,4 +166,5 @@ public class XmlHandlerIntegrationTests extends AbstractHttpHandlerIntegrationTe return name; } } + } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java index bd75c45429..250dcc6645 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java @@ -25,7 +25,6 @@ import java.util.Collections; import java.util.List; import java.util.concurrent.CompletableFuture; -import org.junit.Ignore; import org.junit.Test; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -170,8 +169,6 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati } @Test - @Ignore - //FIXME Fail with Jetty and Tomcat public void streamResult() throws Exception { RestTemplate restTemplate = new RestTemplate(); @@ -468,7 +465,13 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @RequestMapping("/stream-result") public Publisher stringStreamResponseBody() { - return Flux.interval(Duration.ofMillis(100)).take(5); + /* + TODO: replace the following line with: + return Flux.interval(Duration.ofMillis(100)).take(5); + to make the build last shorter. Unfortunately, this hangs the build as of + 20160317. + */ + return Flux.interval(Duration.ofSeconds(1)).take(5); } @RequestMapping("/raw-flux") From 273c1b85e68644ba426603340b1b92cd71dfe341 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Fri, 18 Mar 2016 11:58:55 +0100 Subject: [PATCH 258/498] Reduced batch size to 1, so that we don't block on init. --- .../io/buffer/support/DataBufferUtils.java | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java index 5888ce8f34..745b42d42e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java @@ -59,38 +59,42 @@ public abstract class DataBufferUtils { } /** - * Returns the given data buffer publisher as an input stream, streaming over all - * underlying buffers when available. + * Returns the given data buffer publisher as a blocking input stream, streaming over + * all underlying buffers when available. * @param publisher the publisher to create the input stream for * @return the input stream */ public static InputStream toInputStream(Publisher publisher) { Iterable streams = Flux.from(publisher). map(DataBuffer::asInputStream). - toIterable(); + toIterable(1); Enumeration enumeration = - new IteratorEnumeration(streams); + new IteratorEnumeration(streams.iterator()); return new SequenceInputStream(enumeration); } + /** + * Enumeration wrapping an Iterator. + */ + // TODO: move to CollectionUtils when we merge with Spring Framework? private static class IteratorEnumeration implements Enumeration { - private final Iterator delegate; + private final Iterator iterator; - public IteratorEnumeration(Iterable iterable) { - this.delegate = iterable.iterator(); + public IteratorEnumeration(Iterator iterator) { + this.iterator = iterator; } @Override public boolean hasMoreElements() { - return delegate.hasNext(); + return this.iterator.hasNext(); } @Override public T nextElement() { - return delegate.next(); + return this.iterator.next(); } } From b838fdab88e60e8b8b1645e90c1fd0e58dd5a09f Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Tue, 23 Feb 2016 10:53:43 +0100 Subject: [PATCH 259/498] Added allocator property to DataBuffer --- .../core/io/buffer/DataBuffer.java | 6 ++++++ .../core/io/buffer/DefaultDataBuffer.java | 15 ++++++++++++--- .../io/buffer/DefaultDataBufferAllocator.java | 6 +++--- .../core/io/buffer/NettyDataBuffer.java | 11 ++++++++++- .../core/io/buffer/NettyDataBufferAllocator.java | 8 ++++---- 5 files changed, 35 insertions(+), 11 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DataBuffer.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DataBuffer.java index 591b6c3cc9..cf4770adee 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DataBuffer.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DataBuffer.java @@ -27,6 +27,12 @@ import java.nio.ByteBuffer; */ public interface DataBuffer { + /** + * Returns the {@link DataBufferAllocator} that created this buffer. + * @return the creating buffer allocator + */ + DataBufferAllocator allocator(); + /** * Gets the byte at the specified index. * @param index the index diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java index 3bd7859957..0dca055bec 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java @@ -36,6 +36,8 @@ import org.springframework.util.ObjectUtils; */ public class DefaultDataBuffer implements DataBuffer { + private final DefaultDataBufferAllocator allocator; + private ByteBuffer byteBuffer; private int readPosition; @@ -48,20 +50,27 @@ public class DefaultDataBuffer implements DataBuffer { * ByteBuffer#position() position} of the given buffer. * @param byteBuffer the buffer to base this buffer on */ - DefaultDataBuffer(ByteBuffer byteBuffer) { - this(byteBuffer, byteBuffer.position(), byteBuffer.position()); + DefaultDataBuffer(ByteBuffer byteBuffer, DefaultDataBufferAllocator allocator) { + this(byteBuffer, byteBuffer.position(), byteBuffer.position(), allocator); } - DefaultDataBuffer(ByteBuffer byteBuffer, int readPosition, int writePosition) { + DefaultDataBuffer(ByteBuffer byteBuffer, int readPosition, int writePosition, DefaultDataBufferAllocator allocator) { Assert.notNull(byteBuffer, "'byteBuffer' must not be null"); Assert.isTrue(readPosition >= 0, "'readPosition' must be 0 or higher"); Assert.isTrue(writePosition >= 0, "'writePosition' must be 0 or higher"); Assert.isTrue(readPosition <= writePosition, "'readPosition' must be smaller than or equal to 'writePosition'"); + Assert.notNull(allocator, "'allocator' must not be null"); this.byteBuffer = byteBuffer; this.readPosition = readPosition; this.writePosition = writePosition; + this.allocator = allocator; + } + + @Override + public DefaultDataBufferAllocator allocator() { + return this.allocator; } /** diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBufferAllocator.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBufferAllocator.java index a9b2b56729..0d3c6661b1 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBufferAllocator.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBufferAllocator.java @@ -80,14 +80,14 @@ public class DefaultDataBufferAllocator implements DataBufferAllocator { @Override public DefaultDataBuffer allocateBuffer(int initialCapacity) { return this.preferDirect ? - new DefaultDataBuffer(ByteBuffer.allocateDirect(initialCapacity)) : - new DefaultDataBuffer(ByteBuffer.allocate(initialCapacity)); + new DefaultDataBuffer(ByteBuffer.allocateDirect(initialCapacity), this) : + new DefaultDataBuffer(ByteBuffer.allocate(initialCapacity), this); } @Override public DefaultDataBuffer wrap(ByteBuffer byteBuffer) { ByteBuffer sliced = byteBuffer.slice(); - return new DefaultDataBuffer(sliced, 0, byteBuffer.remaining()); + return new DefaultDataBuffer(sliced, 0, byteBuffer.remaining(), this); } @Override diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/NettyDataBuffer.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/NettyDataBuffer.java index 0c03db43e6..8beb2f41b7 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/NettyDataBuffer.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/NettyDataBuffer.java @@ -38,16 +38,25 @@ import org.springframework.util.ObjectUtils; */ public class NettyDataBuffer implements DataBuffer { + private final NettyDataBufferAllocator allocator; + private ByteBuf byteBuf; /** * Creates a new {@code NettyDataBuffer} based on the given {@code ByteBuff}. * @param byteBuf the buffer to base this buffer on */ - public NettyDataBuffer(ByteBuf byteBuf) { + NettyDataBuffer(ByteBuf byteBuf, NettyDataBufferAllocator allocator) { Assert.notNull(byteBuf, "'byteBuf' must not be null"); + Assert.notNull(allocator, "'allocator' must not be null"); this.byteBuf = byteBuf; + this.allocator = allocator; + } + + @Override + public NettyDataBufferAllocator allocator() { + return allocator; } /** diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/NettyDataBufferAllocator.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/NettyDataBufferAllocator.java index 092e33518b..12f8dae0ab 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/NettyDataBufferAllocator.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/NettyDataBufferAllocator.java @@ -51,19 +51,19 @@ public class NettyDataBufferAllocator implements DataBufferAllocator { @Override public NettyDataBuffer allocateBuffer() { ByteBuf byteBuf = this.byteBufAllocator.buffer(); - return new NettyDataBuffer(byteBuf); + return new NettyDataBuffer(byteBuf, this); } @Override public NettyDataBuffer allocateBuffer(int initialCapacity) { ByteBuf byteBuf = this.byteBufAllocator.buffer(initialCapacity); - return new NettyDataBuffer(byteBuf); + return new NettyDataBuffer(byteBuf, this); } @Override public NettyDataBuffer wrap(ByteBuffer byteBuffer) { ByteBuf byteBuf = Unpooled.wrappedBuffer(byteBuffer); - return new NettyDataBuffer(byteBuf); + return new NettyDataBuffer(byteBuf, this); } /** @@ -72,7 +72,7 @@ public class NettyDataBufferAllocator implements DataBufferAllocator { * @return the wrapped buffer */ public NettyDataBuffer wrap(ByteBuf byteBuf) { - return new NettyDataBuffer(byteBuf); + return new NettyDataBuffer(byteBuf, this); } @Override From e56dda1864bed77ae5215599100f674982d3ef82 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Fri, 18 Mar 2016 14:11:25 +0100 Subject: [PATCH 260/498] Make JsonObjectDecoder use allocator property on DataBuffer. --- .../core/codec/support/JsonObjectDecoder.java | 18 ++++++++---------- .../web/client/reactive/WebClient.java | 2 +- .../RequestMappingHandlerAdapter.java | 2 +- .../codec/support/JsonObjectDecoderTests.java | 11 +++++------ 4 files changed, 15 insertions(+), 18 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectDecoder.java index f9f92357ae..4c5a8c32be 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectDecoder.java @@ -55,23 +55,21 @@ public class JsonObjectDecoder extends AbstractDecoder { private static final int ST_DECODING_ARRAY_STREAM = 2; - private final DataBufferAllocator allocator; - private final int maxObjectLength; private final boolean streamArrayElements; - public JsonObjectDecoder(DataBufferAllocator allocator) { + public JsonObjectDecoder() { // 1 MB - this(allocator, 1024 * 1024); + this(1024 * 1024); } - public JsonObjectDecoder(DataBufferAllocator allocator, int maxObjectLength) { - this(allocator, maxObjectLength, true); + public JsonObjectDecoder(int maxObjectLength) { + this(maxObjectLength, true); } - public JsonObjectDecoder(DataBufferAllocator allocator, boolean streamArrayElements) { - this(allocator, 1024 * 1024, streamArrayElements); + public JsonObjectDecoder(boolean streamArrayElements) { + this(1024 * 1024, streamArrayElements); } @@ -84,11 +82,10 @@ public class JsonObjectDecoder extends AbstractDecoder { * is an array, each of its entries is passed through the pipeline individually * and immediately after it was fully received, allowing for arrays with */ - public JsonObjectDecoder(DataBufferAllocator allocator, int maxObjectLength, + public JsonObjectDecoder(int maxObjectLength, boolean streamArrayElements) { super(new MimeType("application", "json", StandardCharsets.UTF_8), new MimeType("application", "*+json", StandardCharsets.UTF_8)); - this.allocator = allocator; if (maxObjectLength < 1) { throw new IllegalArgumentException("maxObjectLength must be a positive int"); } @@ -133,6 +130,7 @@ public class JsonObjectDecoder extends AbstractDecoder { return Flux.error(new IllegalStateException("object length exceeds " + maxObjectLength + ": " + this.writerIndex + " bytes discarded")); } + DataBufferAllocator allocator = b.allocator(); for (/* use current index */; this.index < this.writerIndex; this.index++) { byte c = this.input.getByte(this.index); if (this.state == ST_DECODING_NORMAL) { diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebClient.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebClient.java index d733acc803..e6488c1f7b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebClient.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebClient.java @@ -89,7 +89,7 @@ public final class WebClient { this.messageEncoders = Arrays.asList(new ByteBufferEncoder(allocator), new StringEncoder(allocator), new JacksonJsonEncoder(allocator)); this.messageDecoders = Arrays.asList(new ByteBufferDecoder(), new StringDecoder(allocator), - new JacksonJsonDecoder(new JsonObjectDecoder(allocator))); + new JacksonJsonDecoder(new JsonObjectDecoder())); } /** diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java index bd19cc446e..55de2dd7f8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java @@ -101,7 +101,7 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Initializin List> decoders = Arrays.asList(new ByteBufferDecoder(), new StringDecoder(allocator), - new JacksonJsonDecoder(new JsonObjectDecoder(allocator))); + new JacksonJsonDecoder(new JsonObjectDecoder())); this.argumentResolvers.add(new RequestParamArgumentResolver()); this.argumentResolvers.add(new RequestBodyArgumentResolver(decoders, this.conversionService)); diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JsonObjectDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JsonObjectDecoderTests.java index e3026f7d1d..622148efad 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JsonObjectDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JsonObjectDecoderTests.java @@ -20,11 +20,10 @@ import java.nio.charset.StandardCharsets; import org.junit.Test; import reactor.core.publisher.Flux; +import reactor.core.test.TestSubscriber; import org.springframework.core.io.buffer.DataBuffer; -import reactor.core.test.TestSubscriber; - /** * @author Sebastien Deleuze */ @@ -33,7 +32,7 @@ public class JsonObjectDecoderTests extends AbstractAllocatingTestCase { @Test public void decodeSingleChunkToJsonObject() { - JsonObjectDecoder decoder = new JsonObjectDecoder(allocator); + JsonObjectDecoder decoder = new JsonObjectDecoder(); Flux source = Flux.just(stringBuffer("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}")); Flux output = @@ -45,7 +44,7 @@ public class JsonObjectDecoderTests extends AbstractAllocatingTestCase { @Test public void decodeMultipleChunksToJsonObject() throws InterruptedException { - JsonObjectDecoder decoder = new JsonObjectDecoder(allocator); + JsonObjectDecoder decoder = new JsonObjectDecoder(); Flux source = Flux.just(stringBuffer("{\"foo\": \"foofoo\""), stringBuffer(", \"bar\": \"barbar\"}")); Flux output = @@ -57,7 +56,7 @@ public class JsonObjectDecoderTests extends AbstractAllocatingTestCase { @Test public void decodeSingleChunkToArray() throws InterruptedException { - JsonObjectDecoder decoder = new JsonObjectDecoder(allocator); + JsonObjectDecoder decoder = new JsonObjectDecoder(); Flux source = Flux.just(stringBuffer( "[{\"foo\": \"foofoo\", \"bar\": \"barbar\"},{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}]")); Flux output = @@ -70,7 +69,7 @@ public class JsonObjectDecoderTests extends AbstractAllocatingTestCase { @Test public void decodeMultipleChunksToArray() throws InterruptedException { - JsonObjectDecoder decoder = new JsonObjectDecoder(allocator); + JsonObjectDecoder decoder = new JsonObjectDecoder(); Flux source = Flux.just(stringBuffer("[{\"foo\": \"foofoo\", \"bar\""), stringBuffer( ": \"barbar\"},{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}]")); From 1836b2825b3c844097105322aa9e659409906e86 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Fri, 18 Mar 2016 16:05:30 +0100 Subject: [PATCH 261/498] Added DataBufferAllocator parameter to Encoder.encode, and use that instead of AbstractAllocatingEncoder base class. --- .../springframework/core/codec/Encoder.java | 8 ++-- .../support/AbstractAllocatingEncoder.java | 42 ------------------- .../core/codec/support/ByteBufferEncoder.java | 13 +++--- .../codec/support/JacksonJsonEncoder.java | 36 +++++++++------- .../core/codec/support/Jaxb2Encoder.java | 17 ++++---- .../core/codec/support/JsonObjectEncoder.java | 9 ++-- .../core/codec/support/StringEncoder.java | 16 +++---- .../codec/support/ByteBufferEncoderTests.java | 6 +-- .../support/JacksonJsonEncoderTests.java | 9 ++-- .../core/codec/support/Jaxb2EncoderTests.java | 9 ++-- .../codec/support/JsonObjectEncoderTests.java | 17 ++++---- .../codec/support/StringEncoderTests.java | 11 +++-- 12 files changed, 83 insertions(+), 110 deletions(-) delete mode 100644 spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractAllocatingEncoder.java diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/Encoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/Encoder.java index 0f82c73081..0a53c9552b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/Encoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/Encoder.java @@ -16,7 +16,6 @@ package org.springframework.core.codec; -import java.nio.ByteBuffer; import java.util.List; import org.reactivestreams.Publisher; @@ -24,6 +23,7 @@ import reactor.core.publisher.Flux; import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferAllocator; import org.springframework.util.MimeType; /** @@ -44,14 +44,16 @@ public interface Encoder { boolean canEncode(ResolvableType type, MimeType mimeType, Object... hints); /** - * Encode an input stream of {@code T} to an output {@link ByteBuffer} stream. + * Encode an input stream of {@code T} to an output {@link DataBuffer} stream. * @param inputStream the input stream to process. + * @param allocator a buffer allocator used to create the output * @param type the stream element type to process. * @param mimeType the mime type to process. * @param hints Additional information about how to do decode, optional. * @return the output stream */ - Flux encode(Publisher inputStream, ResolvableType type, + Flux encode(Publisher inputStream, + DataBufferAllocator allocator, ResolvableType type, MimeType mimeType, Object... hints); /** diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractAllocatingEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractAllocatingEncoder.java deleted file mode 100644 index 0275931fc8..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractAllocatingEncoder.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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.support; - -import org.springframework.core.io.buffer.DataBufferAllocator; -import org.springframework.util.Assert; -import org.springframework.util.MimeType; - -/** - * @author Arjen Poutsma - */ -public abstract class AbstractAllocatingEncoder extends AbstractEncoder { - - private final DataBufferAllocator allocator; - - public AbstractAllocatingEncoder(DataBufferAllocator allocator, - MimeType... supportedMimeTypes) { - super(supportedMimeTypes); - Assert.notNull(allocator, "'allocator' must not be null"); - - this.allocator = allocator; - } - - public DataBufferAllocator allocator() { - return allocator; - } - -} diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferEncoder.java index 3b72c7e355..4e05e1d62b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferEncoder.java @@ -30,10 +30,10 @@ import org.springframework.util.MimeTypeUtils; /** * @author Sebastien Deleuze */ -public class ByteBufferEncoder extends AbstractAllocatingEncoder { +public class ByteBufferEncoder extends AbstractEncoder { - public ByteBufferEncoder(DataBufferAllocator allocator) { - super(allocator, MimeTypeUtils.ALL); + public ByteBufferEncoder() { + super(MimeTypeUtils.ALL); } @@ -45,12 +45,11 @@ public class ByteBufferEncoder extends AbstractAllocatingEncoder { @Override public Flux encode(Publisher inputStream, - ResolvableType type, - MimeType mimeType, Object... hints) { - + DataBufferAllocator allocator, ResolvableType type, MimeType mimeType, + Object... hints) { //noinspection unchecked return Flux.from(inputStream).map(byteBuffer -> { - DataBuffer dataBuffer = allocator().allocateBuffer(byteBuffer.remaining()); + DataBuffer dataBuffer = allocator.allocateBuffer(byteBuffer.remaining()); dataBuffer.write(byteBuffer); return dataBuffer; }); diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonEncoder.java index 6e86446f8a..fe86fe85c6 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonEncoder.java @@ -30,6 +30,7 @@ import org.springframework.core.codec.CodecException; import org.springframework.core.codec.Encoder; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferAllocator; +import org.springframework.core.io.buffer.DefaultDataBufferAllocator; import org.springframework.util.MimeType; /** @@ -38,41 +39,46 @@ import org.springframework.util.MimeType; * @author Sebastien Deleuze * @see JacksonJsonDecoder */ -public class JacksonJsonEncoder extends AbstractAllocatingEncoder { +public class JacksonJsonEncoder extends AbstractEncoder { private final ObjectMapper mapper; private Encoder postProcessor; - public JacksonJsonEncoder(DataBufferAllocator allocator) { - this(allocator, new ObjectMapper(), null); + public JacksonJsonEncoder() { + this(new ObjectMapper(), null); } - public JacksonJsonEncoder(DataBufferAllocator allocator, - Encoder postProcessor) { - this(allocator, new ObjectMapper(), postProcessor); + public JacksonJsonEncoder(Encoder postProcessor) { + this(new ObjectMapper(), postProcessor); } - public JacksonJsonEncoder(DataBufferAllocator allocator, ObjectMapper mapper, + public JacksonJsonEncoder(ObjectMapper mapper, Encoder postProcessor) { - super(allocator, new MimeType("application", "json", StandardCharsets.UTF_8), + super(new MimeType("application", "json", StandardCharsets.UTF_8), new MimeType("application", "*+json", StandardCharsets.UTF_8)); this.mapper = mapper; this.postProcessor = postProcessor; } @Override - public Flux encode(Publisher inputStream, - ResolvableType type, MimeType mimeType, Object... hints) { + public Flux encode(Publisher inputStream, + DataBufferAllocator allocator, ResolvableType type, MimeType mimeType, + Object... hints) { Publisher stream = (inputStream instanceof Mono ? - ((Mono)inputStream).map(this::serialize) : - Flux.from(inputStream).map(this::serialize)); - return (this.postProcessor == null ? Flux.from(stream) : this.postProcessor.encode(stream, type, mimeType, hints)); + ((Mono) inputStream).map(value -> serialize(value, allocator)) : + Flux.from(inputStream).map(value -> serialize(value, allocator))); + // TODO: figure out why using the parameter allocator for the postprocessor + // commits the response too early + DefaultDataBufferAllocator tempAllocator = new DefaultDataBufferAllocator(); + + return (this.postProcessor == null ? Flux.from(stream) : + this.postProcessor.encode(stream, tempAllocator, type, mimeType, hints)); } - private DataBuffer serialize(Object value) { - DataBuffer buffer = allocator().allocateBuffer(); + private DataBuffer serialize(Object value, DataBufferAllocator allocator) { + DataBuffer buffer = allocator.allocateBuffer(); OutputStream outputStream = buffer.asOutputStream(); try { this.mapper.writeValue(outputStream, value); diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Encoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Encoder.java index 652c7e3cdb..5c95b9a00f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Encoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Encoder.java @@ -43,23 +43,22 @@ import org.springframework.util.MimeTypeUtils; * @author Sebastien Deleuze * @see Jaxb2Decoder */ -public class Jaxb2Encoder extends AbstractAllocatingEncoder { +public class Jaxb2Encoder extends AbstractEncoder { private final ConcurrentMap, JAXBContext> jaxbContexts = new ConcurrentHashMap<>(64); - public Jaxb2Encoder(DataBufferAllocator allocator) { - super(allocator, MimeTypeUtils.APPLICATION_XML, MimeTypeUtils.TEXT_XML); + public Jaxb2Encoder() { + super(MimeTypeUtils.APPLICATION_XML, MimeTypeUtils.TEXT_XML); } - @Override - public Flux encode(Publisher messageStream, - ResolvableType type, - MimeType mimeType, Object... hints) { + public Flux encode(Publisher inputStream, + DataBufferAllocator allocator, ResolvableType type, MimeType mimeType, + Object... hints) { - return Flux.from(messageStream).map(value -> { + return Flux.from(inputStream).map(value -> { try { - DataBuffer buffer = allocator().allocateBuffer(1024); + DataBuffer buffer = allocator.allocateBuffer(1024); OutputStream outputStream = buffer.asOutputStream(); Class clazz = ClassUtils.getUserClass(value); Marshaller marshaller = createMarshaller(clazz); diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectEncoder.java index 29e58fb4ff..804856bb1e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectEncoder.java @@ -42,21 +42,22 @@ import org.springframework.util.MimeType; * * @see JsonObjectDecoder */ -public class JsonObjectEncoder extends AbstractAllocatingEncoder { +public class JsonObjectEncoder extends AbstractEncoder { - public JsonObjectEncoder(DataBufferAllocator allocator) { - super(allocator, new MimeType("application", "json", StandardCharsets.UTF_8), + public JsonObjectEncoder() { + super(new MimeType("application", "json", StandardCharsets.UTF_8), new MimeType("application", "*+json", StandardCharsets.UTF_8)); } @Override public Flux encode(Publisher inputStream, + DataBufferAllocator allocator, ResolvableType type, MimeType mimeType, Object... hints) { if (inputStream instanceof Mono) { return Flux.from(inputStream); } return Flux.from(inputStream) - .lift(s -> new JsonArrayEncoderBarrier(s, allocator())); + .lift(s -> new JsonArrayEncoderBarrier(s, allocator)); } private static class JsonArrayEncoderBarrier diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringEncoder.java index e7a80b693e..810d6de311 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringEncoder.java @@ -33,12 +33,12 @@ import org.springframework.util.MimeType; * @author Sebastien Deleuze * @see StringDecoder */ -public class StringEncoder extends AbstractAllocatingEncoder { +public class StringEncoder extends AbstractEncoder { public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; - public StringEncoder(DataBufferAllocator allocator) { - super(allocator, new MimeType("text", "plain", DEFAULT_CHARSET)); + public StringEncoder() { + super(new MimeType("text", "plain", DEFAULT_CHARSET)); } @@ -49,9 +49,9 @@ public class StringEncoder extends AbstractAllocatingEncoder { } @Override - public Flux encode(Publisher elementStream, - ResolvableType type, MimeType mimeType, Object... hints) { - + public Flux encode(Publisher inputStream, + DataBufferAllocator allocator, ResolvableType type, MimeType mimeType, + Object... hints) { Charset charset; if (mimeType != null && mimeType.getCharSet() != null) { charset = mimeType.getCharSet(); @@ -59,9 +59,9 @@ public class StringEncoder extends AbstractAllocatingEncoder { else { charset = DEFAULT_CHARSET; } - return Flux.from(elementStream).map(s -> { + return Flux.from(inputStream).map(s -> { byte[] bytes = s.getBytes(charset); - DataBuffer dataBuffer = allocator().allocateBuffer(bytes.length); + DataBuffer dataBuffer = allocator.allocateBuffer(bytes.length); dataBuffer.write(bytes); return dataBuffer; }); diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ByteBufferEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ByteBufferEncoderTests.java index 3e95dcef1f..9e238c50ad 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ByteBufferEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ByteBufferEncoderTests.java @@ -23,13 +23,13 @@ import org.junit.Before; import org.junit.Test; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; +import reactor.core.test.TestSubscriber; import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.MediaType; import static org.junit.Assert.*; -import reactor.core.test.TestSubscriber; /** * @author Sebastien Deleuze @@ -40,7 +40,7 @@ public class ByteBufferEncoderTests extends AbstractAllocatingTestCase { @Before public void createEncoder() { - encoder = new ByteBufferEncoder(allocator); + encoder = new ByteBufferEncoder(); } @Test @@ -57,7 +57,7 @@ public class ByteBufferEncoderTests extends AbstractAllocatingTestCase { Flux source = Flux.just(ByteBuffer.wrap(fooBytes), ByteBuffer.wrap(barBytes)); - Flux output = encoder.encode(source, + Flux output = encoder.encode(source, allocator, ResolvableType.forClassWithGenerics(Publisher.class, ByteBuffer.class), null); TestSubscriber testSubscriber = new TestSubscriber<>(); diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonEncoderTests.java index e61dc616dc..53383547f4 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonEncoderTests.java @@ -21,11 +21,12 @@ import java.nio.charset.StandardCharsets; import org.junit.Before; import org.junit.Test; import reactor.core.publisher.Flux; +import reactor.core.test.TestSubscriber; import org.springframework.http.MediaType; -import static org.junit.Assert.*; -import reactor.core.test.TestSubscriber; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; /** * @author Sebastien Deleuze @@ -36,7 +37,7 @@ public class JacksonJsonEncoderTests extends AbstractAllocatingTestCase { @Before public void createEncoder() { - encoder = new JacksonJsonEncoder(allocator); + encoder = new JacksonJsonEncoder(); } @Test @@ -48,7 +49,7 @@ public class JacksonJsonEncoderTests extends AbstractAllocatingTestCase { @Test public void write() { Flux source = Flux.just(new Pojo("foofoo", "barbar"), new Pojo("foofoofoo", "barbarbar")); - Flux output = encoder.encode(source, null, null).map(chunk -> { + Flux output = encoder.encode(source, allocator, null, null).map(chunk -> { byte[] b = new byte[chunk.readableByteCount()]; chunk.read(b); return new String(b, StandardCharsets.UTF_8); diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2EncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2EncoderTests.java index a9ef2ea615..0f7a802962 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2EncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2EncoderTests.java @@ -21,11 +21,12 @@ import java.nio.charset.StandardCharsets; import org.junit.Before; import org.junit.Test; import reactor.core.publisher.Flux; +import reactor.core.test.TestSubscriber; import org.springframework.http.MediaType; -import static org.junit.Assert.*; -import reactor.core.test.TestSubscriber; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; /** * @author Sebastien Deleuze @@ -36,7 +37,7 @@ public class Jaxb2EncoderTests extends AbstractAllocatingTestCase { @Before public void createEncoder() { - encoder = new Jaxb2Encoder(allocator); + encoder = new Jaxb2Encoder(); } @Test @@ -49,7 +50,7 @@ public class Jaxb2EncoderTests extends AbstractAllocatingTestCase { @Test public void encode() { Flux source = Flux.just(new Pojo("foofoo", "barbar"), new Pojo("foofoofoo", "barbarbar")); - Flux output = encoder.encode(source, null, null).map(chunk -> { + Flux output = encoder.encode(source, allocator, null, null).map(chunk -> { byte[] b = new byte[chunk.readableByteCount()]; chunk.read(b); return new String(b, StandardCharsets.UTF_8); diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JsonObjectEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JsonObjectEncoderTests.java index 64116126e9..0d4c9f1add 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JsonObjectEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JsonObjectEncoderTests.java @@ -22,11 +22,10 @@ import org.junit.Before; import org.junit.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.core.test.TestSubscriber; import org.springframework.core.io.buffer.DataBuffer; -import reactor.core.test.TestSubscriber; - /** * @author Sebastien Deleuze */ @@ -36,14 +35,15 @@ public class JsonObjectEncoderTests extends AbstractAllocatingTestCase { @Before public void createEncoder() { - encoder = new JsonObjectEncoder(allocator); + encoder = new JsonObjectEncoder(); } @Test public void encodeSingleElementFlux() throws InterruptedException { Flux source = Flux.just(stringBuffer("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}")); - Flux output = Flux.from(encoder.encode(source, null, null)).map(chunk -> { + Flux output = + Flux.from(encoder.encode(source, allocator, null, null)).map(chunk -> { byte[] b = new byte[chunk.readableByteCount()]; chunk.read(b); return new String(b, StandardCharsets.UTF_8); @@ -57,7 +57,8 @@ public class JsonObjectEncoderTests extends AbstractAllocatingTestCase { public void encodeSingleElementMono() throws InterruptedException { Mono source = Mono.just(stringBuffer("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}")); - Flux output = Flux.from(encoder.encode(source, null, null)).map(chunk -> { + Flux output = + Flux.from(encoder.encode(source, allocator, null, null)).map(chunk -> { byte[] b = new byte[chunk.readableByteCount()]; chunk.read(b); return new String(b, StandardCharsets.UTF_8); @@ -72,7 +73,8 @@ public class JsonObjectEncoderTests extends AbstractAllocatingTestCase { Flux source = Flux.just(stringBuffer("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}"), stringBuffer("{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}")); - Flux output = Flux.from(encoder.encode(source, null, null)).map(chunk -> { + Flux output = + Flux.from(encoder.encode(source, allocator, null, null)).map(chunk -> { byte[] b = new byte[chunk.readableByteCount()]; chunk.read(b); return new String(b, StandardCharsets.UTF_8); @@ -91,7 +93,8 @@ public class JsonObjectEncoderTests extends AbstractAllocatingTestCase { stringBuffer("{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}"), stringBuffer("{\"foo\": \"foofoofoofoo\", \"bar\": \"barbarbarbar\"}") ); - Flux output = Flux.from(encoder.encode(source, null, null)).map(chunk -> { + Flux output = + Flux.from(encoder.encode(source, allocator, null, null)).map(chunk -> { byte[] b = new byte[chunk.readableByteCount()]; chunk.read(b); return new String(b, StandardCharsets.UTF_8); diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringEncoderTests.java index 415f2883d2..d69c284638 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringEncoderTests.java @@ -23,12 +23,13 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import reactor.core.publisher.Flux; +import reactor.core.test.TestSubscriber; import org.springframework.core.ResolvableType; import org.springframework.http.MediaType; -import static org.junit.Assert.*; -import reactor.core.test.TestSubscriber; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; /** * @author Sebastien Deleuze @@ -40,7 +41,7 @@ public class StringEncoderTests extends AbstractAllocatingTestCase { @Before public void createEncoder() { - encoder = new StringEncoder(allocator); + encoder = new StringEncoder(); } @Test @@ -52,7 +53,9 @@ public class StringEncoderTests extends AbstractAllocatingTestCase { @Test public void write() throws InterruptedException { - Flux output = Flux.from(encoder.encode(Flux.just("foo"), null, null)).map(chunk -> { + Flux output = + Flux.from(encoder.encode(Flux.just("foo"), allocator, null, null)) + .map(chunk -> { byte[] b = new byte[chunk.readableByteCount()]; chunk.read(b); return new String(b, StandardCharsets.UTF_8); From 7f786ce4d7a8b51a02133d7db404d9f038d5a927 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Fri, 18 Mar 2016 16:14:33 +0100 Subject: [PATCH 262/498] Added DataBufferAllocator property to ReactiveHttpOutputMessage, and use that to pass on to Encoder.encode(). --- .../http/ReactiveHttpOutputMessage.java | 8 ++++ .../reactive/ReactorClientHttpRequest.java | 5 +++ .../reactive/RxNettyClientHttpRequest.java | 7 +++- .../reactive/AbstractServerHttpResponse.java | 39 +++++++++++++------ .../reactive/ReactorHttpHandlerAdapter.java | 4 +- .../reactive/ReactorServerHttpResponse.java | 7 +++- .../reactive/RxNettyHttpHandlerAdapter.java | 3 +- .../reactive/RxNettyServerHttpResponse.java | 6 ++- .../reactive/ServletHttpHandlerAdapter.java | 3 +- .../reactive/ServletServerHttpResponse.java | 5 ++- .../reactive/UndertowHttpHandlerAdapter.java | 5 ++- .../reactive/UndertowServerHttpResponse.java | 7 ++-- .../reactive/DefaultHttpRequestBuilder.java | 6 ++- .../web/client/reactive/WebClient.java | 4 +- .../annotation/ResponseBodyResultHandler.java | 8 +++- .../reactive/MockServerHttpResponse.java | 10 +++++ .../reactive/ServerHttpResponseTests.java | 9 +++-- .../reactive/DispatcherHandlerErrorTests.java | 3 +- .../RequestMappingIntegrationTests.java | 10 ++--- .../ResponseBodyResultHandlerTests.java | 3 +- 20 files changed, 109 insertions(+), 43 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java b/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java index fb6b06150b..0eaa89515d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java @@ -22,6 +22,7 @@ import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferAllocator; /** * A "reactive" HTTP output message that accepts output as a {@link Publisher}. @@ -48,4 +49,11 @@ public interface ReactiveHttpOutputMessage extends HttpMessage { */ Mono setBody(Publisher body); + /** + * Returns a {@link DataBufferAllocator} that can be used for creating the body. + * @return a buffer allocator + * @see #setBody(Publisher) + */ + DataBufferAllocator allocator(); + } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java index 5ff05bc20d..4179184c28 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java @@ -61,6 +61,11 @@ public class ReactorClientHttpRequest extends AbstractClientHttpRequest { this.httpClient = httpClient; } + @Override + public DataBufferAllocator allocator() { + return this.allocator; + } + @Override public HttpMethod getMethod() { return this.httpMethod; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpRequest.java index 7ab96b37c6..16887b0225 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpRequest.java @@ -17,7 +17,6 @@ package org.springframework.http.client.reactive; import java.net.URI; -import java.util.Collection; import java.util.List; import java.util.Map; @@ -32,6 +31,7 @@ import reactor.core.publisher.Mono; import rx.Observable; import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferAllocator; import org.springframework.core.io.buffer.NettyDataBufferAllocator; import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; @@ -60,6 +60,11 @@ public class RxNettyClientHttpRequest extends AbstractClientHttpRequest { this.allocator = allocator; } + @Override + public DataBufferAllocator allocator() { + return this.allocator; + } + /** * Set the body of the message to the given {@link Publisher}. * diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java index 91eff4024e..a06bf640c3 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java @@ -13,17 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.http.server.reactive; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Supplier; import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferAllocator; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseCookie; import org.springframework.util.Assert; @@ -39,32 +41,48 @@ import org.springframework.util.MultiValueMap; */ public abstract class AbstractServerHttpResponse implements ServerHttpResponse { + private static final int STATE_NEW = 1; + + private static final int STATE_COMMITTING = 2; + + private static final int STATE_COMMITTED = 3; + private final HttpHeaders headers; private final MultiValueMap cookies; - private AtomicReference state = new AtomicReference<>(State.NEW); + private final AtomicInteger state = new AtomicInteger(STATE_NEW); private final List>> beforeCommitActions = new ArrayList<>(4); + private final DataBufferAllocator allocator; - protected AbstractServerHttpResponse() { + public AbstractServerHttpResponse(DataBufferAllocator allocator) { + Assert.notNull(allocator, "'allocator' must not be null"); + + this.allocator = allocator; this.headers = new HttpHeaders(); this.cookies = new LinkedMultiValueMap(); } + @Override + public final DataBufferAllocator allocator() { + return this.allocator; + } @Override public HttpHeaders getHeaders() { - if (State.COMITTED.equals(this.state.get())) { + if (STATE_COMMITTED == this.state.get()) { return HttpHeaders.readOnlyHttpHeaders(this.headers); } - return this.headers; + else { + return this.headers; + } } @Override public MultiValueMap getCookies() { - if (State.COMITTED.equals(this.state.get())) { + if (STATE_COMMITTED == this.state.get()) { return CollectionUtils.unmodifiableMultiValueMap(this.cookies); } return this.cookies; @@ -78,16 +96,16 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse { private Mono applyBeforeCommit() { Mono mono = Mono.empty(); - if (this.state.compareAndSet(State.NEW, State.COMMITTING)) { + if (this.state.compareAndSet(STATE_NEW, STATE_COMMITTING)) { for (Supplier> action : this.beforeCommitActions) { - mono = mono.after(() -> action.get()); + mono = mono.after(action); } mono = mono.otherwise(ex -> { // Ignore errors from beforeCommit actions return Mono.empty(); }); mono = mono.after(() -> { - this.state.set(State.COMITTED); + this.state.set(STATE_COMMITTED); writeHeaders(); writeCookies(); return Mono.empty(); @@ -125,7 +143,4 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse { return applyBeforeCommit(); } - - private enum State { NEW, COMMITTING, COMITTED } - } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorHttpHandlerAdapter.java index cf7136a4fa..7c5442b805 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorHttpHandlerAdapter.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.http.server.reactive; import reactor.core.publisher.Mono; @@ -44,7 +45,8 @@ public class ReactorHttpHandlerAdapter public Mono apply(HttpChannel channel) { ReactorServerHttpRequest adaptedRequest = new ReactorServerHttpRequest(channel, allocator); - ReactorServerHttpResponse adaptedResponse = new ReactorServerHttpResponse(channel); + ReactorServerHttpResponse adaptedResponse = + new ReactorServerHttpResponse(channel, allocator); return this.httpHandler.handle(adaptedRequest, adaptedResponse); } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java index e2f220c137..8951934d55 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.http.server.reactive; import java.time.Duration; @@ -27,6 +28,7 @@ import reactor.io.netty.http.model.Cookie; import reactor.io.netty.http.model.Status; import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferAllocator; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseCookie; import org.springframework.util.Assert; @@ -41,8 +43,9 @@ public class ReactorServerHttpResponse extends AbstractServerHttpResponse { private final HttpChannel channel; - - public ReactorServerHttpResponse(HttpChannel response) { + public ReactorServerHttpResponse(HttpChannel response, + DataBufferAllocator allocator) { + super(allocator); Assert.notNull("'response' must not be null."); this.channel = response; } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyHttpHandlerAdapter.java index 6023d34ff7..a3ec1909b1 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyHttpHandlerAdapter.java @@ -46,7 +46,8 @@ public class RxNettyHttpHandlerAdapter implements RequestHandler result = this.httpHandler.handle(adaptedRequest, adaptedResponse); return RxJava1ObservableConverter.from(result); } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java index ebcf5fad1b..2d310c93de 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java @@ -29,6 +29,7 @@ import rx.Observable; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.NettyDataBuffer; +import org.springframework.core.io.buffer.NettyDataBufferAllocator; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseCookie; import org.springframework.util.Assert; @@ -43,8 +44,11 @@ public class RxNettyServerHttpResponse extends AbstractServerHttpResponse { private final HttpServerResponse response; - public RxNettyServerHttpResponse(HttpServerResponse response) { + public RxNettyServerHttpResponse(HttpServerResponse response, + NettyDataBufferAllocator allocator) { + super(allocator); Assert.notNull("'response', response must not be null."); + this.response = response; } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java index 80b447c43c..1182221df4 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java @@ -82,7 +82,8 @@ public class ServletHttpHandlerAdapter extends HttpServlet { this.bufferSize); ServletServerHttpResponse response = - new ServletServerHttpResponse(synchronizer, this.bufferSize); + new ServletServerHttpResponse(synchronizer, this.bufferSize, + this.allocator); HandlerResultSubscriber resultSubscriber = new HandlerResultSubscriber(synchronizer); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java index 465a73eff3..35cfbb00fe 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java @@ -34,6 +34,7 @@ import org.reactivestreams.Subscription; import reactor.core.publisher.Mono; import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferAllocator; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseCookie; @@ -53,7 +54,8 @@ public class ServletServerHttpResponse extends AbstractServerHttpResponse { private final ResponseBodySubscriber responseBodySubscriber; public ServletServerHttpResponse(ServletAsyncContextSynchronizer synchronizer, - int bufferSize) throws IOException { + int bufferSize, DataBufferAllocator allocator) throws IOException { + super(allocator); Assert.notNull(synchronizer, "'synchronizer' must not be null"); this.response = (HttpServletResponse) synchronizer.getResponse(); @@ -62,7 +64,6 @@ public class ServletServerHttpResponse extends AbstractServerHttpResponse { this.response.getOutputStream().setWriteListener(responseBodySubscriber); } - public HttpServletResponse getServletResponse() { return this.response; } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java index d391f0c5f0..340ac750b0 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java @@ -57,6 +57,7 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle private final HttpHandler delegate; + // TODO: use UndertowDBA when introduced private final DataBufferAllocator allocator; public UndertowHttpHandlerAdapter(HttpHandler delegate, @@ -76,7 +77,9 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle ResponseBodySubscriber responseBodySubscriber = new ResponseBodySubscriber(exchange); ServerHttpResponse response = new UndertowServerHttpResponse(exchange, - publisher -> Mono.from(subscriber -> publisher.subscribe(responseBodySubscriber))); + publisher -> Mono + .from(subscriber -> publisher.subscribe(responseBodySubscriber)), + allocator); exchange.dispatch(); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java index 910f913f70..a10806ca57 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java @@ -28,6 +28,7 @@ import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferAllocator; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseCookie; import org.springframework.util.Assert; @@ -44,10 +45,10 @@ public class UndertowServerHttpResponse extends AbstractServerHttpResponse { private final Function, Mono> responseBodyWriter; - public UndertowServerHttpResponse(HttpServerExchange exchange, - Function, Mono> responseBodyWriter) { - + Function, Mono> responseBodyWriter, + DataBufferAllocator allocator) { + super(allocator); Assert.notNull(exchange, "'exchange' is required."); Assert.notNull(responseBodyWriter, "'responseBodyWriter' must not be null"); this.exchange = exchange; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultHttpRequestBuilder.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultHttpRequestBuilder.java index 5ee4731a12..c6105e2582 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultHttpRequestBuilder.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultHttpRequestBuilder.java @@ -30,6 +30,7 @@ import reactor.core.publisher.Flux; import org.springframework.core.ResolvableType; import org.springframework.core.codec.Encoder; +import org.springframework.core.io.buffer.DataBufferAllocator; import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -146,7 +147,10 @@ public class DefaultHttpRequestBuilder implements HttpRequestBuilder { Optional> messageEncoder = resolveEncoder(requestBodyType, mediaType); if (messageEncoder.isPresent()) { - request.setBody(messageEncoder.get().encode(this.contentPublisher, requestBodyType, mediaType)); + DataBufferAllocator allocator = request.allocator(); + request.setBody(messageEncoder.get() + .encode(this.contentPublisher, allocator, requestBodyType, + mediaType)); } else { // TODO: wrap with client exception? diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebClient.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebClient.java index e6488c1f7b..317048314d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebClient.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebClient.java @@ -86,8 +86,8 @@ public final class WebClient { public WebClient(ClientHttpRequestFactory requestFactory) { this.requestFactory = requestFactory; DataBufferAllocator allocator = new DefaultDataBufferAllocator(); - this.messageEncoders = Arrays.asList(new ByteBufferEncoder(allocator), new StringEncoder(allocator), - new JacksonJsonEncoder(allocator)); + this.messageEncoders = Arrays.asList(new ByteBufferEncoder(), new StringEncoder(), + new JacksonJsonEncoder()); this.messageDecoders = Arrays.asList(new ByteBufferDecoder(), new StringDecoder(allocator), new JacksonJsonDecoder(new JsonObjectDecoder())); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java index 0e5af148dd..10f4470754 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -36,6 +36,7 @@ import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.codec.Encoder; import org.springframework.core.convert.ConversionService; +import org.springframework.core.io.buffer.DataBufferAllocator; import org.springframework.http.MediaType; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; @@ -192,7 +193,10 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered if (encoder != null) { ServerHttpResponse response = exchange.getResponse(); response.getHeaders().setContentType(selectedMediaType); - return response.setBody(encoder.encode((Publisher) publisher, elementType, selectedMediaType)); + DataBufferAllocator allocator = response.allocator(); + return response.setBody( + encoder.encode((Publisher) publisher, allocator, elementType, + selectedMediaType)); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java index 42a672ce8d..75a54e0415 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.http.server.reactive; import java.util.function.Supplier; @@ -22,6 +23,8 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferAllocator; +import org.springframework.core.io.buffer.DefaultDataBufferAllocator; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseCookie; @@ -41,6 +44,8 @@ public class MockServerHttpResponse implements ServerHttpResponse { private Publisher body; + private DataBufferAllocator allocator = new DefaultDataBufferAllocator(); + @Override public void setStatusCode(HttpStatus status) { @@ -80,4 +85,9 @@ public class MockServerHttpResponse implements ServerHttpResponse { return Mono.empty(); } + @Override + public DataBufferAllocator allocator() { + return this.allocator; + } + } diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java index dc2f5bf1b3..6b2977598f 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.http.server.reactive; import java.nio.ByteBuffer; @@ -31,10 +32,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseCookie; import static junit.framework.TestCase.assertTrue; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertSame; +import static org.junit.Assert.*; /** * @author Rossen Stoyanchev @@ -147,6 +145,9 @@ public class ServerHttpResponseTests { private final List content = new ArrayList<>(); + public TestServerHttpResponse() { + super(new DefaultDataBufferAllocator()); + } @Override public void setStatusCode(HttpStatus status) { diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java index 21adec20d7..92617fda03 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java @@ -232,8 +232,7 @@ public class DispatcherHandlerErrorTests { @Bean public ResponseBodyResultHandler resultHandler() { - List> encoders = Collections - .singletonList(new StringEncoder(new DefaultDataBufferAllocator())); + List> encoders = Collections.singletonList(new StringEncoder()); return new ResponseBodyResultHandler(encoders, new DefaultConversionService()); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java index 250dcc6645..2ecc53a10c 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java @@ -379,9 +379,8 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @Bean public ResponseBodyResultHandler responseBodyResultHandler() { - List> encoders = Arrays.asList( - new ByteBufferEncoder(this.allocator), new StringEncoder(this.allocator), - new JacksonJsonEncoder(this.allocator, new JsonObjectEncoder(this.allocator))); + List> encoders = Arrays.asList(new ByteBufferEncoder(), + new StringEncoder(), new JacksonJsonEncoder(new JsonObjectEncoder())); ResponseBodyResultHandler resultHandler = new ResponseBodyResultHandler(encoders, conversionService()); resultHandler.setOrder(1); return resultHandler; @@ -458,8 +457,9 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @RequestMapping("/raw") public Publisher rawResponseBody() { - JacksonJsonEncoder encoder = new JacksonJsonEncoder(new DefaultDataBufferAllocator()); - return encoder.encode(Mono.just(new Person("Robert")), + DataBufferAllocator allocator = new DefaultDataBufferAllocator(); + JacksonJsonEncoder encoder = new JacksonJsonEncoder(); + return encoder.encode(Mono.just(new Person("Robert")), allocator, ResolvableType.forClass(Person.class), MediaType.APPLICATION_JSON).map(DataBuffer::asByteBuffer); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandlerTests.java index b0e0b09c50..8216843c3f 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandlerTests.java @@ -24,7 +24,6 @@ import org.reactivestreams.Publisher; import org.springframework.core.ResolvableType; import org.springframework.core.codec.support.StringEncoder; import org.springframework.core.convert.support.DefaultConversionService; -import org.springframework.core.io.buffer.DefaultDataBufferAllocator; import org.springframework.ui.ExtendedModelMap; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.method.HandlerMethod; @@ -42,7 +41,7 @@ public class ResponseBodyResultHandlerTests { @Test public void supports() throws NoSuchMethodException { ResponseBodyResultHandler handler = new ResponseBodyResultHandler(Collections.singletonList( - new StringEncoder(new DefaultDataBufferAllocator())), + new StringEncoder()), new DefaultConversionService()); TestController controller = new TestController(); From f7c6c69e518c54daf637e00089b8f93fdca14ba7 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Tue, 26 Jan 2016 14:40:54 +0100 Subject: [PATCH 263/498] Improve JacksonJsonEncoder This commit removes the need for the custom Subscriber in JsonObjectEncoder, and replaces it with higher-level Flux and Mono-based solution. --- .../codec/support/JacksonJsonEncoder.java | 52 ++++--- .../core/codec/support/JsonObjectDecoder.java | 2 - .../core/codec/support/JsonObjectEncoder.java | 140 ------------------ .../reactive/DefaultHttpRequestBuilder.java | 5 +- .../support/JacksonJsonEncoderTests.java | 4 +- .../codec/support/JsonObjectEncoderTests.java | 110 -------------- .../RequestMappingIntegrationTests.java | 3 +- 7 files changed, 38 insertions(+), 278 deletions(-) delete mode 100644 spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectEncoder.java delete mode 100644 spring-web-reactive/src/test/java/org/springframework/core/codec/support/JsonObjectEncoderTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonEncoder.java index fe86fe85c6..21293e72d3 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonEncoder.java @@ -27,54 +27,59 @@ import reactor.core.publisher.Mono; import org.springframework.core.ResolvableType; import org.springframework.core.codec.CodecException; -import org.springframework.core.codec.Encoder; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferAllocator; -import org.springframework.core.io.buffer.DefaultDataBufferAllocator; +import org.springframework.util.Assert; import org.springframework.util.MimeType; /** * Encode from an {@code Object} stream to a byte stream of JSON objects. * * @author Sebastien Deleuze + * @author Arjen Poutsma * @see JacksonJsonDecoder */ public class JacksonJsonEncoder extends AbstractEncoder { private final ObjectMapper mapper; - private Encoder postProcessor; - public JacksonJsonEncoder() { - this(new ObjectMapper(), null); + this(new ObjectMapper()); } - public JacksonJsonEncoder(Encoder postProcessor) { - this(new ObjectMapper(), postProcessor); - } - - public JacksonJsonEncoder(ObjectMapper mapper, - Encoder postProcessor) { + public JacksonJsonEncoder(ObjectMapper mapper) { super(new MimeType("application", "json", StandardCharsets.UTF_8), new MimeType("application", "*+json", StandardCharsets.UTF_8)); + Assert.notNull(mapper, "'mapper' must not be null"); + this.mapper = mapper; - this.postProcessor = postProcessor; } @Override public Flux encode(Publisher inputStream, DataBufferAllocator allocator, ResolvableType type, MimeType mimeType, Object... hints) { + if (inputStream instanceof Mono) { + // single object + return Flux.from(inputStream).map(value -> serialize(value, allocator)); + } + else { + // array + Mono startArray = Mono.just(charBuffer('[', allocator)); + Flux arraySeparators = + Flux.create(sub -> sub.onNext(charBuffer(',', allocator))); + Mono endArray = Mono.just(charBuffer(']', allocator)); - Publisher stream = (inputStream instanceof Mono ? - ((Mono) inputStream).map(value -> serialize(value, allocator)) : - Flux.from(inputStream).map(value -> serialize(value, allocator))); - // TODO: figure out why using the parameter allocator for the postprocessor - // commits the response too early - DefaultDataBufferAllocator tempAllocator = new DefaultDataBufferAllocator(); + Flux serializedObjects = + Flux.from(inputStream).map(value -> serialize(value, allocator)); - return (this.postProcessor == null ? Flux.from(stream) : - this.postProcessor.encode(stream, tempAllocator, type, mimeType, hints)); + Flux array = Flux.zip(serializedObjects, arraySeparators) + .flatMap(tuple -> Flux.just(tuple.getT1(), tuple.getT2())); + + Flux arrayWithoutLastSeparator = Flux.from(array).skipLast(1); + + return Flux.concat(startArray, arrayWithoutLastSeparator, endArray); + } } private DataBuffer serialize(Object value, DataBufferAllocator allocator) { @@ -89,4 +94,11 @@ public class JacksonJsonEncoder extends AbstractEncoder { return buffer; } + private DataBuffer charBuffer(char ch, DataBufferAllocator allocator) { + DataBuffer buffer = allocator.allocateBuffer(1); + buffer.write((byte) ch); + return buffer; + } + + } diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectDecoder.java index 4c5a8c32be..42d204916a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectDecoder.java @@ -43,7 +43,6 @@ import org.springframework.util.MimeType; * Based on Netty JsonObjectDecoder * * @author Sebastien Deleuze - * @see JsonObjectEncoder */ public class JsonObjectDecoder extends AbstractDecoder { @@ -74,7 +73,6 @@ public class JsonObjectDecoder extends AbstractDecoder { /** - * @param allocator * @param maxObjectLength maximum number of bytes a JSON object/array may * use (including braces and all). Objects exceeding this length are dropped * and an {@link IllegalStateException} is thrown. diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectEncoder.java deleted file mode 100644 index 804856bb1e..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectEncoder.java +++ /dev/null @@ -1,140 +0,0 @@ -/* - * 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.support; - -import java.nio.charset.StandardCharsets; -import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; -import java.util.concurrent.atomic.AtomicLongFieldUpdater; - -import org.reactivestreams.Publisher; -import org.reactivestreams.Subscriber; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.core.subscriber.SubscriberBarrier; -import reactor.core.util.BackpressureUtils; - -import org.springframework.core.ResolvableType; -import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferAllocator; -import org.springframework.util.MimeType; - -/** - * Encode a byte stream of individual JSON element to a byte stream representing: - * - the same JSON object than the input stream if it is a {@link Mono} - * - a JSON array for other kinds of {@link Publisher} - * - * @author Sebastien Deleuze - * @author Stephane Maldini - * - * @see JsonObjectDecoder - */ -public class JsonObjectEncoder extends AbstractEncoder { - - public JsonObjectEncoder() { - super(new MimeType("application", "json", StandardCharsets.UTF_8), - new MimeType("application", "*+json", StandardCharsets.UTF_8)); - } - - @Override - public Flux encode(Publisher inputStream, - DataBufferAllocator allocator, - ResolvableType type, MimeType mimeType, Object... hints) { - if (inputStream instanceof Mono) { - return Flux.from(inputStream); - } - return Flux.from(inputStream) - .lift(s -> new JsonArrayEncoderBarrier(s, allocator)); - } - - private static class JsonArrayEncoderBarrier - extends SubscriberBarrier { - - @SuppressWarnings("rawtypes") - static final AtomicLongFieldUpdater REQUESTED = - AtomicLongFieldUpdater.newUpdater(JsonArrayEncoderBarrier.class, "requested"); - - static final AtomicIntegerFieldUpdater TERMINATED = - AtomicIntegerFieldUpdater.newUpdater(JsonArrayEncoderBarrier.class, "terminated"); - - private final DataBufferAllocator allocator; - - private DataBuffer prev = null; - - private long count = 0; - - private volatile long requested; - - private volatile int terminated; - - public JsonArrayEncoderBarrier(Subscriber subscriber, - DataBufferAllocator allocator) { - super(subscriber); - this.allocator = allocator; - } - - - @Override - protected void doRequest(long n) { - BackpressureUtils.getAndAdd(REQUESTED, this, n); - if(TERMINATED.compareAndSet(this, 1, 2)){ - drainLast(); - } - else { - super.doRequest(n); - } - } - - @Override - protected void doNext(DataBuffer next) { - this.count++; - - DataBuffer tmp = this.prev; - this.prev = next; - DataBuffer buffer = allocator.allocateBuffer(); - if (this.count == 1) { - buffer.write((byte) '['); - } - if (tmp != null) { - buffer.write(tmp); - } - if (this.count > 1) { - buffer.write((byte) ','); - } - - BackpressureUtils.getAndSub(REQUESTED, this, 1L); - subscriber.onNext(buffer); - } - - protected void drainLast(){ - if(BackpressureUtils.getAndSub(REQUESTED, this, 1L) > 0) { - DataBuffer buffer = allocator.allocateBuffer(); - buffer.write(this.prev); - buffer.write((byte) ']'); - subscriber.onNext(buffer); - super.doComplete(); - } - } - - @Override - protected void doComplete() { - if(TERMINATED.compareAndSet(this, 0, 1)) { - drainLast(); - } - } - } - -} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultHttpRequestBuilder.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultHttpRequestBuilder.java index c6105e2582..c6e19a414c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultHttpRequestBuilder.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultHttpRequestBuilder.java @@ -27,6 +27,7 @@ import java.util.stream.Collectors; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import org.springframework.core.ResolvableType; import org.springframework.core.codec.Encoder; @@ -55,7 +56,7 @@ public class DefaultHttpRequestBuilder implements HttpRequestBuilder { protected URI url; - protected Flux contentPublisher; + protected Publisher contentPublisher; protected List> messageEncoders; @@ -127,7 +128,7 @@ public class DefaultHttpRequestBuilder implements HttpRequestBuilder { } public DefaultHttpRequestBuilder content(Object content) { - this.contentPublisher = Flux.just(content); + this.contentPublisher = Mono.just(content); return this; } diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonEncoderTests.java index 53383547f4..0ea1c99f68 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonEncoderTests.java @@ -56,8 +56,8 @@ public class JacksonJsonEncoderTests extends AbstractAllocatingTestCase { }); TestSubscriber testSubscriber = new TestSubscriber<>(); testSubscriber.bindTo(output) - .assertValues("{\"foo\":\"foofoo\",\"bar\":\"barbar\"}", - "{\"foo\":\"foofoofoo\",\"bar\":\"barbarbar\"}"); + .assertValues("[", "{\"foo\":\"foofoo\",\"bar\":\"barbar\"}", ",", + "{\"foo\":\"foofoofoo\",\"bar\":\"barbarbar\"}", "]"); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JsonObjectEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JsonObjectEncoderTests.java deleted file mode 100644 index 0d4c9f1add..0000000000 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JsonObjectEncoderTests.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * 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.support; - -import java.nio.charset.StandardCharsets; - -import org.junit.Before; -import org.junit.Test; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.core.test.TestSubscriber; - -import org.springframework.core.io.buffer.DataBuffer; - -/** - * @author Sebastien Deleuze - */ -public class JsonObjectEncoderTests extends AbstractAllocatingTestCase { - - private JsonObjectEncoder encoder; - - @Before - public void createEncoder() { - encoder = new JsonObjectEncoder(); - } - - @Test - public void encodeSingleElementFlux() throws InterruptedException { - Flux source = - Flux.just(stringBuffer("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}")); - Flux output = - Flux.from(encoder.encode(source, allocator, null, null)).map(chunk -> { - byte[] b = new byte[chunk.readableByteCount()]; - chunk.read(b); - return new String(b, StandardCharsets.UTF_8); - }); - TestSubscriber testSubscriber = new TestSubscriber<>(); - testSubscriber.bindTo(output) - .assertValues("[", "{\"foo\": \"foofoo\", \"bar\": \"barbar\"}]"); - } - - @Test - public void encodeSingleElementMono() throws InterruptedException { - Mono source = - Mono.just(stringBuffer("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}")); - Flux output = - Flux.from(encoder.encode(source, allocator, null, null)).map(chunk -> { - byte[] b = new byte[chunk.readableByteCount()]; - chunk.read(b); - return new String(b, StandardCharsets.UTF_8); - }); - TestSubscriber testSubscriber = new TestSubscriber<>(); - testSubscriber.bindTo(output) - .assertValues("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}"); - } - - @Test - public void encodeTwoElementsFlux() throws InterruptedException { - Flux source = - Flux.just(stringBuffer("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}"), - stringBuffer("{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}")); - Flux output = - Flux.from(encoder.encode(source, allocator, null, null)).map(chunk -> { - byte[] b = new byte[chunk.readableByteCount()]; - chunk.read(b); - return new String(b, StandardCharsets.UTF_8); - }); - TestSubscriber testSubscriber = new TestSubscriber<>(); - testSubscriber.bindTo(output) - .assertValues("[", - "{\"foo\": \"foofoo\", \"bar\": \"barbar\"},", - "{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}]"); - } - - @Test - public void encodeThreeElementsFlux() throws InterruptedException { - Flux source = - Flux.just(stringBuffer("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}"), - stringBuffer("{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}"), - stringBuffer("{\"foo\": \"foofoofoofoo\", \"bar\": \"barbarbarbar\"}") - ); - Flux output = - Flux.from(encoder.encode(source, allocator, null, null)).map(chunk -> { - byte[] b = new byte[chunk.readableByteCount()]; - chunk.read(b); - return new String(b, StandardCharsets.UTF_8); - }); - TestSubscriber testSubscriber = new TestSubscriber<>(); - testSubscriber.bindTo(output) - .assertValues("[", - "{\"foo\": \"foofoo\", \"bar\": \"barbar\"},", - "{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"},", - "{\"foo\": \"foofoofoofoo\", \"bar\": \"barbarbarbar\"}]"); - } - -} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java index 2ecc53a10c..c49861f30a 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java @@ -40,7 +40,6 @@ import org.springframework.core.ResolvableType; import org.springframework.core.codec.Encoder; import org.springframework.core.codec.support.ByteBufferEncoder; import org.springframework.core.codec.support.JacksonJsonEncoder; -import org.springframework.core.codec.support.JsonObjectEncoder; import org.springframework.core.codec.support.StringEncoder; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.GenericConversionService; @@ -380,7 +379,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @Bean public ResponseBodyResultHandler responseBodyResultHandler() { List> encoders = Arrays.asList(new ByteBufferEncoder(), - new StringEncoder(), new JacksonJsonEncoder(new JsonObjectEncoder())); + new StringEncoder(), new JacksonJsonEncoder()); ResponseBodyResultHandler resultHandler = new ResponseBodyResultHandler(encoders, conversionService()); resultHandler.setOrder(1); return resultHandler; From d20b0003c61d0bcbab3301f5548bbd8ab74ec2a2 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Thu, 17 Mar 2016 15:40:16 +0100 Subject: [PATCH 264/498] Refactoring of Servlet 3.1 and Undertow support - Introduce abstract base class for Servlet 3.1 and Undertow support - Simplify Undertow support --- .../AbstractResponseBodyPublisher.java | 208 ++++++++ .../ServletAsyncContextSynchronizer.java | 27 +- .../reactive/ServletServerHttpRequest.java | 205 ++------ .../reactive/ServletServerHttpResponse.java | 170 ++++--- .../reactive/UndertowHttpHandlerAdapter.java | 452 ++++++------------ 5 files changed, 504 insertions(+), 558 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractResponseBodyPublisher.java diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractResponseBodyPublisher.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractResponseBodyPublisher.java new file mode 100644 index 0000000000..cdd3fffc16 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractResponseBodyPublisher.java @@ -0,0 +1,208 @@ +/* + * 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.http.server.reactive; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicLong; + +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import reactor.core.util.BackpressureUtils; + +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.util.Assert; + +/** + * Abstract base class for {@code Publisher} implementations that bridge between + * event-listener APIs and Reactive Streams. Specifically, base class for the Servlet 3.1 + * and Undertow support. + * + * @author Arjen Poutsma + * @see ServletServerHttpRequest + * @see UndertowHttpHandlerAdapter + */ +abstract class AbstractResponseBodyPublisher implements Publisher { + + private ResponseBodySubscription subscription; + + private volatile boolean stalled; + + @Override + public void subscribe(Subscriber subscriber) { + Objects.requireNonNull(subscriber); + Assert.state(this.subscription == null, "Only a single subscriber allowed"); + + this.subscription = new ResponseBodySubscription(subscriber); + subscriber.onSubscribe(this.subscription); + } + + /** + * Publishes the given signal to the subscriber. + * @param dataBuffer the signal to publish + * @see Subscriber#onNext(Object) + */ + protected final void publishOnNext(DataBuffer dataBuffer) { + Assert.state(this.subscription != null); + this.subscription.publishOnNext(dataBuffer); + } + + /** + * Publishes the given error to the subscriber. + * @param t the error to publish + * @see Subscriber#onError(Throwable) + */ + protected final void publishOnError(Throwable t) { + if (this.subscription != null) { + this.subscription.publishOnError(t); + } + } + + /** + * Publishes the complete signal to the subscriber. + * @see Subscriber#onComplete() + */ + protected final void publishOnComplete() { + if (this.subscription != null) { + this.subscription.publishOnComplete(); + } + } + + /** + * Returns true if the {@code Subscriber} associated with this {@code Publisher} has + * cancelled its {@code Subscription}. + * @return {@code true} if a subscriber has been registered and its subscription has + * been cancelled; {@code false} otherwise + * @see ResponseBodySubscription#isCancelled() + * @see Subscription#cancel() + */ + protected final boolean isSubscriptionCancelled() { + return (this.subscription != null && this.subscription.isCancelled()); + } + + /** + * Checks the subscription for demand, and marks this publisher as "stalled" if there + * is none. The next time the subscriber {@linkplain Subscription#request(long) + * requests} more events, the {@link #noLongerStalled()} method is called. + * @return {@code true} if there is demand; {@code false} otherwise + */ + protected final boolean checkSubscriptionForDemand() { + if (this.subscription == null || !this.subscription.hasDemand()) { + this.stalled = true; + return false; + } + else { + return true; + } + } + + /** + * Abstract template method called when this publisher is no longer "stalled". Used in + * sub-classes to resume reading from the request. + */ + protected abstract void noLongerStalled(); + + private final class ResponseBodySubscription implements Subscription { + + private final Subscriber subscriber; + + private final AtomicLong demand = new AtomicLong(); + + private boolean cancelled; + + public ResponseBodySubscription(Subscriber subscriber) { + Assert.notNull(subscriber, "'subscriber' must not be null"); + + this.subscriber = subscriber; + } + + @Override + public final void cancel() { + this.cancelled = true; + } + + /** + * Indicates whether this subscription has been cancelled. + * @see #cancel() + */ + protected final boolean isCancelled() { + return this.cancelled; + } + + @Override + public final void request(long n) { + if (!isCancelled() && BackpressureUtils.checkRequest(n, this.subscriber)) { + long demand = BackpressureUtils.addAndGet(this.demand, n); + + if (stalled && demand > 0) { + stalled = false; + noLongerStalled(); + } + } + } + + /** + * Indicates whether this subscription has demand. + * @see #request(long) + */ + protected final boolean hasDemand() { + return this.demand.get() > 0; + } + + /** + * Publishes the given signal to the subscriber wrapped by this subscription, if + * it has not been cancelled. If there is {@linkplain #hasDemand() no demand} for + * the signal, an exception will be thrown. + * @param dataBuffer the signal to publish + * @see Subscriber#onNext(Object) + */ + protected final void publishOnNext(DataBuffer dataBuffer) { + if (!isCancelled()) { + if (hasDemand()) { + BackpressureUtils.getAndSub(this.demand, 1L); + this.subscriber.onNext(dataBuffer); + } + else { + throw new IllegalStateException("No demand for: " + dataBuffer); + } + } + } + + /** + * Publishes the given error to the subscriber wrapped by this subscription, if it + * has not been cancelled. + * @param t the error to publish + * @see Subscriber#onError(Throwable) + */ + protected final void publishOnError(Throwable t) { + if (!isCancelled()) { + this.subscriber.onError(t); + } + } + + /** + * Publishes the complete signal to the subscriber wrapped by this subscription, + * if it has not been cancelled. + * @see Subscriber#onComplete() + */ + protected final void publishOnComplete() { + if (!isCancelled()) { + this.subscriber.onComplete(); + } + } + } +} diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletAsyncContextSynchronizer.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletAsyncContextSynchronizer.java index 283b598e9e..ce507c9729 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletAsyncContextSynchronizer.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletAsyncContextSynchronizer.java @@ -16,11 +16,8 @@ package org.springframework.http.server.reactive; -import java.io.IOException; import java.util.concurrent.atomic.AtomicInteger; import javax.servlet.AsyncContext; -import javax.servlet.ServletInputStream; -import javax.servlet.ServletOutputStream; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; @@ -56,32 +53,20 @@ final class ServletAsyncContextSynchronizer { this.asyncContext = asyncContext; } + /** + * Returns the request of this synchronizer. + */ public ServletRequest getRequest() { return this.asyncContext.getRequest(); } + /** + * Returns the response of this synchronizer. + */ public ServletResponse getResponse() { return this.asyncContext.getResponse(); } - /** - * Returns the input stream of this synchronizer. - * @return the input stream - * @throws IOException if an input or output exception occurred - */ - public ServletInputStream getInputStream() throws IOException { - return getRequest().getInputStream(); - } - - /** - * Returns the output stream of this synchronizer. - * @return the output stream - * @throws IOException if an input or output exception occurred - */ - public ServletOutputStream getOutputStream() throws IOException { - return getResponse().getOutputStream(); - } - /** * Completes the reading side of the asynchronous operation. When both this method and * {@link #writeComplete()} have been called, the {@code AsyncContext} will be diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java index 289d20b8c8..f615e0495e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java @@ -22,7 +22,6 @@ import java.net.URISyntaxException; import java.nio.charset.Charset; import java.util.Enumeration; import java.util.Map; -import java.util.concurrent.atomic.AtomicLong; import javax.servlet.ReadListener; import javax.servlet.ServletInputStream; import javax.servlet.http.Cookie; @@ -30,9 +29,6 @@ import javax.servlet.http.HttpServletRequest; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.reactivestreams.Publisher; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; import reactor.core.publisher.Flux; import org.springframework.core.io.buffer.DataBuffer; @@ -68,7 +64,6 @@ public class ServletServerHttpRequest extends AbstractServerHttpRequest { RequestBodyPublisher bodyPublisher = new RequestBodyPublisher(synchronizer, allocator, bufferSize); this.requestBodyPublisher = Flux.from(bodyPublisher); - this.request.getInputStream().setReadListener(bodyPublisher); } @@ -142,8 +137,10 @@ public class ServletServerHttpRequest extends AbstractServerHttpRequest { return this.requestBodyPublisher; } - private static class RequestBodyPublisher - implements ReadListener, Publisher { + private static class RequestBodyPublisher extends AbstractResponseBodyPublisher { + + private final RequestBodyReadListener readListener = + new RequestBodyReadListener(); private final ServletAsyncContextSynchronizer synchronizer; @@ -151,184 +148,78 @@ public class ServletServerHttpRequest extends AbstractServerHttpRequest { private final byte[] buffer; - private final DemandCounter demand = new DemandCounter(); - - private Subscriber subscriber; - - private boolean stalled; - - private boolean cancelled; - public RequestBodyPublisher(ServletAsyncContextSynchronizer synchronizer, - DataBufferAllocator allocator, int bufferSize) { + DataBufferAllocator allocator, int bufferSize) throws IOException { this.synchronizer = synchronizer; this.allocator = allocator; this.buffer = new byte[bufferSize]; + synchronizer.getRequest().getInputStream().setReadListener(readListener); } @Override - public void subscribe(Subscriber subscriber) { - if (subscriber == null) { - throw new NullPointerException(); + protected void noLongerStalled() { + try { + readListener.onDataAvailable(); } - else if (this.subscriber != null) { - subscriber.onError( - new IllegalStateException("Only one subscriber allowed")); - } - this.subscriber = subscriber; - this.subscriber.onSubscribe(new RequestBodySubscription()); - } - - @Override - public void onDataAvailable() throws IOException { - if (cancelled) { - return; - } - ServletInputStream input = this.synchronizer.getInputStream(); - logger.trace("onDataAvailable: " + input); - - while (true) { - logger.trace("Demand: " + this.demand); - - if (!demand.hasDemand()) { - stalled = true; - break; - } - - boolean ready = input.isReady(); - logger.trace( - "Input ready: " + ready + " finished: " + input.isFinished()); - - if (!ready) { - break; - } - - int read = input.read(buffer); - logger.trace("Input read:" + read); - - if (read == -1) { - break; - } - else if (read > 0) { - this.demand.decrement(); - - DataBuffer dataBuffer = allocator.allocateBuffer(read); - dataBuffer.write(this.buffer, 0, read); - - this.subscriber.onNext(dataBuffer); - - } + catch (IOException ex) { + readListener.onError(ex); } } - @Override - public void onAllDataRead() throws IOException { - if (cancelled) { - return; - } - logger.trace("All data read"); - this.synchronizer.readComplete(); - if (this.subscriber != null) { - this.subscriber.onComplete(); - } - } - - @Override - public void onError(Throwable t) { - if (cancelled) { - return; - } - logger.trace("RequestBodyPublisher Error", t); - this.synchronizer.readComplete(); - if (this.subscriber != null) { - this.subscriber.onError(t); - } - } - - private class RequestBodySubscription implements Subscription { + private class RequestBodyReadListener implements ReadListener { @Override - public void request(long n) { - if (cancelled) { + public void onDataAvailable() throws IOException { + if (isSubscriptionCancelled()) { return; } - logger.trace("Updating demand " + demand + " by " + n); + logger.trace("onDataAvailable"); + ServletInputStream input = synchronizer.getRequest().getInputStream(); - demand.increase(n); - - logger.trace("Stalled: " + stalled); - - if (stalled) { - stalled = false; - try { - onDataAvailable(); + while (true) { + if (!checkSubscriptionForDemand()) { + break; } - catch (IOException ex) { - onError(ex); + + boolean ready = input.isReady(); + logger.trace( + "Input ready: " + ready + " finished: " + input.isFinished()); + + if (!ready) { + break; + } + + int read = input.read(buffer); + logger.trace("Input read:" + read); + + if (read == -1) { + break; + } + else if (read > 0) { + DataBuffer dataBuffer = allocator.allocateBuffer(read); + dataBuffer.write(buffer, 0, read); + + publishOnNext(dataBuffer); } } } @Override - public void cancel() { - if (cancelled) { - return; - } - cancelled = true; + public void onAllDataRead() throws IOException { + logger.trace("All data read"); synchronizer.readComplete(); - demand.reset(); - } - } - /** - * Small utility class for keeping track of Reactive Streams demand. - */ - private static final class DemandCounter { - - private final AtomicLong demand = new AtomicLong(); - - /** - * Increases the demand by the given number - * @param n the positive number to increase demand by - * @return the increased demand - * @see Subscription#request(long) - */ - public long increase(long n) { - Assert.isTrue(n > 0, "'n' must be higher than 0"); - return demand - .updateAndGet(d -> d != Long.MAX_VALUE ? d + n : Long.MAX_VALUE); - } - - /** - * Decreases the demand by one. - * @return the decremented demand - */ - public long decrement() { - return demand - .updateAndGet(d -> d != Long.MAX_VALUE ? d - 1 : Long.MAX_VALUE); - } - - /** - * Indicates whether this counter has demand, i.e. whether it is higher than - * 0. - * @return {@code true} if this counter has demand; {@code false} otherwise - */ - public boolean hasDemand() { - return this.demand.get() > 0; - } - - /** - * Resets this counter to 0. - * @see Subscription#cancel() - */ - public void reset() { - this.demand.set(0); + publishOnComplete(); } @Override - public String toString() { - return demand.toString(); + public void onError(Throwable t) { + logger.trace("RequestBodyReadListener Error", t); + synchronizer.readComplete(); + + publishOnError(t); } } + } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java index 35cfbb00fe..237fe48994 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java @@ -32,6 +32,7 @@ import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import reactor.core.publisher.Mono; +import reactor.core.util.BackpressureUtils; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferAllocator; @@ -61,7 +62,6 @@ public class ServletServerHttpResponse extends AbstractServerHttpResponse { this.response = (HttpServletResponse) synchronizer.getResponse(); this.responseBodySubscriber = new ResponseBodySubscriber(synchronizer, bufferSize); - this.response.getOutputStream().setWriteListener(responseBodySubscriber); } public HttpServletResponse getServletResponse() { @@ -118,39 +118,46 @@ public class ServletServerHttpResponse extends AbstractServerHttpResponse { } } - private static class ResponseBodySubscriber - implements WriteListener, Subscriber { + private static class ResponseBodySubscriber implements Subscriber { + + private final ResponseBodyWriteListener writeListener = + new ResponseBodyWriteListener(); private final ServletAsyncContextSynchronizer synchronizer; private final int bufferSize; + private volatile DataBuffer dataBuffer; + + private volatile boolean completed = false; + private Subscription subscription; - private DataBuffer dataBuffer; - - private volatile boolean subscriberComplete = false; - public ResponseBodySubscriber(ServletAsyncContextSynchronizer synchronizer, - int bufferSize) { + int bufferSize) throws IOException { this.synchronizer = synchronizer; this.bufferSize = bufferSize; + synchronizer.getResponse().getOutputStream().setWriteListener(writeListener); } @Override public void onSubscribe(Subscription subscription) { - this.subscription = subscription; - this.subscription.request(1); + logger.trace("onSubscribe. Subscription: " + subscription); + if (BackpressureUtils.validate(this.subscription, subscription)) { + this.subscription = subscription; + this.subscription.request(1); + } } @Override public void onNext(DataBuffer dataBuffer) { - Assert.isNull(this.dataBuffer); + Assert.state(this.dataBuffer == null); + logger.trace("onNext. buffer: " + dataBuffer); this.dataBuffer = dataBuffer; try { - onWritePossible(); + this.writeListener.onWritePossible(); } catch (IOException e) { onError(e); @@ -158,66 +165,93 @@ public class ServletServerHttpResponse extends AbstractServerHttpResponse { } @Override - public void onComplete() { - logger.trace("onComplete. buffer: " + dataBuffer); - - this.subscriberComplete = true; - - if (dataBuffer == null) { - this.synchronizer.writeComplete(); - } - } - - @Override - public void onWritePossible() throws IOException { - ServletOutputStream output = this.synchronizer.getOutputStream(); - - boolean ready = output.isReady(); - logger.trace("onWritePossible. ready: " + ready + " buffer: " + dataBuffer); - - if (ready) { - if (this.dataBuffer != null) { - int toBeWritten = this.dataBuffer.readableByteCount(); - InputStream input = this.dataBuffer.asInputStream(); - int writeCount = write(input, output); - logger.trace("written: " + writeCount + " total: " + toBeWritten); - if (writeCount == toBeWritten) { - this.dataBuffer = null; - if (!this.subscriberComplete) { - this.subscription.request(1); - } - else { - this.synchronizer.writeComplete(); - } - } - } - else if (this.subscription != null) { - this.subscription.request(1); - } - } - } - - private int write(InputStream in, ServletOutputStream output) throws IOException { - int byteCount = 0; - byte[] buffer = new byte[bufferSize]; - int bytesRead = -1; - while (output.isReady() && (bytesRead = in.read(buffer)) != -1) { - output.write(buffer, 0, bytesRead); - byteCount += bytesRead; - } - return byteCount; - } - - @Override - public void onError(Throwable ex) { - if (this.subscription != null) { - this.subscription.cancel(); - } - logger.error("ResponseBodySubscriber error", ex); + public void onError(Throwable t) { + logger.error("onError", t); HttpServletResponse response = (HttpServletResponse) this.synchronizer.getResponse(); response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); this.synchronizer.complete(); + + } + + @Override + public void onComplete() { + logger.trace("onComplete. buffer: " + this.dataBuffer); + + this.completed = true; + + if (this.dataBuffer != null) { + try { + this.writeListener.onWritePossible(); + } + catch (IOException ex) { + onError(ex); + } + } + + if (this.dataBuffer == null) { + this.synchronizer.writeComplete(); + } + } + + private class ResponseBodyWriteListener implements WriteListener { + + @Override + public void onWritePossible() throws IOException { + logger.trace("onWritePossible"); + ServletOutputStream output = synchronizer.getResponse().getOutputStream(); + + boolean ready = output.isReady(); + logger.trace("ready: " + ready + " buffer: " + dataBuffer); + + if (ready) { + if (dataBuffer != null) { + + int total = dataBuffer.readableByteCount(); + int written = writeDataBuffer(); + + logger.trace("written: " + written + " total: " + total); + if (written == total) { + releaseBuffer(); + if (!completed) { + subscription.request(1); + } + else { + synchronizer.writeComplete(); + } + } + } + else if (subscription != null) { + subscription.request(1); + } + } + } + + private int writeDataBuffer() throws IOException { + InputStream input = dataBuffer.asInputStream(); + ServletOutputStream output = synchronizer.getResponse().getOutputStream(); + + int bytesWritten = 0; + byte[] buffer = new byte[bufferSize]; + int bytesRead = -1; + + while (output.isReady() && (bytesRead = input.read(buffer)) != -1) { + output.write(buffer, 0, bytesRead); + bytesWritten += bytesRead; + } + + return bytesWritten; + } + + private void releaseBuffer() { + // TODO: call PooledDataBuffer.release() when we it is introduced + dataBuffer = null; + } + + @Override + public void onError(Throwable ex) { + logger.error("ResponseBodyWriteListener error", ex); + } } } } \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java index 340ac750b0..d4d48b5a59 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java @@ -18,18 +18,11 @@ package org.springframework.http.server.reactive; import java.io.IOException; import java.nio.ByteBuffer; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLongFieldUpdater; import io.undertow.connector.PooledByteBuffer; import io.undertow.server.HttpServerExchange; -import io.undertow.util.SameThreadExecutor; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import org.xnio.ChannelListener; @@ -38,9 +31,7 @@ import org.xnio.IoUtils; import org.xnio.channels.StreamSinkChannel; import org.xnio.channels.StreamSourceChannel; import reactor.core.publisher.Mono; -import reactor.core.subscriber.BaseSubscriber; import reactor.core.util.BackpressureUtils; -import reactor.core.util.Exceptions; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferAllocator; @@ -75,14 +66,14 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle RequestBodyPublisher requestBody = new RequestBodyPublisher(exchange, allocator); ServerHttpRequest request = new UndertowServerHttpRequest(exchange, requestBody); - ResponseBodySubscriber responseBodySubscriber = new ResponseBodySubscriber(exchange); + ResponseBodySubscriber responseBodySubscriber = + new ResponseBodySubscriber(exchange); + ServerHttpResponse response = new UndertowServerHttpResponse(exchange, publisher -> Mono .from(subscriber -> publisher.subscribe(responseBodySubscriber)), allocator); - exchange.dispatch(); - this.delegate.handle(request, response).subscribe(new Subscriber() { @Override @@ -113,375 +104,212 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle }); } - private static class RequestBodyPublisher implements Publisher { + private static class RequestBodyPublisher extends AbstractResponseBodyPublisher { - private static final AtomicLongFieldUpdater DEMAND = - AtomicLongFieldUpdater.newUpdater(RequestBodySubscription.class, "demand"); + private static final Log logger = LogFactory.getLog(RequestBodyPublisher.class); + private final ChannelListener listener = + new RequestBodyListener(); - private final HttpServerExchange exchange; + private final StreamSourceChannel requestChannel; private final DataBufferAllocator allocator; - private Subscriber subscriber; + private final PooledByteBuffer pooledByteBuffer; public RequestBodyPublisher(HttpServerExchange exchange, DataBufferAllocator allocator) { - this.exchange = exchange; + this.requestChannel = exchange.getRequestChannel(); + this.requestChannel.getReadSetter().set(listener); + this.requestChannel.resumeReads(); + this.pooledByteBuffer = + exchange.getConnection().getByteBufferPool().allocate(); this.allocator = allocator; } - @Override - public void subscribe(Subscriber subscriber) { - if (subscriber == null) { - throw Exceptions.argumentIsNullException(); + private void close() { + if (this.pooledByteBuffer != null) { + IoUtils.safeClose(this.pooledByteBuffer); } - if (this.subscriber != null) { - subscriber.onError(new IllegalStateException("Only one subscriber allowed")); + if (this.requestChannel != null) { + IoUtils.safeClose(this.requestChannel); } - - this.subscriber = subscriber; - this.subscriber.onSubscribe(new RequestBodySubscription()); } + @Override + protected void noLongerStalled() { + listener.handleEvent(requestChannel); + } - private class RequestBodySubscription implements Subscription, Runnable, - ChannelListener { - - volatile long demand; - - private PooledByteBuffer pooledBuffer; - - private StreamSourceChannel channel; - - private boolean subscriptionClosed; - - private boolean draining; - - - @Override - public void request(long n) { - BackpressureUtils.checkRequest(n, subscriber); - if (this.subscriptionClosed) { - return; - } - BackpressureUtils.getAndAdd(DEMAND, this, n); - scheduleNextMessage(); - } - - private void scheduleNextMessage() { - exchange.dispatch(exchange.isInIoThread() ? SameThreadExecutor.INSTANCE : - exchange.getIoThread(), this); - } - - @Override - public void cancel() { - this.subscriptionClosed = true; - close(); - } - - private void close() { - if (this.pooledBuffer != null) { - IoUtils.safeClose(this.pooledBuffer); - this.pooledBuffer = null; - } - if (this.channel != null) { - IoUtils.safeClose(this.channel); - this.channel = null; - } - } - - @Override - public void run() { - if (this.subscriptionClosed || this.draining) { - return; - } - if (0 == BackpressureUtils.getAndSub(DEMAND, this, 1)) { - return; - } - - this.draining = true; - - if (this.channel == null) { - this.channel = exchange.getRequestChannel(); - - if (this.channel == null) { - if (exchange.isRequestComplete()) { - return; - } - else { - throw new IllegalStateException("Failed to acquire channel!"); - } - } - } - if (this.pooledBuffer == null) { - this.pooledBuffer = exchange.getConnection().getByteBufferPool().allocate(); - } - else { - this.pooledBuffer.getBuffer().clear(); - } - - try { - ByteBuffer buffer = this.pooledBuffer.getBuffer(); - int count; - do { - count = this.channel.read(buffer); - if (count == 0) { - this.channel.getReadSetter().set(this); - this.channel.resumeReads(); - } - else if (count == -1) { - if (buffer.position() > 0) { - doOnNext(buffer); - } - doOnComplete(); - } - else { - if (buffer.remaining() == 0) { - if (this.demand == 0) { - this.channel.suspendReads(); - } - doOnNext(buffer); - if (this.demand > 0) { - scheduleNextMessage(); - } - break; - } - } - } while (count > 0); - } - catch (IOException e) { - doOnError(e); - } - } - - private void doOnNext(ByteBuffer buffer) { - this.draining = false; - buffer.flip(); - DataBuffer dataBuffer = allocator.wrap(buffer); - subscriber.onNext(dataBuffer); - } - - private void doOnComplete() { - this.subscriptionClosed = true; - try { - subscriber.onComplete(); - } - finally { - close(); - } - } - - private void doOnError(Throwable t) { - this.subscriptionClosed = true; - try { - subscriber.onError(t); - } - finally { - close(); - } - } + private class RequestBodyListener + implements ChannelListener { @Override public void handleEvent(StreamSourceChannel channel) { - if (this.subscriptionClosed) { + if (isSubscriptionCancelled()) { return; } - + logger.trace("handleEvent"); + ByteBuffer byteBuffer = pooledByteBuffer.getBuffer(); try { - ByteBuffer buffer = this.pooledBuffer.getBuffer(); - int count; - do { - count = channel.read(buffer); - if (count == 0) { - return; + while (true) { + if (!checkSubscriptionForDemand()) { + break; } - else if (count == -1) { - if (buffer.position() > 0) { - doOnNext(buffer); - } - doOnComplete(); + int read = channel.read(byteBuffer); + logger.trace("Input read:" + read); + + if (read == -1) { + publishOnComplete(); + close(); + break; + } + else if (read == 0) { + // input not ready, wait until we are invoked again + break; } else { - if (buffer.remaining() == 0) { - if (this.demand == 0) { - channel.suspendReads(); - } - doOnNext(buffer); - if (this.demand > 0) { - scheduleNextMessage(); - } - break; - } + byteBuffer.flip(); + DataBuffer dataBuffer = allocator.wrap(byteBuffer); + publishOnNext(dataBuffer); } - } while (count > 0); + } } - catch (IOException e) { - doOnError(e); + catch (IOException ex) { + publishOnError(ex); } } } + } - private static class ResponseBodySubscriber - implements ChannelListener, BaseSubscriber{ + private static class ResponseBodySubscriber implements Subscriber { + + private static final Log logger = LogFactory.getLog(ResponseBodySubscriber.class); + + private final ChannelListener listener = + new ResponseBodyListener(); private final HttpServerExchange exchange; + private final StreamSinkChannel responseChannel; + + private volatile ByteBuffer byteBuffer; + + private volatile boolean completed = false; + private Subscription subscription; - private final Queue buffers = new ConcurrentLinkedQueue<>(); - - private final AtomicInteger writing = new AtomicInteger(); - - private final AtomicBoolean closing = new AtomicBoolean(); - - private StreamSinkChannel responseChannel; - - public ResponseBodySubscriber(HttpServerExchange exchange) { this.exchange = exchange; + this.responseChannel = exchange.getResponseChannel(); + this.responseChannel.getWriteSetter().set(listener); + this.responseChannel.resumeWrites(); } @Override public void onSubscribe(Subscription subscription) { - BaseSubscriber.super.onSubscribe(subscription); - this.subscription = subscription; - this.subscription.request(1); + logger.trace("onSubscribe. Subscription: " + subscription); + if (BackpressureUtils.validate(this.subscription, subscription)) { + this.subscription = subscription; + this.subscription.request(1); + } } @Override public void onNext(DataBuffer dataBuffer) { - BaseSubscriber.super.onNext(dataBuffer); + Assert.state(this.byteBuffer == null); + logger.trace("onNext. buffer: " + dataBuffer); - ByteBuffer buffer = dataBuffer.asByteBuffer(); - - if (this.responseChannel == null) { - this.responseChannel = exchange.getResponseChannel(); - } - - this.writing.incrementAndGet(); - try { - int c; - do { - c = this.responseChannel.write(buffer); - } while (buffer.hasRemaining() && c > 0); - - if (buffer.hasRemaining()) { - this.writing.incrementAndGet(); - enqueue(buffer); - this.responseChannel.getWriteSetter().set(this); - this.responseChannel.resumeWrites(); - } - else { - this.subscription.request(1); - } - - } - catch (IOException ex) { - onError(ex); - } - finally { - this.writing.decrementAndGet(); - if (this.closing.get()) { - closeIfDone(); - } - } - } - - private void enqueue(ByteBuffer src) { - do { - PooledByteBuffer buffer = exchange.getConnection().getByteBufferPool().allocate(); - ByteBuffer dst = buffer.getBuffer(); - copy(dst, src); - dst.flip(); - this.buffers.add(buffer); - } while (src.remaining() > 0); - } - - private void copy(ByteBuffer dst, ByteBuffer src) { - int n = Math.min(dst.capacity(), src.remaining()); - for (int i = 0; i < n; i++) { - dst.put(src.get()); - } + this.byteBuffer = dataBuffer.asByteBuffer(); } @Override - public void handleEvent(StreamSinkChannel channel) { - try { - int c; - do { - ByteBuffer buffer = this.buffers.peek().getBuffer(); - do { - c = channel.write(buffer); - } while (buffer.hasRemaining() && c > 0); - - if (!buffer.hasRemaining()) { - IoUtils.safeClose(this.buffers.remove()); - } - } while (!this.buffers.isEmpty() && c > 0); - - if (!this.buffers.isEmpty()) { - channel.resumeWrites(); - } - else { - this.writing.decrementAndGet(); - - if (this.closing.get()) { - closeIfDone(); - } - else { - this.subscription.request(1); - } - } - } - catch (IOException ex) { - onError(ex); - } - } - - @Override - public void onError(Throwable ex) { - BaseSubscriber.super.onError(ex); - logger.error("ResponseBodySubscriber error", ex); + public void onError(Throwable t) { + logger.error("onError", t); if (!exchange.isResponseStarted() && exchange.getStatusCode() < 500) { exchange.setStatusCode(500); } + closeChannel(responseChannel); } @Override public void onComplete() { - if (this.responseChannel != null) { - this.closing.set(true); - closeIfDone(); + logger.trace("onComplete. buffer: " + this.byteBuffer); + + this.completed = true; + + if (this.byteBuffer == null) { + closeChannel(responseChannel); } } - private void closeIfDone() { - if (this.writing.get() == 0) { - if (this.closing.compareAndSet(true, false)) { - closeChannel(); - } - } - } - - private void closeChannel() { + private void closeChannel(StreamSinkChannel channel) { try { - this.responseChannel.shutdownWrites(); + channel.shutdownWrites(); - if (!this.responseChannel.flush()) { - this.responseChannel.getWriteSetter().set(ChannelListeners - .flushingChannelListener( - o -> IoUtils.safeClose(this.responseChannel), + if (!channel.flush()) { + channel.getWriteSetter().set(ChannelListeners + .flushingChannelListener(o -> IoUtils.safeClose(channel), ChannelListeners.closingChannelExceptionHandler())); - this.responseChannel.resumeWrites(); + channel.resumeWrites(); } - this.responseChannel = null; } - catch (IOException ex) { - onError(ex); + catch (IOException ignored) { + logger.error(ignored, ignored); + } } + + private class ResponseBodyListener implements ChannelListener { + + @Override + public void handleEvent(StreamSinkChannel channel) { + if (byteBuffer != null) { + try { + int total = byteBuffer.remaining(); + int written = writeByteBuffer(channel); + + logger.trace("written: " + written + " total: " + total); + + if (written == total) { + releaseBuffer(); + if (!completed) { + subscription.request(1); + } + else { + closeChannel(channel); + } + } + } + catch (IOException ex) { + onError(ex); + } + } + else if (subscription != null) { + subscription.request(1); + } + + } + + private void releaseBuffer() { + byteBuffer = null; + + } + + private int writeByteBuffer(StreamSinkChannel channel) throws IOException { + int written; + int totalWritten = 0; + do { + written = channel.write(byteBuffer); + totalWritten += written; + } + while (byteBuffer.hasRemaining() && written > 0); + return totalWritten; + } + + } + } } From 35e511b03fd00c96f00f48eafcfd0ae16e888e16 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Wed, 23 Mar 2016 16:23:42 +0100 Subject: [PATCH 265/498] Resolved comments from review This commit resolves a few comments brought forward during a review meeting, specifically: - It renames AbstractResponseBodyPublisher to AbstractRequestBodyPublisher. - It separates out registration logic into a register method. - It moves the RequestBodyPublisher and ResponseBodySubscriber for the Servlet 3.1 support back into ServletHttpHandlerAdapter. --- ...java => AbstractRequestBodyPublisher.java} | 2 +- .../reactive/RxNettyServerHttpResponse.java | 8 +- .../reactive/ServletHttpHandlerAdapter.java | 256 +++++++++++++++++- .../reactive/ServletServerHttpRequest.java | 105 +------ .../reactive/ServletServerHttpResponse.java | 171 +----------- .../reactive/UndertowHttpHandlerAdapter.java | 22 +- .../reactive/UndertowServerHttpResponse.java | 8 +- .../RequestMappingIntegrationTests.java | 2 + 8 files changed, 292 insertions(+), 282 deletions(-) rename spring-web-reactive/src/main/java/org/springframework/http/server/reactive/{AbstractResponseBodyPublisher.java => AbstractRequestBodyPublisher.java} (98%) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractResponseBodyPublisher.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractRequestBodyPublisher.java similarity index 98% rename from spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractResponseBodyPublisher.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractRequestBodyPublisher.java index cdd3fffc16..2a29fec904 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractResponseBodyPublisher.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractRequestBodyPublisher.java @@ -36,7 +36,7 @@ import org.springframework.util.Assert; * @see ServletServerHttpRequest * @see UndertowHttpHandlerAdapter */ -abstract class AbstractResponseBodyPublisher implements Publisher { +abstract class AbstractRequestBodyPublisher implements Publisher { private ResponseBodySubscription subscription; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java index 2d310c93de..a45c2b1b17 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java @@ -95,12 +95,8 @@ public class RxNettyServerHttpResponse extends AbstractServerHttpResponse { if (!httpCookie.getMaxAge().isNegative()) { cookie.setMaxAge(httpCookie.getMaxAge().getSeconds()); } - if (httpCookie.getDomain().isPresent()) { - cookie.setDomain(httpCookie.getDomain().get()); - } - if (httpCookie.getPath().isPresent()) { - cookie.setPath(httpCookie.getPath().get()); - } + httpCookie.getDomain().ifPresent(cookie::setDomain); + httpCookie.getPath().ifPresent(cookie::setPath); cookie.setSecure(httpCookie.isSecure()); cookie.setHttpOnly(httpCookie.isHttpOnly()); this.response.addCookie(cookie); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java index 1182221df4..329949bee5 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java @@ -17,8 +17,13 @@ package org.springframework.http.server.reactive; import java.io.IOException; +import java.io.InputStream; import javax.servlet.AsyncContext; +import javax.servlet.ReadListener; import javax.servlet.ServletException; +import javax.servlet.ServletInputStream; +import javax.servlet.ServletOutputStream; +import javax.servlet.WriteListener; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; @@ -28,7 +33,10 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; +import reactor.core.publisher.Mono; +import reactor.core.util.BackpressureUtils; +import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferAllocator; import org.springframework.core.io.buffer.DefaultDataBufferAllocator; import org.springframework.http.HttpStatus; @@ -77,13 +85,19 @@ public class ServletHttpHandlerAdapter extends HttpServlet { AsyncContext context = servletRequest.startAsync(); ServletAsyncContextSynchronizer synchronizer = new ServletAsyncContextSynchronizer(context); + RequestBodyPublisher requestBody = + new RequestBodyPublisher(synchronizer, allocator, bufferSize); + requestBody.registerListener(); ServletServerHttpRequest request = - new ServletServerHttpRequest(synchronizer, this.allocator, - this.bufferSize); + new ServletServerHttpRequest(servletRequest, requestBody); + ResponseBodySubscriber responseBody = + new ResponseBodySubscriber(synchronizer, bufferSize); + responseBody.registerListener(); ServletServerHttpResponse response = - new ServletServerHttpResponse(synchronizer, this.bufferSize, - this.allocator); + new ServletServerHttpResponse(servletResponse, allocator, + publisher -> Mono + .from(subscriber -> publisher.subscribe(responseBody))); HandlerResultSubscriber resultSubscriber = new HandlerResultSubscriber(synchronizer); @@ -124,4 +138,238 @@ public class ServletHttpHandlerAdapter extends HttpServlet { this.synchronizer.complete(); } } + + private static class RequestBodyPublisher extends AbstractRequestBodyPublisher { + + private static final Log logger = LogFactory.getLog(RequestBodyPublisher.class); + + private final RequestBodyReadListener readListener = + new RequestBodyReadListener(); + + private final ServletAsyncContextSynchronizer synchronizer; + + private final DataBufferAllocator allocator; + + private final byte[] buffer; + + public RequestBodyPublisher(ServletAsyncContextSynchronizer synchronizer, + DataBufferAllocator allocator, int bufferSize) { + this.synchronizer = synchronizer; + this.allocator = allocator; + this.buffer = new byte[bufferSize]; + } + + public void registerListener() throws IOException { + this.synchronizer.getRequest().getInputStream().setReadListener(readListener); + } + + @Override + protected void noLongerStalled() { + try { + readListener.onDataAvailable(); + } + catch (IOException ex) { + readListener.onError(ex); + } + } + + private class RequestBodyReadListener implements ReadListener { + + @Override + public void onDataAvailable() throws IOException { + if (isSubscriptionCancelled()) { + return; + } + logger.trace("onDataAvailable"); + ServletInputStream input = synchronizer.getRequest().getInputStream(); + + while (true) { + if (!checkSubscriptionForDemand()) { + break; + } + + boolean ready = input.isReady(); + logger.trace( + "Input ready: " + ready + " finished: " + input.isFinished()); + + if (!ready) { + break; + } + + int read = input.read(buffer); + logger.trace("Input read:" + read); + + if (read == -1) { + break; + } + else if (read > 0) { + DataBuffer dataBuffer = allocator.allocateBuffer(read); + dataBuffer.write(buffer, 0, read); + + publishOnNext(dataBuffer); + } + } + } + + @Override + public void onAllDataRead() throws IOException { + logger.trace("All data read"); + synchronizer.readComplete(); + + publishOnComplete(); + } + + @Override + public void onError(Throwable t) { + logger.trace("RequestBodyReadListener Error", t); + synchronizer.readComplete(); + + publishOnError(t); + } + } + + } + + private static class ResponseBodySubscriber implements Subscriber { + + private static final Log logger = LogFactory.getLog(ResponseBodySubscriber.class); + + private final ResponseBodyWriteListener writeListener = + new ResponseBodyWriteListener(); + + private final ServletAsyncContextSynchronizer synchronizer; + + private final int bufferSize; + + private volatile DataBuffer dataBuffer; + + private volatile boolean completed = false; + + private Subscription subscription; + + public ResponseBodySubscriber(ServletAsyncContextSynchronizer synchronizer, + int bufferSize) { + this.synchronizer = synchronizer; + this.bufferSize = bufferSize; + } + + public void registerListener() throws IOException { + synchronizer.getResponse().getOutputStream().setWriteListener(writeListener); + } + + @Override + public void onSubscribe(Subscription subscription) { + logger.trace("onSubscribe. Subscription: " + subscription); + if (BackpressureUtils.validate(this.subscription, subscription)) { + this.subscription = subscription; + this.subscription.request(1); + } + } + + @Override + public void onNext(DataBuffer dataBuffer) { + Assert.state(this.dataBuffer == null); + + logger.trace("onNext. buffer: " + dataBuffer); + + this.dataBuffer = dataBuffer; + try { + this.writeListener.onWritePossible(); + } + catch (IOException e) { + onError(e); + } + } + + @Override + public void onError(Throwable t) { + logger.error("onError", t); + HttpServletResponse response = + (HttpServletResponse) this.synchronizer.getResponse(); + response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); + this.synchronizer.complete(); + + } + + @Override + public void onComplete() { + logger.trace("onComplete. buffer: " + this.dataBuffer); + + this.completed = true; + + if (this.dataBuffer != null) { + try { + this.writeListener.onWritePossible(); + } + catch (IOException ex) { + onError(ex); + } + } + + if (this.dataBuffer == null) { + this.synchronizer.writeComplete(); + } + } + + private class ResponseBodyWriteListener implements WriteListener { + + @Override + public void onWritePossible() throws IOException { + logger.trace("onWritePossible"); + ServletOutputStream output = synchronizer.getResponse().getOutputStream(); + + boolean ready = output.isReady(); + logger.trace("ready: " + ready + " buffer: " + dataBuffer); + + if (ready) { + if (dataBuffer != null) { + + int total = dataBuffer.readableByteCount(); + int written = writeDataBuffer(); + + logger.trace("written: " + written + " total: " + total); + if (written == total) { + releaseBuffer(); + if (!completed) { + subscription.request(1); + } + else { + synchronizer.writeComplete(); + } + } + } + else if (subscription != null) { + subscription.request(1); + } + } + } + + private int writeDataBuffer() throws IOException { + InputStream input = dataBuffer.asInputStream(); + ServletOutputStream output = synchronizer.getResponse().getOutputStream(); + + int bytesWritten = 0; + byte[] buffer = new byte[bufferSize]; + int bytesRead = -1; + + while (output.isReady() && (bytesRead = input.read(buffer)) != -1) { + output.write(buffer, 0, bytesRead); + bytesWritten += bytesRead; + } + + return bytesWritten; + } + + private void releaseBuffer() { + // TODO: call PooledDataBuffer.release() when we it is introduced + dataBuffer = null; + } + + @Override + public void onError(Throwable ex) { + logger.error("ResponseBodyWriteListener error", ex); + } + } + } + } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java index f615e0495e..952a93cc7c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java @@ -16,23 +16,20 @@ package org.springframework.http.server.reactive; -import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.Charset; import java.util.Enumeration; import java.util.Map; -import javax.servlet.ReadListener; -import javax.servlet.ServletInputStream; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferAllocator; import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -55,15 +52,12 @@ public class ServletServerHttpRequest extends AbstractServerHttpRequest { private final Flux requestBodyPublisher; - public ServletServerHttpRequest(ServletAsyncContextSynchronizer synchronizer, - DataBufferAllocator allocator, int bufferSize) throws IOException { - Assert.notNull(synchronizer, "'synchronizer' must not be null"); - Assert.notNull(allocator, "'allocator' must not be null"); - - this.request = (HttpServletRequest) synchronizer.getRequest(); - RequestBodyPublisher bodyPublisher = - new RequestBodyPublisher(synchronizer, allocator, bufferSize); - this.requestBodyPublisher = Flux.from(bodyPublisher); + public ServletServerHttpRequest(HttpServletRequest request, + Publisher body) { + Assert.notNull(request, "'request' must not be null."); + Assert.notNull(body, "'body' must not be null."); + this.request = request; + this.requestBodyPublisher = Flux.from(body); } @@ -137,89 +131,4 @@ public class ServletServerHttpRequest extends AbstractServerHttpRequest { return this.requestBodyPublisher; } - private static class RequestBodyPublisher extends AbstractResponseBodyPublisher { - - private final RequestBodyReadListener readListener = - new RequestBodyReadListener(); - - private final ServletAsyncContextSynchronizer synchronizer; - - private final DataBufferAllocator allocator; - - private final byte[] buffer; - - public RequestBodyPublisher(ServletAsyncContextSynchronizer synchronizer, - DataBufferAllocator allocator, int bufferSize) throws IOException { - this.synchronizer = synchronizer; - this.allocator = allocator; - this.buffer = new byte[bufferSize]; - synchronizer.getRequest().getInputStream().setReadListener(readListener); - } - - @Override - protected void noLongerStalled() { - try { - readListener.onDataAvailable(); - } - catch (IOException ex) { - readListener.onError(ex); - } - } - - private class RequestBodyReadListener implements ReadListener { - - @Override - public void onDataAvailable() throws IOException { - if (isSubscriptionCancelled()) { - return; - } - logger.trace("onDataAvailable"); - ServletInputStream input = synchronizer.getRequest().getInputStream(); - - while (true) { - if (!checkSubscriptionForDemand()) { - break; - } - - boolean ready = input.isReady(); - logger.trace( - "Input ready: " + ready + " finished: " + input.isFinished()); - - if (!ready) { - break; - } - - int read = input.read(buffer); - logger.trace("Input read:" + read); - - if (read == -1) { - break; - } - else if (read > 0) { - DataBuffer dataBuffer = allocator.allocateBuffer(read); - dataBuffer.write(buffer, 0, read); - - publishOnNext(dataBuffer); - } - } - } - - @Override - public void onAllDataRead() throws IOException { - logger.trace("All data read"); - synchronizer.readComplete(); - - publishOnComplete(); - } - - @Override - public void onError(Throwable t) { - logger.trace("RequestBodyReadListener Error", t); - synchronizer.readComplete(); - - publishOnError(t); - } - } - - } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java index 237fe48994..17e69de735 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java @@ -16,23 +16,17 @@ package org.springframework.http.server.reactive; -import java.io.IOException; -import java.io.InputStream; import java.nio.charset.Charset; import java.util.List; import java.util.Map; -import javax.servlet.ServletOutputStream; -import javax.servlet.WriteListener; +import java.util.function.Function; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletResponse; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.reactivestreams.Publisher; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; import reactor.core.publisher.Mono; -import reactor.core.util.BackpressureUtils; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferAllocator; @@ -52,16 +46,16 @@ public class ServletServerHttpResponse extends AbstractServerHttpResponse { private final HttpServletResponse response; - private final ResponseBodySubscriber responseBodySubscriber; + private final Function, Mono> responseBodyWriter; - public ServletServerHttpResponse(ServletAsyncContextSynchronizer synchronizer, - int bufferSize, DataBufferAllocator allocator) throws IOException { + public ServletServerHttpResponse(HttpServletResponse response, + DataBufferAllocator allocator, + Function, Mono> responseBodyWriter) { super(allocator); - Assert.notNull(synchronizer, "'synchronizer' must not be null"); - - this.response = (HttpServletResponse) synchronizer.getResponse(); - this.responseBodySubscriber = - new ResponseBodySubscriber(synchronizer, bufferSize); + Assert.notNull(response, "'response' must not be null"); + Assert.notNull(responseBodyWriter, "'responseBodyWriter' must not be null"); + this.response = response; + this.responseBodyWriter = responseBodyWriter; } public HttpServletResponse getServletResponse() { @@ -75,8 +69,7 @@ public class ServletServerHttpResponse extends AbstractServerHttpResponse { @Override protected Mono setBodyInternal(Publisher publisher) { - return Mono.from((Publisher) subscriber -> publisher - .subscribe(this.responseBodySubscriber)); + return this.responseBodyWriter.apply(publisher); } @Override @@ -105,12 +98,8 @@ public class ServletServerHttpResponse extends AbstractServerHttpResponse { if (!httpCookie.getMaxAge().isNegative()) { cookie.setMaxAge((int) httpCookie.getMaxAge().getSeconds()); } - if (httpCookie.getDomain().isPresent()) { - cookie.setDomain(httpCookie.getDomain().get()); - } - if (httpCookie.getPath().isPresent()) { - cookie.setPath(httpCookie.getPath().get()); - } + httpCookie.getDomain().ifPresent(cookie::setDomain); + httpCookie.getPath().ifPresent(cookie::setPath); cookie.setSecure(httpCookie.isSecure()); cookie.setHttpOnly(httpCookie.isHttpOnly()); this.response.addCookie(cookie); @@ -118,140 +107,4 @@ public class ServletServerHttpResponse extends AbstractServerHttpResponse { } } - private static class ResponseBodySubscriber implements Subscriber { - - private final ResponseBodyWriteListener writeListener = - new ResponseBodyWriteListener(); - - private final ServletAsyncContextSynchronizer synchronizer; - - private final int bufferSize; - - private volatile DataBuffer dataBuffer; - - private volatile boolean completed = false; - - private Subscription subscription; - - public ResponseBodySubscriber(ServletAsyncContextSynchronizer synchronizer, - int bufferSize) throws IOException { - this.synchronizer = synchronizer; - this.bufferSize = bufferSize; - synchronizer.getResponse().getOutputStream().setWriteListener(writeListener); - } - - @Override - public void onSubscribe(Subscription subscription) { - logger.trace("onSubscribe. Subscription: " + subscription); - if (BackpressureUtils.validate(this.subscription, subscription)) { - this.subscription = subscription; - this.subscription.request(1); - } - } - - @Override - public void onNext(DataBuffer dataBuffer) { - Assert.state(this.dataBuffer == null); - - logger.trace("onNext. buffer: " + dataBuffer); - - this.dataBuffer = dataBuffer; - try { - this.writeListener.onWritePossible(); - } - catch (IOException e) { - onError(e); - } - } - - @Override - public void onError(Throwable t) { - logger.error("onError", t); - HttpServletResponse response = - (HttpServletResponse) this.synchronizer.getResponse(); - response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); - this.synchronizer.complete(); - - } - - @Override - public void onComplete() { - logger.trace("onComplete. buffer: " + this.dataBuffer); - - this.completed = true; - - if (this.dataBuffer != null) { - try { - this.writeListener.onWritePossible(); - } - catch (IOException ex) { - onError(ex); - } - } - - if (this.dataBuffer == null) { - this.synchronizer.writeComplete(); - } - } - - private class ResponseBodyWriteListener implements WriteListener { - - @Override - public void onWritePossible() throws IOException { - logger.trace("onWritePossible"); - ServletOutputStream output = synchronizer.getResponse().getOutputStream(); - - boolean ready = output.isReady(); - logger.trace("ready: " + ready + " buffer: " + dataBuffer); - - if (ready) { - if (dataBuffer != null) { - - int total = dataBuffer.readableByteCount(); - int written = writeDataBuffer(); - - logger.trace("written: " + written + " total: " + total); - if (written == total) { - releaseBuffer(); - if (!completed) { - subscription.request(1); - } - else { - synchronizer.writeComplete(); - } - } - } - else if (subscription != null) { - subscription.request(1); - } - } - } - - private int writeDataBuffer() throws IOException { - InputStream input = dataBuffer.asInputStream(); - ServletOutputStream output = synchronizer.getResponse().getOutputStream(); - - int bytesWritten = 0; - byte[] buffer = new byte[bufferSize]; - int bytesRead = -1; - - while (output.isReady() && (bytesRead = input.read(buffer)) != -1) { - output.write(buffer, 0, bytesRead); - bytesWritten += bytesRead; - } - - return bytesWritten; - } - - private void releaseBuffer() { - // TODO: call PooledDataBuffer.release() when we it is introduced - dataBuffer = null; - } - - @Override - public void onError(Throwable ex) { - logger.error("ResponseBodyWriteListener error", ex); - } - } - } } \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java index d4d48b5a59..8acffe45bb 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java @@ -64,14 +64,13 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle public void handleRequest(HttpServerExchange exchange) throws Exception { RequestBodyPublisher requestBody = new RequestBodyPublisher(exchange, allocator); + requestBody.registerListener(); ServerHttpRequest request = new UndertowServerHttpRequest(exchange, requestBody); - ResponseBodySubscriber responseBodySubscriber = - new ResponseBodySubscriber(exchange); - + ResponseBodySubscriber responseBody = new ResponseBodySubscriber(exchange); + responseBody.registerListener(); ServerHttpResponse response = new UndertowServerHttpResponse(exchange, - publisher -> Mono - .from(subscriber -> publisher.subscribe(responseBodySubscriber)), + publisher -> Mono.from(subscriber -> publisher.subscribe(responseBody)), allocator); this.delegate.handle(request, response).subscribe(new Subscriber() { @@ -104,7 +103,7 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle }); } - private static class RequestBodyPublisher extends AbstractResponseBodyPublisher { + private static class RequestBodyPublisher extends AbstractRequestBodyPublisher { private static final Log logger = LogFactory.getLog(RequestBodyPublisher.class); @@ -120,13 +119,16 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle public RequestBodyPublisher(HttpServerExchange exchange, DataBufferAllocator allocator) { this.requestChannel = exchange.getRequestChannel(); - this.requestChannel.getReadSetter().set(listener); - this.requestChannel.resumeReads(); this.pooledByteBuffer = exchange.getConnection().getByteBufferPool().allocate(); this.allocator = allocator; } + public void registerListener() { + this.requestChannel.getReadSetter().set(listener); + this.requestChannel.resumeReads(); + } + private void close() { if (this.pooledByteBuffer != null) { IoUtils.safeClose(this.pooledByteBuffer); @@ -203,10 +205,14 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle public ResponseBodySubscriber(HttpServerExchange exchange) { this.exchange = exchange; this.responseChannel = exchange.getResponseChannel(); + } + + public void registerListener() { this.responseChannel.getWriteSetter().set(listener); this.responseChannel.resumeWrites(); } + @Override public void onSubscribe(Subscription subscription) { logger.trace("onSubscribe. Subscription: " + subscription); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java index a10806ca57..1d3d1598da 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java @@ -87,12 +87,8 @@ public class UndertowServerHttpResponse extends AbstractServerHttpResponse { if (!httpCookie.getMaxAge().isNegative()) { cookie.setMaxAge((int) httpCookie.getMaxAge().getSeconds()); } - if (httpCookie.getDomain().isPresent()) { - cookie.setDomain(httpCookie.getDomain().get()); - } - if (httpCookie.getPath().isPresent()) { - cookie.setPath(httpCookie.getPath().get()); - } + httpCookie.getDomain().ifPresent(cookie::setDomain); + httpCookie.getPath().ifPresent(cookie::setPath); cookie.setSecure(httpCookie.isSecure()); cookie.setHttpOnly(httpCookie.isHttpOnly()); this.exchange.getResponseCookies().putIfAbsent(name, cookie); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java index c49861f30a..1249e9aafa 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java @@ -25,6 +25,7 @@ import java.util.Collections; import java.util.List; import java.util.concurrent.CompletableFuture; +import org.junit.Ignore; import org.junit.Test; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -168,6 +169,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati } @Test + @Ignore public void streamResult() throws Exception { RestTemplate restTemplate = new RestTemplate(); From d9d76ecd193c086220af80be4957a31f51e307b1 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Thu, 24 Mar 2016 09:39:31 +0100 Subject: [PATCH 266/498] Polishing --- .../springframework/core/codec/support/JacksonJsonEncoder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonEncoder.java index 21293e72d3..01b2bcbc41 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonEncoder.java @@ -76,7 +76,7 @@ public class JacksonJsonEncoder extends AbstractEncoder { Flux array = Flux.zip(serializedObjects, arraySeparators) .flatMap(tuple -> Flux.just(tuple.getT1(), tuple.getT2())); - Flux arrayWithoutLastSeparator = Flux.from(array).skipLast(1); + Flux arrayWithoutLastSeparator = array.skipLast(1); return Flux.concat(startArray, arrayWithoutLastSeparator, endArray); } From 23f3fff367680397c1245b39d1c1311bf52ca2e5 Mon Sep 17 00:00:00 2001 From: Lukas Krecan Date: Tue, 29 Mar 2016 10:33:21 +0200 Subject: [PATCH 267/498] Replacing potentionally slow and infinite while loop by a latch --- .../server/reactive/XmlHandlerIntegrationTests.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/XmlHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/XmlHandlerIntegrationTests.java index b286a8273b..502b9f9176 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/XmlHandlerIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/XmlHandlerIntegrationTests.java @@ -19,6 +19,8 @@ package org.springframework.http.server.reactive; import java.io.InputStream; import java.io.OutputStream; import java.net.URI; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import javax.xml.bind.JAXBContext; import javax.xml.bind.Marshaller; import javax.xml.bind.Unmarshaller; @@ -69,9 +71,7 @@ public class XmlHandlerIntegrationTests extends AbstractHttpHandlerIntegrationTe ResponseEntity response = restTemplate.exchange(request, Person.class); assertEquals(janeDoe, response.getBody()); - while (!handler.requestComplete) { - Thread.sleep(100); - } + handler.requestComplete.await(10, TimeUnit.SECONDS); if (handler.requestError != null) { throw handler.requestError; } @@ -81,7 +81,7 @@ public class XmlHandlerIntegrationTests extends AbstractHttpHandlerIntegrationTe private static class XmlHandler implements HttpHandler { - private volatile boolean requestComplete = false; + private CountDownLatch requestComplete = new CountDownLatch(1); private Person requestPerson; @@ -108,7 +108,7 @@ public class XmlHandlerIntegrationTests extends AbstractHttpHandlerIntegrationTe requestError = ex; } finally { - requestComplete = true; + requestComplete.countDown(); } }; From 75d006d2f90f9f963813fcc361d61e33ea7a05cf Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Thu, 31 Mar 2016 11:31:02 +0200 Subject: [PATCH 268/498] Improve StringDecoder. --- .../support/AbstractRawByteStreamDecoder.java | 240 ------------------ .../core/codec/support/StringDecoder.java | 60 ++--- .../web/client/reactive/WebClient.java | 2 +- .../RequestMappingHandlerAdapter.java | 2 +- .../codec/support/StringDecoderTests.java | 22 +- 5 files changed, 40 insertions(+), 286 deletions(-) delete mode 100644 spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractRawByteStreamDecoder.java diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractRawByteStreamDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractRawByteStreamDecoder.java deleted file mode 100644 index 7fc77d8137..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractRawByteStreamDecoder.java +++ /dev/null @@ -1,240 +0,0 @@ -/* - * 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.support; - -import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; -import java.util.concurrent.atomic.AtomicLongFieldUpdater; - -import org.reactivestreams.Publisher; -import org.reactivestreams.Subscriber; -import reactor.core.publisher.Flux; -import reactor.core.subscriber.SubscriberBarrier; -import reactor.core.util.BackpressureUtils; - -import org.springframework.core.ResolvableType; -import org.springframework.core.codec.Decoder; -import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferAllocator; -import org.springframework.util.Assert; -import org.springframework.util.MimeType; - -/** - * Abstract {@link Decoder} that plugs a {@link SubscriberBarrier} into the {@code Flux} - * pipeline in order to apply splitting/aggregation operations on the stream of data. - * - * @author Brian Clozel - */ -public abstract class AbstractRawByteStreamDecoder extends AbstractDecoder { - - private final DataBufferAllocator allocator; - - public AbstractRawByteStreamDecoder(DataBufferAllocator allocator, - MimeType... supportedMimeTypes) { - super(supportedMimeTypes); - Assert.notNull(allocator, "'allocator' must not be null"); - - this.allocator = allocator; - } - - @Override - public Flux decode(Publisher inputStream, ResolvableType type, - MimeType mimeType, Object... hints) { - - return decodeInternal(Flux.from(inputStream).lift(bbs -> subscriberBarrier(bbs)), - type, mimeType, hints); - } - - /** - * Create a {@link SubscriberBarrier} instance that will be plugged into the Publisher pipeline - * - *

    Implementations should provide their own {@link SubscriberBarrier} or use one of the - * provided implementations by this class - */ - public abstract SubscriberBarrier subscriberBarrier( - Subscriber subscriber); - - public abstract Flux decodeInternal(Publisher inputStream, - ResolvableType type - , MimeType mimeType, Object... hints); - - - /** - * {@code SubscriberBarrier} implementation that buffers all received elements and emits a single - * {@code DataBuffer} once the incoming stream has been completed - */ - public static class ReduceSingleByteStreamBarrier - extends SubscriberBarrier { - - @SuppressWarnings("rawtypes") - static final AtomicLongFieldUpdater REQUESTED = - AtomicLongFieldUpdater.newUpdater(ReduceSingleByteStreamBarrier.class, "requested"); - - static final AtomicIntegerFieldUpdater TERMINATED = - AtomicIntegerFieldUpdater.newUpdater(ReduceSingleByteStreamBarrier.class, "terminated"); - - private volatile long requested; - - private volatile int terminated; - - private DataBuffer buffer; - - public ReduceSingleByteStreamBarrier(Subscriber subscriber, - DataBufferAllocator allocator) { - super(subscriber); - this.buffer = allocator.allocateBuffer(); - } - - @Override - protected void doRequest(long n) { - BackpressureUtils.getAndAdd(REQUESTED, this, n); - if (TERMINATED.compareAndSet(this, 1, 2)) { - drainLast(); - } - else { - super.doRequest(Long.MAX_VALUE); - } - } - - @Override - protected void doComplete() { - if (TERMINATED.compareAndSet(this, 0, 1)) { - drainLast(); - } - } - - /* - * TODO: when available, wrap buffers with a single buffer and avoid copying data for every method call. - */ - @Override - protected void doNext(DataBuffer dataBuffer) { - this.buffer.write(dataBuffer); - } - - protected void drainLast() { - if (BackpressureUtils.getAndSub(REQUESTED, this, 1L) > 0) { - subscriber.onNext(this.buffer); - super.doComplete(); - } - } - } - - /** - * {@code SubscriberBarrier} implementation that splits incoming elements - * using line return delimiters: {@code "\n"} and {@code "\r\n"} - */ - public static class SplitLinesByteStreamBarrier - extends SubscriberBarrier { - - @SuppressWarnings("rawtypes") - static final AtomicLongFieldUpdater REQUESTED = - AtomicLongFieldUpdater.newUpdater(SplitLinesByteStreamBarrier.class, "requested"); - - static final AtomicIntegerFieldUpdater TERMINATED = - AtomicIntegerFieldUpdater.newUpdater(SplitLinesByteStreamBarrier.class, "terminated"); - - private final DataBufferAllocator allocator; - - - private volatile long requested; - - private volatile int terminated; - - private DataBuffer buffer; - - public SplitLinesByteStreamBarrier(Subscriber subscriber, - DataBufferAllocator allocator) { - super(subscriber); - this.allocator = allocator; - this.buffer = allocator.allocateBuffer(); - } - - @Override - protected void doRequest(long n) { - BackpressureUtils.getAndAdd(REQUESTED, this, n); - if (TERMINATED.compareAndSet(this, 1, 2)) { - drainLast(); - } - else { - super.doRequest(n); - } - } - - @Override - protected void doComplete() { - if (TERMINATED.compareAndSet(this, 0, 1)) { - drainLast(); - } - } - - /* - * TODO: when available, wrap buffers with a single buffer and avoid copying data for every method call. - */ - @Override - protected void doNext(DataBuffer dataBuffer) { - this.buffer.write(dataBuffer); - - while (REQUESTED.get(this) > 0) { - int separatorIndex = findEndOfLine(this.buffer); - if (separatorIndex != -1) { - if (BackpressureUtils.getAndSub(REQUESTED, this, 1L) > 0) { - byte[] message = new byte[separatorIndex]; - this.buffer.read(message); - consumeSeparator(this.buffer); -// this.buffer = this.buffer.slice(); - DataBuffer buffer2 = allocator.allocateBuffer(message.length); - buffer2.write(message); - super.doNext(buffer2); - } - } - else { - super.doRequest(1); - } - } - } - - protected int findEndOfLine(DataBuffer buffer) { - - final int n = buffer.readableByteCount(); - for (int i = 0; i < n; i++) { - final byte b = buffer.get(i); - if (b == '\n') { - return i; - } - else if (b == '\r' && i < n - 1 && buffer.get(i + 1) == '\n') { - return i; - } - } - - return -1; - } - - protected void consumeSeparator(DataBuffer buffer) { - byte sep = buffer.read(); - if (sep == '\r') { - buffer.read(); - } - } - - protected void drainLast() { - if (BackpressureUtils.getAndSub(REQUESTED, this, 1L) > 0) { - subscriber.onNext(this.buffer); - super.doComplete(); - } - } - } - -} diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java index 7889e1593e..51fc665aa9 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java @@ -20,13 +20,10 @@ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import org.reactivestreams.Publisher; -import org.reactivestreams.Subscriber; import reactor.core.publisher.Flux; -import reactor.core.subscriber.SubscriberBarrier; import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferAllocator; import org.springframework.util.MimeType; /** @@ -40,15 +37,14 @@ import org.springframework.util.MimeType; * * @author Sebastien Deleuze * @author Brian Clozel + * @author Arjen Poutsma * @see StringEncoder */ -public class StringDecoder extends AbstractRawByteStreamDecoder { +public class StringDecoder extends AbstractDecoder { public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; - public final boolean reduceToSingleBuffer; - - private final DataBufferAllocator allocator; + private final boolean reduceToSingleBuffer; /** * Create a {@code StringDecoder} that decodes a bytes stream to a String stream @@ -56,8 +52,8 @@ public class StringDecoder extends AbstractRawByteStreamDecoder { *

    By default, this decoder will buffer bytes and * emit a single String as a result. */ - public StringDecoder(DataBufferAllocator allocator) { - this(allocator, true); + public StringDecoder() { + this(true); } /** @@ -66,45 +62,39 @@ public class StringDecoder extends AbstractRawByteStreamDecoder { * @param reduceToSingleBuffer whether this decoder should buffer all received items * and decode a single consolidated String or re-emit items as they are provided */ - public StringDecoder(DataBufferAllocator allocator, boolean reduceToSingleBuffer) { - super(allocator, new MimeType("text", "plain", DEFAULT_CHARSET)); + public StringDecoder(boolean reduceToSingleBuffer) { + super(new MimeType("text", "plain", DEFAULT_CHARSET)); this.reduceToSingleBuffer = reduceToSingleBuffer; - this.allocator = allocator; } @Override public boolean canDecode(ResolvableType type, MimeType mimeType, Object... hints) { - return super.canDecode(type, mimeType, hints) - && String.class.isAssignableFrom(type.getRawClass()); + return super.canDecode(type, mimeType, hints) && + String.class.equals(type.getRawClass()); } @Override - public SubscriberBarrier subscriberBarrier( - Subscriber subscriber) { - if (reduceToSingleBuffer) { - return new ReduceSingleByteStreamBarrier(subscriber, allocator); + public Flux decode(Publisher inputStream, ResolvableType type, + MimeType mimeType, Object... hints) { + Flux inputFlux = Flux.from(inputStream); + if (this.reduceToSingleBuffer) { + inputFlux = Flux.from(inputFlux.reduce(DataBuffer::write)); } - else { - return new SubscriberBarrier(subscriber); - } - - } - - @Override - public Flux decodeInternal(Publisher inputStream, - ResolvableType type, MimeType mimeType, Object... hints) { - Charset charset; - if (mimeType != null && mimeType.getCharSet() != null) { - charset = mimeType.getCharSet(); - } - else { - charset = DEFAULT_CHARSET; - } - return Flux.from(inputStream).map(content -> { + Charset charset = getCharset(mimeType); + return inputFlux.map(content -> { byte[] bytes = new byte[content.readableByteCount()]; content.read(bytes); return new String(bytes, charset); }); } + private Charset getCharset(MimeType mimeType) { + if (mimeType != null && mimeType.getCharSet() != null) { + return mimeType.getCharSet(); + } + else { + return DEFAULT_CHARSET; + } + } + } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebClient.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebClient.java index 317048314d..ff825bc4ba 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebClient.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebClient.java @@ -88,7 +88,7 @@ public final class WebClient { DataBufferAllocator allocator = new DefaultDataBufferAllocator(); this.messageEncoders = Arrays.asList(new ByteBufferEncoder(), new StringEncoder(), new JacksonJsonEncoder()); - this.messageDecoders = Arrays.asList(new ByteBufferDecoder(), new StringDecoder(allocator), + this.messageDecoders = Arrays.asList(new ByteBufferDecoder(), new StringDecoder(), new JacksonJsonDecoder(new JsonObjectDecoder())); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java index 55de2dd7f8..75d98f2a71 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java @@ -100,7 +100,7 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Initializin if (ObjectUtils.isEmpty(this.argumentResolvers)) { List> decoders = Arrays.asList(new ByteBufferDecoder(), - new StringDecoder(allocator), + new StringDecoder(), new JacksonJsonDecoder(new JsonObjectDecoder())); this.argumentResolvers.add(new RequestParamArgumentResolver()); diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringDecoderTests.java index 2a87fe86be..d8494312b9 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringDecoderTests.java @@ -18,9 +18,9 @@ package org.springframework.core.codec.support; import org.junit.Before; import org.junit.Test; +import reactor.core.converter.RxJava1SingleConverter; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import reactor.core.converter.RxJava1SingleConverter; import reactor.core.test.TestSubscriber; import rx.Single; @@ -40,7 +40,7 @@ public class StringDecoderTests extends AbstractAllocatingTestCase { @Before public void createEncoder() { - decoder = new StringDecoder(allocator); + decoder = new StringDecoder(); } @@ -53,29 +53,33 @@ public class StringDecoderTests extends AbstractAllocatingTestCase { @Test public void decode() throws InterruptedException { - Flux source = Flux.just(stringBuffer("foo"), stringBuffer("bar")); - Flux output = this.decoder.decode(source, ResolvableType.forClassWithGenerics(Flux.class, String.class), null); + Flux source = + Flux.just(stringBuffer("foo"), stringBuffer("bar"), stringBuffer("baz")); + Flux output = + this.decoder.decode(source, ResolvableType.forClass(String.class), null); TestSubscriber testSubscriber = new TestSubscriber<>(); - testSubscriber.bindTo(output).assertValues("foobar"); + testSubscriber.bindTo(output).assertValues("foobarbaz"); } @Test public void decodeDoNotBuffer() throws InterruptedException { - StringDecoder decoder = new StringDecoder(allocator, false); + StringDecoder decoder = new StringDecoder(false); Flux source = Flux.just(stringBuffer("foo"), stringBuffer("bar")); - Flux output = decoder.decode(source, ResolvableType.forClassWithGenerics(Flux.class, String.class), null); + Flux output = + decoder.decode(source, ResolvableType.forClass(String.class), null); TestSubscriber testSubscriber = new TestSubscriber<>(); testSubscriber.bindTo(output).assertValues("foo", "bar"); } @Test public void decodeMono() throws InterruptedException { - Flux source = Flux.just(stringBuffer("foo"), stringBuffer("bar")); + Flux source = + Flux.just(stringBuffer("foo"), stringBuffer("bar"), stringBuffer("baz")); Mono mono = Mono.from(this.decoder.decode(source, ResolvableType.forClassWithGenerics(Mono.class, String.class), MediaType.TEXT_PLAIN)); TestSubscriber testSubscriber = new TestSubscriber<>(); - testSubscriber.bindTo(mono).assertValues("foobar"); + testSubscriber.bindTo(mono).assertValues("foobarbaz"); } @Test From c4b9d94c339533fe4b42be8e4161f1433b1f7b81 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 31 Mar 2016 16:39:39 +0200 Subject: [PATCH 269/498] Use CharsetDecoder to decode a DataBuffer into a String. --- .../core/codec/support/StringDecoder.java | 13 ++++++++++--- .../core/codec/support/StringDecoderTests.java | 11 +++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java index 51fc665aa9..a72e4249fb 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java @@ -16,6 +16,7 @@ package org.springframework.core.codec.support; +import java.nio.CharBuffer; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; @@ -38,11 +39,13 @@ import org.springframework.util.MimeType; * @author Sebastien Deleuze * @author Brian Clozel * @author Arjen Poutsma + * @author Mark Paluch * @see StringEncoder */ public class StringDecoder extends AbstractDecoder { public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; + public static final String EMPTY = ""; private final boolean reduceToSingleBuffer; @@ -82,9 +85,13 @@ public class StringDecoder extends AbstractDecoder { } Charset charset = getCharset(mimeType); return inputFlux.map(content -> { - byte[] bytes = new byte[content.readableByteCount()]; - content.read(bytes); - return new String(bytes, charset); + // fast-path exit. + if(content.readableByteCount() == 0) { + return EMPTY; + } + + CharBuffer charBuffer = charset.decode(content.asByteBuffer()); + return charBuffer.toString(); }); } diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringDecoderTests.java index d8494312b9..4195dc4052 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringDecoderTests.java @@ -33,6 +33,7 @@ import static org.junit.Assert.*; /** * @author Sebastien Deleuze * @author Brian Clozel + * @author Mark Paluch */ public class StringDecoderTests extends AbstractAllocatingTestCase { @@ -92,4 +93,14 @@ public class StringDecoderTests extends AbstractAllocatingTestCase { assertEquals("foobar", result); } + @Test + public void decodeEmpty() throws InterruptedException { + Flux source = Flux.just(stringBuffer("")); + Single single = RxJava1SingleConverter.from(this.decoder.decode(source, + ResolvableType.forClassWithGenerics(Single.class, String.class), + MediaType.TEXT_PLAIN)); + String result = single.toBlocking().value(); + assertEquals("", result); + } + } From fd6fc30eead2f509b10c422c77a9f15b579f0fcf Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Thu, 31 Mar 2016 12:33:45 -0500 Subject: [PATCH 270/498] Add RequestPostProcessor Fixes gh-81 --- .../reactive/DefaultHttpRequestBuilder.java | 16 ++++++ .../client/reactive/RequestPostProcessor.java | 36 +++++++++++++ .../DefaultHttpRequestBuilderTests.java | 52 +++++++++++++++++++ 3 files changed, 104 insertions(+) create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/client/reactive/RequestPostProcessor.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/client/reactive/DefaultHttpRequestBuilderTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultHttpRequestBuilder.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultHttpRequestBuilder.java index c6e19a414c..05fb554f92 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultHttpRequestBuilder.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultHttpRequestBuilder.java @@ -38,6 +38,7 @@ import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.http.client.reactive.ClientHttpRequest; import org.springframework.http.client.reactive.ClientHttpRequestFactory; +import org.springframework.util.Assert; import org.springframework.web.client.RestClientException; /** @@ -137,6 +138,21 @@ public class DefaultHttpRequestBuilder implements HttpRequestBuilder { return this; } + /** + * Allows performing more complex operations with a strategy. For example, a + * {@link RequestPostProcessor} implementation might accept the arguments of + * username and password and set an HTTP Basic authentication header. + * + * @param postProcessor the {@link RequestPostProcessor} to use. Cannot be null. + * + * @return this instance for further modifications. + */ + public DefaultHttpRequestBuilder apply(RequestPostProcessor postProcessor) { + Assert.notNull(postProcessor, "`postProcessor` is required"); + postProcessor.postProcess(this); + return this; + } + public ClientHttpRequest build(ClientHttpRequestFactory factory) { ClientHttpRequest request = factory.createRequest(this.httpMethod, this.url, this.httpHeaders); request.getHeaders().putAll(this.httpHeaders); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/RequestPostProcessor.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/RequestPostProcessor.java new file mode 100644 index 0000000000..ac8ec9783e --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/RequestPostProcessor.java @@ -0,0 +1,36 @@ +/* + * 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.web.client.reactive; + +/** + * Allows post processing the {@link DefaultHttpRequestBuilder} for strategy for + * performing more complex operations. + * + * @author Rob Winch + * @see DefaultHttpRequestBuilder#apply(RequestPostProcessor) + */ +public interface RequestPostProcessor { + + /** + * Implementations can modify the {@link DefaultHttpRequestBuilder} passed + * in. + * + * @param toPostProcess + * the {@link DefaultHttpRequestBuilder} to be modified. + */ + void postProcess(DefaultHttpRequestBuilder toPostProcess); +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/client/reactive/DefaultHttpRequestBuilderTests.java b/spring-web-reactive/src/test/java/org/springframework/web/client/reactive/DefaultHttpRequestBuilderTests.java new file mode 100644 index 0000000000..112c62d6d7 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/client/reactive/DefaultHttpRequestBuilderTests.java @@ -0,0 +1,52 @@ +/* + * 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.web.client.reactive; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.http.HttpMethod; + +/** + * + * @author Rob Winch + * + */ +public class DefaultHttpRequestBuilderTests { + private DefaultHttpRequestBuilder builder; + + @Before + public void setup() { + builder = new DefaultHttpRequestBuilder(HttpMethod.GET, "https://example.com/foo"); + } + + @Test + public void apply() { + RequestPostProcessor postProcessor = mock(RequestPostProcessor.class); + + builder.apply(postProcessor); + + verify(postProcessor).postProcess(builder); + } + + @Test(expected = IllegalArgumentException.class) + public void applyNullPostProcessorThrowsIllegalArgumentException() { + builder.apply(null); + } +} From 9e79b344ca02c7dd04d59f5c7567e29947b79c22 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 4 Apr 2016 09:49:22 +0200 Subject: [PATCH 271/498] Implement review feedback. Remove empty string optimization. Simplify test to use only Mono/Flux/TestSubscriber instead of mixing with RxJava. --- .../core/codec/support/StringDecoder.java | 6 ------ .../core/codec/support/StringDecoderTests.java | 11 +++++------ 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java index a72e4249fb..56ac1d9f46 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java @@ -45,7 +45,6 @@ import org.springframework.util.MimeType; public class StringDecoder extends AbstractDecoder { public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; - public static final String EMPTY = ""; private final boolean reduceToSingleBuffer; @@ -85,11 +84,6 @@ public class StringDecoder extends AbstractDecoder { } Charset charset = getCharset(mimeType); return inputFlux.map(content -> { - // fast-path exit. - if(content.readableByteCount() == 0) { - return EMPTY; - } - CharBuffer charBuffer = charset.decode(content.asByteBuffer()); return charBuffer.toString(); }); diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringDecoderTests.java index 4195dc4052..0901d5bca0 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringDecoderTests.java @@ -95,12 +95,11 @@ public class StringDecoderTests extends AbstractAllocatingTestCase { @Test public void decodeEmpty() throws InterruptedException { - Flux source = Flux.just(stringBuffer("")); - Single single = RxJava1SingleConverter.from(this.decoder.decode(source, - ResolvableType.forClassWithGenerics(Single.class, String.class), - MediaType.TEXT_PLAIN)); - String result = single.toBlocking().value(); - assertEquals("", result); + Mono source = Mono.just(stringBuffer("")); + Flux output = + this.decoder.decode(source, ResolvableType.forClass(String.class), null); + TestSubscriber testSubscriber = new TestSubscriber<>(); + testSubscriber.bindTo(output).assertValues(""); } } From a8f27af5fbfc255199d6065d26c321aac70b3417 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Thu, 31 Mar 2016 11:52:03 +0200 Subject: [PATCH 272/498] Polishing. --- .../core/codec/support/ByteBufferEncoder.java | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferEncoder.java index 4e05e1d62b..3d006f2c99 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferEncoder.java @@ -47,12 +47,8 @@ public class ByteBufferEncoder extends AbstractEncoder { public Flux encode(Publisher inputStream, DataBufferAllocator allocator, ResolvableType type, MimeType mimeType, Object... hints) { - //noinspection unchecked - return Flux.from(inputStream).map(byteBuffer -> { - DataBuffer dataBuffer = allocator.allocateBuffer(byteBuffer.remaining()); - dataBuffer.write(byteBuffer); - return dataBuffer; - }); + + return Flux.from(inputStream).map(allocator::wrap); } } \ No newline at end of file From 75399814cdc99c26b0efaa044c2b6e596cf57c66 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Mon, 21 Mar 2016 14:02:52 +0100 Subject: [PATCH 273/498] Improve Jaxb2Decoder - Introcuces XmlEventDecoder which decodes from DataBuffer to javax.xml.stream.events.XMLEvent. It uses the Aalto async XML API if available, but falls back to a blocking default if not. - Refacors Jaxb2Decoder to use said XmlEventDecoder, and split the stream of events into separate substreams by using the JAXB annotation value, one stream for each part of the tree that can be unmarshaled to the given type. - Various improvements in the JAXB code. --- spring-web-reactive/build.gradle | 2 + .../core/codec/support/Jaxb2Decoder.java | 217 ++++++++++----- .../core/codec/support/Jaxb2Encoder.java | 53 ++-- .../codec/support/JaxbContextContainer.java | 56 ++++ .../support/ListBasedXMLEventReader.java | 151 +++++++++++ .../core/codec/support/XmlEventDecoder.java | 145 ++++++++++ .../RequestMappingHandlerAdapter.java | 3 +- .../core/codec/support/Jaxb2DecoderTests.java | 254 +++++++++++++++++- .../core/codec/support/Jaxb2EncoderTests.java | 43 ++- .../codec/support/XmlEventDecoderTests.java | 77 ++++++ .../codec/support/jaxb/XmlRootElement.java | 25 ++ .../support/jaxb/XmlRootElementWithName.java | 27 ++ .../XmlRootElementWithNameAndNamespace.java | 27 ++ .../core/codec/support/jaxb/XmlType.java | 25 ++ .../codec/support/jaxb/XmlTypeWithName.java | 27 ++ .../jaxb/XmlTypeWithNameAndNamespace.java | 27 ++ .../core/codec/support/jaxb/package-info.java | 18 ++ .../RequestMappingIntegrationTests.java | 56 +++- 18 files changed, 1105 insertions(+), 128 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/core/codec/support/JaxbContextContainer.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/core/codec/support/ListBasedXMLEventReader.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/core/codec/support/XmlEventDecoder.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/core/codec/support/XmlEventDecoderTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlRootElement.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlRootElementWithName.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlRootElementWithNameAndNamespace.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlType.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlTypeWithName.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlTypeWithNameAndNamespace.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/package-info.java diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index 3d7e230b9a..abfdf7fb4f 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -105,6 +105,7 @@ dependencies { optional "org.eclipse.jetty:jetty-server:${jettyVersion}" optional "org.eclipse.jetty:jetty-servlet:${jettyVersion}" optional("org.freemarker:freemarker:2.3.23") + optional("com.fasterxml:aalto-xml:1.0.0") provided "javax.servlet:javax.servlet-api:3.1.0" @@ -118,6 +119,7 @@ dependencies { } testCompile "org.hamcrest:hamcrest-all:1.3" testCompile "com.squareup.okhttp3:mockwebserver:3.0.1" + testCompile("xmlunit:xmlunit:1.6") // Needed to run Javadoc without error optional "org.apache.httpcomponents:httpclient:4.5.1" diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Decoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Decoder.java index ec1e260fe9..42287d10e7 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Decoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Decoder.java @@ -16,30 +16,28 @@ package org.springframework.core.codec.support; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import javax.xml.bind.JAXBContext; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; +import javax.xml.XMLConstants; import javax.xml.bind.JAXBElement; import javax.xml.bind.JAXBException; -import javax.xml.bind.UnmarshalException; import javax.xml.bind.Unmarshaller; import javax.xml.bind.annotation.XmlRootElement; -import javax.xml.transform.Source; -import javax.xml.transform.sax.SAXSource; -import javax.xml.transform.stream.StreamSource; +import javax.xml.bind.annotation.XmlSchema; +import javax.xml.bind.annotation.XmlType; +import javax.xml.namespace.QName; +import javax.xml.stream.XMLEventReader; +import javax.xml.stream.events.XMLEvent; import org.reactivestreams.Publisher; -import org.xml.sax.InputSource; -import org.xml.sax.SAXException; -import org.xml.sax.XMLReader; -import org.xml.sax.helpers.XMLReaderFactory; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import org.springframework.core.ResolvableType; import org.springframework.core.codec.CodecException; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.support.DataBufferUtils; -import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import org.springframework.util.MimeType; import org.springframework.util.MimeTypeUtils; @@ -47,87 +45,170 @@ import org.springframework.util.MimeTypeUtils; * Decode from a bytes stream of XML elements to a stream of {@code Object} (POJO). * * @author Sebastien Deleuze + * @author Arjen Poutsma * @see Jaxb2Encoder */ public class Jaxb2Decoder extends AbstractDecoder { - private final ConcurrentMap, JAXBContext> jaxbContexts = new ConcurrentHashMap<>(64); + /** + * The default value for JAXB annotations. + * @see XmlRootElement#name() + * @see XmlRootElement#namespace() + * @see XmlType#name() + * @see XmlType#namespace() + */ + private final static String JAXB_DEFAULT_ANNOTATION_VALUE = "##default"; + private final XmlEventDecoder xmlEventDecoder = new XmlEventDecoder(); + + private final JaxbContextContainer jaxbContexts = new JaxbContextContainer(); public Jaxb2Decoder() { super(MimeTypeUtils.APPLICATION_XML, MimeTypeUtils.TEXT_XML); } + @Override + public boolean canDecode(ResolvableType type, MimeType mimeType, Object... hints) { + if (super.canDecode(type, mimeType, hints)) { + Class outputClass = type.getRawClass(); + return outputClass.isAnnotationPresent(XmlRootElement.class) || + outputClass.isAnnotationPresent(XmlType.class); + } + else { + return false; + } + } @Override public Flux decode(Publisher inputStream, ResolvableType type, MimeType mimeType, Object... hints) { - Class outputClass = type.getRawClass(); - try { - Source source = processSource( - new StreamSource(DataBufferUtils.toInputStream(inputStream))); - Unmarshaller unmarshaller = createUnmarshaller(outputClass); - if (outputClass.isAnnotationPresent(XmlRootElement.class)) { - return Flux.just(unmarshaller.unmarshal(source)); - } - else { - JAXBElement jaxbElement = unmarshaller.unmarshal(source, outputClass); - return Flux.just(jaxbElement.getValue()); - } - } - catch (UnmarshalException ex) { - return Flux.error( - new CodecException("Could not unmarshal to [" + outputClass + "]: " + ex.getMessage(), ex)); - } - catch (JAXBException ex) { - return Flux.error(new CodecException("Could not instantiate JAXBContext: " + - ex.getMessage(), ex)); - } + Flux xmlEventFlux = + this.xmlEventDecoder.decode(inputStream, null, mimeType); + + QName typeName = toQName(outputClass); + Flux> splitEvents = split(xmlEventFlux, typeName); + + return splitEvents.map(events -> unmarshal(events, outputClass)); } - protected Source processSource(Source source) { - if (source instanceof StreamSource) { - StreamSource streamSource = (StreamSource) source; - InputSource inputSource = new InputSource(streamSource.getInputStream()); - try { - XMLReader xmlReader = XMLReaderFactory.createXMLReader(); - return new SAXSource(xmlReader, inputSource); - } - catch (SAXException ex) { - throw new CodecException("Error while processing the source", ex); - } + /** + * Returns the qualified name for the given class, according to the mapping rules + * in the JAXB specification. + */ + QName toQName(Class outputClass) { + String localPart; + String namespaceUri; + + if (outputClass.isAnnotationPresent(XmlRootElement.class)) { + XmlRootElement annotation = outputClass.getAnnotation(XmlRootElement.class); + localPart = annotation.name(); + namespaceUri = annotation.namespace(); + } + else if (outputClass.isAnnotationPresent(XmlType.class)) { + XmlType annotation = outputClass.getAnnotation(XmlType.class); + localPart = annotation.name(); + namespaceUri = annotation.namespace(); } else { - return source; + throw new IllegalArgumentException("Outputclass [" + outputClass + "] is " + + "neither annotated with @XmlRootElement nor @XmlType"); } + + if (JAXB_DEFAULT_ANNOTATION_VALUE.equals(localPart)) { + localPart = ClassUtils.getShortNameAsProperty(outputClass); + } + if (JAXB_DEFAULT_ANNOTATION_VALUE.equals(namespaceUri)) { + Package outputClassPackage = outputClass.getPackage(); + if (outputClassPackage != null && + outputClassPackage.isAnnotationPresent(XmlSchema.class)) { + XmlSchema annotation = outputClassPackage.getAnnotation(XmlSchema.class); + namespaceUri = annotation.namespace(); + } + else { + namespaceUri = XMLConstants.NULL_NS_URI; + } + } + return new QName(namespaceUri, localPart); } - protected final Unmarshaller createUnmarshaller(Class clazz) throws JAXBException { + /** + * Split a flux of {@link XMLEvent}s into a flux of XMLEvent lists, one list for each + * branch of the tree that starts with the given qualified name. + * That is, given the XMLEvents shown + * {@linkplain XmlEventDecoder here}, + * and the {@code desiredName} "{@code child}", this method + * returns a flux of two lists, each of which containing the events of a particular + * branch of the tree that starts with "{@code child}". + *
      + *
    1. The first list, dealing with the first branch of the tree + *
        + *
      1. {@link javax.xml.stream.events.StartElement} {@code child}
      2. + *
      3. {@link javax.xml.stream.events.Characters} {@code foo}
      4. + *
      5. {@link javax.xml.stream.events.EndElement} {@code child}
      6. + *
      + *
    2. The second list, dealing with the second branch of the tree + *
        + *
      1. {@link javax.xml.stream.events.StartElement} {@code child}
      2. + *
      3. {@link javax.xml.stream.events.Characters} {@code bar}
      4. + *
      5. {@link javax.xml.stream.events.EndElement} {@code child}
      6. + *
      + *
    3. + *
    + */ + Flux> split(Flux xmlEventFlux, QName desiredName) { + return xmlEventFlux + .flatMap(new Function>>() { + + private List events = null; + + private int elementDepth = 0; + + private int barrier = Integer.MAX_VALUE; + + @Override + public Publisher> apply(XMLEvent event) { + if (event.isStartElement()) { + if (this.barrier == Integer.MAX_VALUE) { + QName startElementName = event.asStartElement().getName(); + if (desiredName.equals(startElementName)) { + this.events = new ArrayList(); + this.barrier = this.elementDepth; + } + } + this.elementDepth++; + } + if (this.elementDepth > this.barrier) { + this.events.add(event); + } + if (event.isEndElement()) { + this.elementDepth--; + if (this.elementDepth == this.barrier) { + this.barrier = Integer.MAX_VALUE; + return Mono.just(this.events); + } + } + return Mono.empty(); + } + }); + } + + private Object unmarshal(List eventFlux, Class outputClass) { try { - JAXBContext jaxbContext = getJaxbContext(clazz); - return jaxbContext.createUnmarshaller(); + Unmarshaller unmarshaller = this.jaxbContexts.createUnmarshaller(outputClass); + XMLEventReader eventReader = new ListBasedXMLEventReader(eventFlux); + if (outputClass.isAnnotationPresent(XmlRootElement.class)) { + return unmarshaller.unmarshal(eventReader); + } + else { + JAXBElement jaxbElement = + unmarshaller.unmarshal(eventReader, outputClass); + return jaxbElement.getValue(); + } } catch (JAXBException ex) { - throw new CodecException("Could not create Unmarshaller for class " + - "[" + clazz + "]: " + ex.getMessage(), ex); + throw new CodecException(ex.getMessage(), ex); } } - protected final JAXBContext getJaxbContext(Class clazz) { - Assert.notNull(clazz, "'clazz' must not be null"); - JAXBContext jaxbContext = this.jaxbContexts.get(clazz); - if (jaxbContext == null) { - try { - jaxbContext = JAXBContext.newInstance(clazz); - this.jaxbContexts.putIfAbsent(clazz, jaxbContext); - } - catch (JAXBException ex) { - throw new CodecException("Could not instantiate JAXBContext for class " + - "[" + clazz + "]: " + ex.getMessage(), ex); - } - } - return jaxbContext; - } - } diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Encoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Encoder.java index 5c95b9a00f..d299722342 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Encoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Encoder.java @@ -18,12 +18,11 @@ package org.springframework.core.codec.support; import java.io.OutputStream; import java.nio.charset.StandardCharsets; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.MarshalException; import javax.xml.bind.Marshaller; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlType; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -32,7 +31,6 @@ import org.springframework.core.ResolvableType; import org.springframework.core.codec.CodecException; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferAllocator; -import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.MimeType; import org.springframework.util.MimeTypeUtils; @@ -41,27 +39,43 @@ import org.springframework.util.MimeTypeUtils; * Encode from an {@code Object} stream to a byte stream of XML elements. * * @author Sebastien Deleuze + * @author Arjen Poutsma * @see Jaxb2Decoder */ public class Jaxb2Encoder extends AbstractEncoder { - private final ConcurrentMap, JAXBContext> jaxbContexts = new ConcurrentHashMap<>(64); + private final JaxbContextContainer jaxbContexts = new JaxbContextContainer(); public Jaxb2Encoder() { super(MimeTypeUtils.APPLICATION_XML, MimeTypeUtils.TEXT_XML); } + @Override + public boolean canEncode(ResolvableType type, MimeType mimeType, Object... hints) { + if (super.canEncode(type, mimeType, hints)) { + Class outputClass = type.getRawClass(); + return outputClass.isAnnotationPresent(XmlRootElement.class) || + outputClass.isAnnotationPresent(XmlType.class); + } + else { + return false; + } + + } + @Override public Flux encode(Publisher inputStream, DataBufferAllocator allocator, ResolvableType type, MimeType mimeType, Object... hints) { - return Flux.from(inputStream).map(value -> { + return Flux.from(inputStream). + take(1). // only map 1 value to ensure valid XML output + map(value -> { try { DataBuffer buffer = allocator.allocateBuffer(1024); OutputStream outputStream = buffer.asOutputStream(); Class clazz = ClassUtils.getUserClass(value); - Marshaller marshaller = createMarshaller(clazz); + Marshaller marshaller = jaxbContexts.createMarshaller(clazz); marshaller.setProperty(Marshaller.JAXB_ENCODING, StandardCharsets.UTF_8.name()); marshaller.marshal(value, outputStream); return buffer; @@ -75,32 +89,7 @@ public class Jaxb2Encoder extends AbstractEncoder { }); } - protected final Marshaller createMarshaller(Class clazz) { - try { - JAXBContext jaxbContext = getJaxbContext(clazz); - return jaxbContext.createMarshaller(); - } - catch (JAXBException ex) { - throw new CodecException("Could not create Marshaller for class " + - "[" + clazz + "]: " + ex.getMessage(), ex); - } - } - protected final JAXBContext getJaxbContext(Class clazz) { - Assert.notNull(clazz, "'clazz' must not be null"); - JAXBContext jaxbContext = this.jaxbContexts.get(clazz); - if (jaxbContext == null) { - try { - jaxbContext = JAXBContext.newInstance(clazz); - this.jaxbContexts.putIfAbsent(clazz, jaxbContext); - } - catch (JAXBException ex) { - throw new CodecException("Could not instantiate JAXBContext for class " + - "[" + clazz + "]: " + ex.getMessage(), ex); - } - } - return jaxbContext; - } } diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JaxbContextContainer.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JaxbContextContainer.java new file mode 100644 index 0000000000..46d38ff5ae --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JaxbContextContainer.java @@ -0,0 +1,56 @@ +/* + * 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.support; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Marshaller; +import javax.xml.bind.Unmarshaller; + +import org.springframework.util.Assert; + +/** + * @author Arjen Poutsma + */ +final class JaxbContextContainer { + + private final ConcurrentMap, JAXBContext> jaxbContexts = + new ConcurrentHashMap<>(64); + + public Marshaller createMarshaller(Class clazz) throws JAXBException { + JAXBContext jaxbContext = getJaxbContext(clazz); + return jaxbContext.createMarshaller(); + } + + public Unmarshaller createUnmarshaller(Class clazz) throws JAXBException { + JAXBContext jaxbContext = getJaxbContext(clazz); + return jaxbContext.createUnmarshaller(); + } + + private JAXBContext getJaxbContext(Class clazz) throws JAXBException { + Assert.notNull(clazz, "'clazz' must not be null"); + JAXBContext jaxbContext = this.jaxbContexts.get(clazz); + if (jaxbContext == null) { + jaxbContext = JAXBContext.newInstance(clazz); + this.jaxbContexts.putIfAbsent(clazz, jaxbContext); + } + return jaxbContext; + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ListBasedXMLEventReader.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ListBasedXMLEventReader.java new file mode 100644 index 0000000000..95a22c9080 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ListBasedXMLEventReader.java @@ -0,0 +1,151 @@ +/* + * 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.support; + +import java.util.List; +import java.util.NoSuchElementException; +import javax.xml.stream.XMLEventReader; +import javax.xml.stream.XMLStreamConstants; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.events.Characters; +import javax.xml.stream.events.XMLEvent; + +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * TODO: move to org.springframework.util.xml when merging, hidden behind StaxUtils + * + * @author Arjen Poutsma + */ +class ListBasedXMLEventReader implements XMLEventReader { + + private final XMLEvent[] events; + + private int cursor = 0; + + public ListBasedXMLEventReader(List events) { + Assert.notNull(events, "'events' must not be null"); + this.events = events.toArray(new XMLEvent[events.size()]); + } + + @Override + public boolean hasNext() { + Assert.notNull(events, "'events' must not be null"); + return cursor != events.length; + } + + @Override + public XMLEvent nextEvent() { + if (cursor < events.length) { + return events[cursor++]; + } + else { + throw new NoSuchElementException(); + } + } + + @Override + public XMLEvent peek() { + if (cursor < events.length) { + return events[cursor]; + } + else { + return null; + } + } + + @Override + public Object next() { + return nextEvent(); + } + + /** + * Throws an {@code UnsupportedOperationException} when called. + * @throws UnsupportedOperationException when called + */ + @Override + public void remove() { + throw new UnsupportedOperationException( + "remove not supported on " + ClassUtils.getShortName(getClass())); + } + + @Override + public String getElementText() throws XMLStreamException { + if (!peek().isStartElement()) { + throw new XMLStreamException("Not at START_ELEMENT"); + } + + StringBuilder builder = new StringBuilder(); + while (true) { + XMLEvent event = nextEvent(); + if (event.isEndElement()) { + break; + } + else if (!event.isCharacters()) { + throw new XMLStreamException( + "Unexpected event [" + event + "] in getElementText()"); + } + Characters characters = event.asCharacters(); + if (!characters.isIgnorableWhiteSpace()) { + builder.append(event.asCharacters().getData()); + } + } + return builder.toString(); + } + + @Override + public XMLEvent nextTag() throws XMLStreamException { + while (true) { + XMLEvent event = nextEvent(); + switch (event.getEventType()) { + case XMLStreamConstants.START_ELEMENT: + case XMLStreamConstants.END_ELEMENT: + return event; + case XMLStreamConstants.END_DOCUMENT: + return null; + case XMLStreamConstants.SPACE: + case XMLStreamConstants.COMMENT: + case XMLStreamConstants.PROCESSING_INSTRUCTION: + continue; + case XMLStreamConstants.CDATA: + case XMLStreamConstants.CHARACTERS: + if (!event.asCharacters().isWhiteSpace()) { + throw new XMLStreamException( + "Non-ignorable whitespace CDATA or CHARACTERS event in nextTag()"); + } + break; + default: + throw new XMLStreamException("Received event [" + event + + "], instead of START_ELEMENT or END_ELEMENT."); + } + } + } + + /** + * Throws an {@code IllegalArgumentException} when called. + * @throws IllegalArgumentException when called. + */ + @Override + public Object getProperty(String name) throws IllegalArgumentException { + throw new IllegalArgumentException("Property not supported: [" + name + "]"); + } + + @Override + public void close() { + } +} diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/XmlEventDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/XmlEventDecoder.java new file mode 100644 index 0000000000..1a2a77ca60 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/XmlEventDecoder.java @@ -0,0 +1,145 @@ +/* + * 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.support; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; +import javax.xml.stream.XMLEventReader; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.events.XMLEvent; +import javax.xml.stream.util.XMLEventAllocator; + +import com.fasterxml.aalto.AsyncByteBufferFeeder; +import com.fasterxml.aalto.AsyncXMLInputFactory; +import com.fasterxml.aalto.AsyncXMLStreamReader; +import com.fasterxml.aalto.evt.EventAllocatorImpl; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.core.ResolvableType; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.support.DataBufferUtils; +import org.springframework.util.ClassUtils; +import org.springframework.util.MimeType; +import org.springframework.util.MimeTypeUtils; + +/** + * Decodes a {@link DataBuffer} stream into a stream of {@link XMLEvent}s. That is, given + * the following XML: + *
    {@code
    + * 
    + *     foo
    + *     bar
    + * }
    + * 
    + * this method with result in a flux with the following events: + *
      + *
    1. {@link javax.xml.stream.events.StartDocument}
    2. + *
    3. {@link javax.xml.stream.events.StartElement} {@code root}
    4. + *
    5. {@link javax.xml.stream.events.StartElement} {@code child}
    6. + *
    7. {@link javax.xml.stream.events.Characters} {@code foo}
    8. + *
    9. {@link javax.xml.stream.events.EndElement} {@code child}
    10. + *
    11. {@link javax.xml.stream.events.StartElement} {@code child}
    12. + *
    13. {@link javax.xml.stream.events.Characters} {@code bar}
    14. + *
    15. {@link javax.xml.stream.events.EndElement} {@code child}
    16. + *
    17. {@link javax.xml.stream.events.EndElement} {@code root}
    18. + *
    + * + * Note that this decoder is not registered by default, but used internally by other + * decoders who are. + * + * @author Arjen Poutsma + */ +public class XmlEventDecoder extends AbstractDecoder { + + private static final boolean aaltoPresent = ClassUtils + .isPresent("com.fasterxml.aalto.AsyncXMLStreamReader", + XmlEventDecoder.class.getClassLoader()); + + private static final XMLInputFactory inputFactory = XMLInputFactory.newFactory(); + + public XmlEventDecoder() { + super(MimeTypeUtils.APPLICATION_XML, MimeTypeUtils.TEXT_XML); + } + + @Override + public Flux decode(Publisher inputStream, ResolvableType type, + MimeType mimeType, Object... hints) { + if (aaltoPresent) { + return Flux.from(inputStream).flatMap(new AaltoDataBufferToXmlEvent()); + } + else { + try { + InputStream blockingStream = DataBufferUtils.toInputStream(inputStream); + + XMLEventReader eventReader = + inputFactory.createXMLEventReader(blockingStream); + + return Flux.fromIterable((Iterable) () -> eventReader); + } + catch (XMLStreamException ex) { + return Flux.error(ex); + } + } + } + + /* + * Separate static class to isolate Aalto dependency. + */ + private static class AaltoDataBufferToXmlEvent + implements Function> { + + private static final AsyncXMLInputFactory inputFactory = + (AsyncXMLInputFactory) XmlEventDecoder.inputFactory; + + private final AsyncXMLStreamReader streamReader = + inputFactory.createAsyncForByteBuffer(); + + private final XMLEventAllocator eventAllocator = + EventAllocatorImpl.getDefaultInstance(); + + @Override + public Publisher apply(DataBuffer dataBuffer) { + try { + streamReader.getInputFeeder().feedInput(dataBuffer.asByteBuffer()); + List events = new ArrayList<>(); + while (true) { + if (streamReader.next() == AsyncXMLStreamReader.EVENT_INCOMPLETE) { + // no more events with what currently has been fed to the reader + break; + } + else { + XMLEvent event = eventAllocator.allocate(streamReader); + events.add(event); + if (event.isEndDocument()) { + break; + } + } + } + return Flux.fromIterable(events); + } + catch (XMLStreamException ex) { + return Mono.error(ex); + } + + } + } +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java index 75d98f2a71..4f369a6577 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java @@ -31,6 +31,7 @@ import org.springframework.beans.factory.InitializingBean; import org.springframework.core.codec.Decoder; import org.springframework.core.codec.support.ByteBufferDecoder; import org.springframework.core.codec.support.JacksonJsonDecoder; +import org.springframework.core.codec.support.Jaxb2Decoder; import org.springframework.core.codec.support.JsonObjectDecoder; import org.springframework.core.codec.support.StringDecoder; import org.springframework.core.convert.ConversionService; @@ -100,7 +101,7 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Initializin if (ObjectUtils.isEmpty(this.argumentResolvers)) { List> decoders = Arrays.asList(new ByteBufferDecoder(), - new StringDecoder(), + new StringDecoder(), new Jaxb2Decoder(), new JacksonJsonDecoder(new JsonObjectDecoder())); this.argumentResolvers.add(new RequestParamArgumentResolver()); diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2DecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2DecoderTests.java index 3a5fe365ee..21aa3caf09 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2DecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2DecoderTests.java @@ -16,38 +16,270 @@ package org.springframework.core.codec.support; +import java.util.List; +import javax.xml.namespace.QName; +import javax.xml.stream.events.XMLEvent; + import org.junit.Test; import reactor.core.publisher.Flux; +import reactor.core.test.TestSubscriber; import org.springframework.core.ResolvableType; +import org.springframework.core.codec.support.jaxb.XmlRootElement; +import org.springframework.core.codec.support.jaxb.XmlRootElementWithName; +import org.springframework.core.codec.support.jaxb.XmlRootElementWithNameAndNamespace; +import org.springframework.core.codec.support.jaxb.XmlType; +import org.springframework.core.codec.support.jaxb.XmlTypeWithName; +import org.springframework.core.codec.support.jaxb.XmlTypeWithNameAndNamespace; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.MediaType; import static org.junit.Assert.*; -import reactor.core.test.TestSubscriber; /** * @author Sebastien Deleuze */ public class Jaxb2DecoderTests extends AbstractAllocatingTestCase { + private static final String POJO_ROOT = "" + + "" + + "foofoo" + + "barbar" + + ""; + + private static final String POJO_CHILD = + "" + + "" + + "" + + "foo" + + "bar" + + "" + + "" + + "foofoo" + + "barbar" + + "" + + ""; + private final Jaxb2Decoder decoder = new Jaxb2Decoder(); + private final XmlEventDecoder xmlEventDecoder = new XmlEventDecoder(); + + @Test public void canDecode() { - assertTrue(decoder.canDecode(null, MediaType.APPLICATION_XML)); - assertTrue(decoder.canDecode(null, MediaType.TEXT_XML)); - assertFalse(decoder.canDecode(null, MediaType.APPLICATION_JSON)); + assertTrue(decoder.canDecode(ResolvableType.forClass(Pojo.class), + MediaType.APPLICATION_XML)); + assertTrue(decoder.canDecode(ResolvableType.forClass(Pojo.class), + MediaType.TEXT_XML)); + assertFalse(decoder.canDecode(ResolvableType.forClass(Pojo.class), + MediaType.APPLICATION_JSON)); + + assertTrue(decoder.canDecode(ResolvableType.forClass(TypePojo.class), + MediaType.APPLICATION_XML)); + + assertFalse(decoder.canDecode(ResolvableType.forClass(getClass()), + MediaType.APPLICATION_XML)); } @Test - public void decode() { - Flux source = Flux.just(stringBuffer( - "barbarfoofoo")); - Flux output = decoder.decode(source, ResolvableType.forClass(Pojo.class), null); - TestSubscriber testSubscriber = new TestSubscriber<>(); - testSubscriber.bindTo(output) - .assertValues(new Pojo("foofoo", "barbar")); + public void splitOneBranches() { + Flux xmlEvents = + xmlEventDecoder.decode(Flux.just(stringBuffer(POJO_ROOT)), null, null); + Flux> result = decoder.split(xmlEvents, new QName("pojo")); + + TestSubscriber> resultSubscriber = new TestSubscriber<>(); + resultSubscriber.bindTo(result). + assertNoError(). + assertComplete(). + assertValuesWith(events -> { + assertEquals(8, events.size()); + assertStartElement(events.get(0), "pojo"); + assertStartElement(events.get(1), "foo"); + assertCharacters(events.get(2), "foofoo"); + assertEndElement(events.get(3), "foo"); + assertStartElement(events.get(4), "bar"); + assertCharacters(events.get(5), "barbar"); + assertEndElement(events.get(6), "bar"); + assertEndElement(events.get(7), "pojo"); + }); + + } + @Test + public void splitMultipleBranches() { + Flux xmlEvents = + xmlEventDecoder.decode(Flux.just(stringBuffer(POJO_CHILD)), null, null); + Flux> result = decoder.split(xmlEvents, new QName("pojo")); + + TestSubscriber> resultSubscriber = new TestSubscriber<>(); + resultSubscriber.bindTo(result). + assertNoError(). + assertComplete(). + assertValuesWith(events -> { + assertEquals(8, events.size()); + assertStartElement(events.get(0), "pojo"); + assertStartElement(events.get(1), "foo"); + assertCharacters(events.get(2), "foo"); + assertEndElement(events.get(3), "foo"); + assertStartElement(events.get(4), "bar"); + assertCharacters(events.get(5), "bar"); + assertEndElement(events.get(6), "bar"); + assertEndElement(events.get(7), "pojo"); + }, events -> { + assertEquals(8, events.size()); + assertStartElement(events.get(0), "pojo"); + assertStartElement(events.get(1), "foo"); + assertCharacters(events.get(2), "foofoo"); + assertEndElement(events.get(3), "foo"); + assertStartElement(events.get(4), "bar"); + assertCharacters(events.get(5), "barbar"); + assertEndElement(events.get(6), "bar"); + assertEndElement(events.get(7), "pojo"); + }); + } + + private static void assertStartElement(XMLEvent event, String expectedLocalName) { + assertTrue(event.isStartElement()); + assertEquals(expectedLocalName, event.asStartElement().getName().getLocalPart()); + } + + private static void assertEndElement(XMLEvent event, String expectedLocalName) { + assertTrue(event.isEndElement()); + assertEquals(expectedLocalName, event.asEndElement().getName().getLocalPart()); + } + + private static void assertCharacters(XMLEvent event, String expectedData) { + assertTrue(event.isCharacters()); + assertEquals(expectedData, event.asCharacters().getData()); + } + + @Test + public void decodeSingleXmlRootElement() throws Exception { + Flux source = Flux.just(stringBuffer(POJO_ROOT)); + Flux output = + decoder.decode(source, ResolvableType.forClass(Pojo.class), null); + + TestSubscriber testSubscriber = new TestSubscriber<>(); + + testSubscriber.bindTo(output). + assertNoError(). + assertComplete(). + assertValues(new Pojo("foofoo", "barbar") + + ); + } + + @Test + public void decodeSingleXmlTypeElement() throws Exception { + Flux source = Flux.just(stringBuffer(POJO_ROOT)); + Flux output = + decoder.decode(source, ResolvableType.forClass(TypePojo.class), null); + + TestSubscriber testSubscriber = new TestSubscriber<>(); + + testSubscriber.bindTo(output). + assertNoError(). + assertComplete(). + assertValues(new TypePojo("foofoo", "barbar") + + ); + } + + @Test + public void decodeMultipleXmlRootElement() throws Exception { + Flux source = Flux.just(stringBuffer(POJO_CHILD)); + Flux output = + decoder.decode(source, ResolvableType.forClass(Pojo.class), null); + + TestSubscriber testSubscriber = new TestSubscriber<>(); + + testSubscriber.bindTo(output). + assertNoError(). + assertComplete(). + assertValues(new Pojo("foo", "bar"), new Pojo("foofoo", "barbar") + + ); + } + + @Test + public void decodeMultipleXmlTypeElement() throws Exception { + Flux source = Flux.just(stringBuffer(POJO_CHILD)); + Flux output = + decoder.decode(source, ResolvableType.forClass(TypePojo.class), null); + + TestSubscriber testSubscriber = new TestSubscriber<>(); + + testSubscriber.bindTo(output). + assertNoError(). + assertComplete(). + assertValues(new TypePojo("foo", "bar"), new TypePojo("foofoo", "barbar") + + ); + } + + @Test + public void toExpectedQName() { + assertEquals(new QName("pojo"), decoder.toQName(Pojo.class)); + assertEquals(new QName("pojo"), decoder.toQName(TypePojo.class)); + + assertEquals(new QName("namespace", "name"), + decoder.toQName(XmlRootElementWithNameAndNamespace.class)); + assertEquals(new QName("namespace", "name"), + decoder.toQName(XmlRootElementWithName.class)); + assertEquals(new QName("namespace", "xmlRootElement"), + decoder.toQName(XmlRootElement.class)); + + assertEquals(new QName("namespace", "name"), + decoder.toQName(XmlTypeWithNameAndNamespace.class)); + assertEquals(new QName("namespace", "name"), + decoder.toQName(XmlTypeWithName.class)); + assertEquals(new QName("namespace", "xmlType"), decoder.toQName(XmlType.class)); + + } + + @javax.xml.bind.annotation.XmlType(name = "pojo") + public static class TypePojo { + + private String foo; + + private String bar; + + public TypePojo() { + } + + public TypePojo(String foo, String bar) { + this.foo = foo; + this.bar = bar; + } + + public String getFoo() { + return this.foo; + } + + public void setFoo(String foo) { + this.foo = foo; + } + + public String getBar() { + return this.bar; + } + + public void setBar(String bar) { + this.bar = bar; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o instanceof TypePojo) { + TypePojo other = (TypePojo) o; + return this.foo.equals(other.foo) && this.bar.equals(other.bar); + } + return false; + } + + } } diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2EncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2EncoderTests.java index 0f7a802962..21c37595e6 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2EncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2EncoderTests.java @@ -16,20 +16,27 @@ package org.springframework.core.codec.support; +import java.io.IOException; import java.nio.charset.StandardCharsets; import org.junit.Before; import org.junit.Test; +import org.xml.sax.SAXException; import reactor.core.publisher.Flux; import reactor.core.test.TestSubscriber; +import org.springframework.core.ResolvableType; +import org.springframework.core.io.buffer.support.DataBufferTestUtils; import org.springframework.http.MediaType; +import static org.custommonkey.xmlunit.XMLAssert.assertXMLEqual; +import static org.custommonkey.xmlunit.XMLAssert.fail; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; /** * @author Sebastien Deleuze + * @author Arjen Poutsma */ public class Jaxb2EncoderTests extends AbstractAllocatingTestCase { @@ -42,23 +49,37 @@ public class Jaxb2EncoderTests extends AbstractAllocatingTestCase { @Test public void canEncode() { - assertTrue(encoder.canEncode(null, MediaType.APPLICATION_XML)); - assertTrue(encoder.canEncode(null, MediaType.TEXT_XML)); - assertFalse(encoder.canEncode(null, MediaType.APPLICATION_JSON)); + assertTrue(encoder.canEncode(ResolvableType.forClass(Pojo.class), + MediaType.APPLICATION_XML)); + assertTrue(encoder.canEncode(ResolvableType.forClass(Pojo.class), + MediaType.TEXT_XML)); + assertFalse(encoder.canEncode(ResolvableType.forClass(Pojo.class), + MediaType.APPLICATION_JSON)); + + assertTrue(encoder.canEncode( + ResolvableType.forClass(Jaxb2DecoderTests.TypePojo.class), + MediaType.APPLICATION_XML)); + + assertFalse(encoder.canEncode(ResolvableType.forClass(getClass()), + MediaType.APPLICATION_XML)); } @Test public void encode() { Flux source = Flux.just(new Pojo("foofoo", "barbar"), new Pojo("foofoofoo", "barbarbar")); - Flux output = encoder.encode(source, allocator, null, null).map(chunk -> { - byte[] b = new byte[chunk.readableByteCount()]; - chunk.read(b); - return new String(b, StandardCharsets.UTF_8); - }); + Flux output = + encoder.encode(source, allocator, ResolvableType.forClass(Pojo.class), + MediaType.APPLICATION_XML).map(chunk -> DataBufferTestUtils + .dumpString(chunk, StandardCharsets.UTF_8)); TestSubscriber testSubscriber = new TestSubscriber<>(); - testSubscriber.bindTo(output) - .assertValues("barbarfoofoo", - "barbarbarfoofoofoo"); + testSubscriber.bindTo(output).assertValuesWith(s -> { + try { + assertXMLEqual("barbarfoofoo", s); + } + catch (SAXException | IOException e) { + fail(e.getMessage()); + } + }); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/XmlEventDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/XmlEventDecoderTests.java new file mode 100644 index 0000000000..6f41f48027 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/XmlEventDecoderTests.java @@ -0,0 +1,77 @@ +/* + * 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.support; + +import javax.xml.stream.events.XMLEvent; + +import org.junit.Test; +import reactor.core.publisher.Flux; +import reactor.core.test.TestSubscriber; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author Arjen Poutsma + */ +public class XmlEventDecoderTests extends AbstractAllocatingTestCase { + + private static final String XML = "" + + "" + + "foofoo" + + "barbar" + + ""; + + private XmlEventDecoder decoder = new XmlEventDecoder(); + + @Test + public void toXMLEvents() { + + Flux events = decoder.decode(Flux.just(stringBuffer(XML)), null, null); + + TestSubscriber testSubscriber = new TestSubscriber<>(); + testSubscriber.bindTo(events). + assertNoError(). + assertComplete(). + assertValuesWith(e -> assertTrue(e.isStartDocument()), + e -> assertStartElement(e, "pojo"), + e -> assertStartElement(e, "foo"), + e -> assertCharacters(e, "foofoo"), + e -> assertEndElement(e, "foo"), + e -> assertStartElement(e, "bar"), + e -> assertCharacters(e, "barbar"), + e -> assertEndElement(e, "bar"), + e -> assertEndElement(e, "pojo")); + } + + private static void assertStartElement(XMLEvent event, String expectedLocalName) { + assertTrue(event.isStartElement()); + assertEquals(expectedLocalName, event.asStartElement().getName().getLocalPart()); + } + + private static void assertEndElement(XMLEvent event, String expectedLocalName) { + assertTrue(event + " is no end element", event.isEndElement()); + assertEquals(expectedLocalName, event.asEndElement().getName().getLocalPart()); + } + + private static void assertCharacters(XMLEvent event, String expectedData) { + assertTrue(event.isCharacters()); + assertEquals(expectedData, event.asCharacters().getData()); + } + + +} \ No newline at end of file diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlRootElement.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlRootElement.java new file mode 100644 index 0000000000..92470a5876 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlRootElement.java @@ -0,0 +1,25 @@ +/* + * 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.support.jaxb; + +/** + * @author Arjen Poutsma + */ +@javax.xml.bind.annotation.XmlRootElement +public class XmlRootElement { + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlRootElementWithName.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlRootElementWithName.java new file mode 100644 index 0000000000..deb7929916 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlRootElementWithName.java @@ -0,0 +1,27 @@ +/* + * 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.support.jaxb; + +import javax.xml.bind.annotation.XmlRootElement; + +/** + * @author Arjen Poutsma + */ +@XmlRootElement(name = "name") +public class XmlRootElementWithName { + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlRootElementWithNameAndNamespace.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlRootElementWithNameAndNamespace.java new file mode 100644 index 0000000000..e4330da2bc --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlRootElementWithNameAndNamespace.java @@ -0,0 +1,27 @@ +/* + * 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.support.jaxb; + +import javax.xml.bind.annotation.XmlRootElement; + +/** + * @author Arjen Poutsma + */ +@XmlRootElement(name = "name", namespace = "namespace") +public class XmlRootElementWithNameAndNamespace { + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlType.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlType.java new file mode 100644 index 0000000000..49d158674c --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlType.java @@ -0,0 +1,25 @@ +/* + * 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.support.jaxb; + +/** + * @author Arjen Poutsma + */ +@javax.xml.bind.annotation.XmlType +public class XmlType { + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlTypeWithName.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlTypeWithName.java new file mode 100644 index 0000000000..f62be41835 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlTypeWithName.java @@ -0,0 +1,27 @@ +/* + * 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.support.jaxb; + +import javax.xml.bind.annotation.XmlType; + +/** + * @author Arjen Poutsma + */ +@XmlType(name = "name") +public class XmlTypeWithName { + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlTypeWithNameAndNamespace.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlTypeWithNameAndNamespace.java new file mode 100644 index 0000000000..4cf7cb6f6b --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlTypeWithNameAndNamespace.java @@ -0,0 +1,27 @@ +/* + * 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.support.jaxb; + +import javax.xml.bind.annotation.XmlType; + +/** + * @author Arjen Poutsma + */ +@XmlType(name = "name", namespace = "namespace") +public class XmlTypeWithNameAndNamespace { + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/package-info.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/package-info.java new file mode 100644 index 0000000000..0500a2aae5 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/package-info.java @@ -0,0 +1,18 @@ +/* + * 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. + */ + +@javax.xml.bind.annotation.XmlSchema(namespace = "namespace") +package org.springframework.core.codec.support.jaxb; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java index 1249e9aafa..6a4ae62956 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java @@ -24,6 +24,8 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.concurrent.CompletableFuture; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; import org.junit.Ignore; import org.junit.Test; @@ -114,7 +116,8 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati RestTemplate restTemplate = new RestTemplate(); URI url = new URI("http://localhost:" + port + "/raw"); - RequestEntity request = RequestEntity.get(url).build(); + RequestEntity request = + RequestEntity.get(url).accept(MediaType.APPLICATION_JSON).build(); Person person = restTemplate.exchange(request, Person.class).getBody(); assertEquals(new Person("Robert"), person); @@ -262,17 +265,32 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @Test public void publisherCreate() throws Exception { - create("http://localhost:" + this.port + "/publisher-create"); + createJson("http://localhost:" + this.port + "/publisher-create"); + } + + @Test + public void publisherCreateXml() throws Exception { + createXml("http://localhost:" + this.port + "/publisher-create"); } @Test public void fluxCreate() throws Exception { - create("http://localhost:" + this.port + "/flux-create"); + createJson("http://localhost:" + this.port + "/flux-create"); + } + + @Test + public void fluxCreateXml() throws Exception { + createXml("http://localhost:" + this.port + "/flux-create"); } @Test public void observableCreate() throws Exception { - create("http://localhost:" + this.port + "/observable-create"); + createJson("http://localhost:" + this.port + "/observable-create"); + } + + @Test + public void observableCreateXml() throws Exception { + createXml("http://localhost:" + this.port + "/observable-create"); } @Test @@ -337,7 +355,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati assertEquals("MARIE", results.get(1).getName()); } - private void create(String requestUrl) throws Exception { + private void createJson(String requestUrl) throws Exception { RestTemplate restTemplate = new RestTemplate(); URI url = new URI(requestUrl); RequestEntity> request = RequestEntity.post(url) @@ -349,6 +367,21 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati assertEquals(2, this.wac.getBean(TestRestController.class).persons.size()); } + private void createXml(String requestUrl) throws Exception { + RestTemplate restTemplate = new RestTemplate(); + URI url = new URI(requestUrl); + People people = new People(); + people.getPerson().add(new Person("Robert")); + people.getPerson().add(new Person("Marie")); + RequestEntity request = + RequestEntity.post(url).contentType(MediaType.APPLICATION_XML) + .body(people); + ResponseEntity response = restTemplate.exchange(request, Void.class); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals(2, this.wac.getBean(TestRestController.class).persons.size()); + } + @Configuration @SuppressWarnings("unused") @@ -609,6 +642,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati } + @XmlRootElement private static class Person { private String name; @@ -654,4 +688,16 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati } } + @XmlRootElement + private static class People { + + private List persons = new ArrayList<>(); + + @XmlElement + public List getPerson() { + return this.persons; + } + } + + } From 04fae44163c93dba78bd70e1094bf825ad1fd374 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Thu, 7 Apr 2016 15:10:49 +0200 Subject: [PATCH 274/498] Switch back to reactor-core SNAPSHOT versions Some important issues have been fixed in recent SNAPSHOT versions. See #78 and #70 --- spring-web-reactive/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index abfdf7fb4f..427767e6ca 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -28,7 +28,7 @@ repositories { ext { springVersion = '4.2.3.RELEASE' - reactorVersion = '2.5.0.M2' + reactorVersion = '2.5.0.BUILD-SNAPSHOT' reactorNettyVersion = '2.5.0.BUILD-SNAPSHOT' tomcatVersion = '8.0.28' jettyVersion = '9.3.5.v20151012' From 24a63f1d90ffd58553e805c9ee2f7edca42c3fab Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Thu, 7 Apr 2016 16:04:39 +0200 Subject: [PATCH 275/498] Upgrade RxNetty to 0.5.2-SNAPSHOT and Tomcat to 8.0.33 --- spring-web-reactive/build.gradle | 4 ++-- .../http/client/reactive/RxNettyClientHttpResponse.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index 427767e6ca..8078bbf5c2 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -30,7 +30,7 @@ ext { springVersion = '4.2.3.RELEASE' reactorVersion = '2.5.0.BUILD-SNAPSHOT' reactorNettyVersion = '2.5.0.BUILD-SNAPSHOT' - tomcatVersion = '8.0.28' + tomcatVersion = '8.0.33' jettyVersion = '9.3.5.v20151012' nettyVersion = '4.1.0.CR3' @@ -96,7 +96,7 @@ dependencies { optional "org.springframework:spring-context-support:${springVersion}" // for FreeMarker optional 'io.reactivex:rxjava:1.1.0' - optional "io.reactivex:rxnetty-http:0.5.0-SNAPSHOT" + optional "io.reactivex:rxnetty-http:0.5.2-SNAPSHOT" optional "com.fasterxml.jackson.core:jackson-databind:2.6.2" optional "io.projectreactor:reactor-netty:${reactorNettyVersion}" optional "org.apache.tomcat:tomcat-util:${tomcatVersion}" diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpResponse.java index 5edd02ef9e..9077bdc0f5 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpResponse.java @@ -56,7 +56,7 @@ public class RxNettyClientHttpResponse implements ClientHttpResponse { this.allocator = allocator; this.response = response; this.headers = new HttpHeaders(); - this.response.headerIterator().forEachRemaining(e -> this.headers.set(e.getKey(), e.getValue())); + this.response.headerIterator().forEachRemaining(e -> this.headers.set(e.getKey().toString(), e.getValue().toString())); this.cookies = initCookies(response); } From e2a07659ab3bcef618661c94583a4596765cf1a6 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Mon, 4 Apr 2016 22:20:04 +0200 Subject: [PATCH 276/498] Add generics support for WebResponseExtractors This commit adds generics support for WebResponseExtractors. Types should be described using a ResolvableType, which can be created with static methods like: // for the type Event ResolvableType.forClassWithGenerics(Event.class, Registration.class) Fixes #89 --- .../reactive/WebResponseExtractors.java | 62 ++++++++++++++----- 1 file changed, 47 insertions(+), 15 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebResponseExtractors.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebResponseExtractors.java index ac4331b6cb..2d3d7aeda8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebResponseExtractors.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebResponseExtractors.java @@ -47,33 +47,47 @@ public class WebResponseExtractors { /** * Extract the response body and decode it, returning it as a {@code Mono} + * @see ResolvableType#forClassWithGenerics(Class, Class[]) */ - public static WebResponseExtractor> body(Class sourceClass) { - - ResolvableType resolvableType = ResolvableType.forClass(sourceClass); + public static WebResponseExtractor> body(ResolvableType bodyType) { //noinspection unchecked return webResponse -> (Mono) webResponse.getClientResponse() - .flatMap(resp -> decodeResponseBody(resp, resolvableType, webResponse.getMessageDecoders())) + .flatMap(resp -> decodeResponseBody(resp, bodyType, webResponse.getMessageDecoders())) .next(); } + /** + * Extract the response body and decode it, returning it as a {@code Mono} + */ + public static WebResponseExtractor> body(Class sourceClass) { + ResolvableType bodyType = ResolvableType.forClass(sourceClass); + return body(bodyType); + } + + + /** + * Extract the response body and decode it, returning it as a {@code Flux} + * @see ResolvableType#forClassWithGenerics(Class, Class[]) + */ + public static WebResponseExtractor> bodyStream(ResolvableType bodyType) { + return webResponse -> webResponse.getClientResponse() + .flatMap(resp -> decodeResponseBody(resp, bodyType, webResponse.getMessageDecoders())); + } + /** * Extract the response body and decode it, returning it as a {@code Flux} */ public static WebResponseExtractor> bodyStream(Class sourceClass) { - - ResolvableType resolvableType = ResolvableType.forClass(sourceClass); - return webResponse -> webResponse.getClientResponse() - .flatMap(resp -> decodeResponseBody(resp, resolvableType, webResponse.getMessageDecoders())); + ResolvableType bodyType = ResolvableType.forClass(sourceClass); + return bodyStream(bodyType); } /** * Extract the full response body as a {@code ResponseEntity} * with its body decoded as a single type {@code T} + * @see ResolvableType#forClassWithGenerics(Class, Class[]) */ - public static WebResponseExtractor>> response(Class bodyClass) { - - ResolvableType bodyType = ResolvableType.forClass(bodyClass); + public static WebResponseExtractor>> response(ResolvableType bodyType) { return webResponse -> webResponse.getClientResponse() .then(response -> { List> decoders = webResponse.getMessageDecoders(); @@ -89,16 +103,34 @@ public class WebResponseExtractors { }); } + /** + * Extract the full response body as a {@code ResponseEntity} + * with its body decoded as a single type {@code T} + */ + public static WebResponseExtractor>> response(Class bodyClass) { + ResolvableType bodyType = ResolvableType.forClass(bodyClass); + return response(bodyType); + } + + /** + * Extract the full response body as a {@code ResponseEntity} + * with its body decoded as a {@code Flux} + * @see ResolvableType#forClassWithGenerics(Class, Class[]) + */ + public static WebResponseExtractor>>> responseStream(ResolvableType type) { + return webResponse -> webResponse.getClientResponse() + .map(response -> new ResponseEntity<>( + decodeResponseBody(response, type, webResponse.getMessageDecoders()), + response.getHeaders(), response.getStatusCode())); + } + /** * Extract the full response body as a {@code ResponseEntity} * with its body decoded as a {@code Flux} */ public static WebResponseExtractor>>> responseStream(Class sourceClass) { ResolvableType resolvableType = ResolvableType.forClass(sourceClass); - return webResponse -> webResponse.getClientResponse() - .map(response -> new ResponseEntity<>( - decodeResponseBody(response, resolvableType, webResponse.getMessageDecoders()), - response.getHeaders(), response.getStatusCode())); + return responseStream(resolvableType); } /** From 8ed2925ce3cac5ebf55651a1790dd6fe208d5b6d Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Mon, 4 Apr 2016 22:34:04 +0200 Subject: [PATCH 277/498] Propagate Errors in DefaultHttpRequestBuilder This commit introduces the new `WebClientException` for wrapping client-side exceptions. This exception is now thrown when now message encoder can be found for writing the request body as an HTTP request message. Fixes #83 --- .../reactive/DefaultHttpRequestBuilder.java | 5 ++- .../client/reactive/WebClientException.java | 36 +++++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebClientException.java diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultHttpRequestBuilder.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultHttpRequestBuilder.java index 05fb554f92..dde80dd455 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultHttpRequestBuilder.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultHttpRequestBuilder.java @@ -170,10 +170,9 @@ public class DefaultHttpRequestBuilder implements HttpRequestBuilder { mediaType)); } else { - // TODO: wrap with client exception? - request.setBody(Flux.error(new IllegalStateException("Can't write request body" + + throw new WebClientException("Can't write request body " + "of type '" + requestBodyType.toString() + - "' for content-type '" + mediaType.toString() + "'"))); + "' for content-type '" + mediaType.toString() + "'"); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebClientException.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebClientException.java new file mode 100644 index 0000000000..4c4ce11fcb --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebClientException.java @@ -0,0 +1,36 @@ +/* + * 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.web.client.reactive; + +import org.springframework.core.NestedRuntimeException; + +/** + * Base class for exceptions thrown by {@link WebClient} whenever + * it encounters client-side errors. + * + * @author Brian Clozel + */ +public class WebClientException extends NestedRuntimeException { + + public WebClientException(String msg) { + super(msg); + } + + public WebClientException(String msg, Throwable cause) { + super(msg, cause); + } +} From ddf996cfeb5a725f9ff21e8c14b44c41fcf27d99 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Mon, 4 Apr 2016 22:47:24 +0200 Subject: [PATCH 278/498] Polish HttpRequestBuilder API This commit makes messageEncoders a required argument for building a client request - those are needed to actually encode the body object as a reactive stream to be written to the HTTP request body. Removed raw types usage in DefaultHttpRequestBuilder. DefaultHttpRequestBuilder now uses a UriTemplateHandler to expand URI templates + variables into a concrete URI. Fixes #80, fixes #85, fixes #86 --- .../reactive/DefaultHttpRequestBuilder.java | 42 +++++-------------- .../client/reactive/HttpRequestBuilder.java | 8 +++- .../web/client/reactive/WebClient.java | 7 +--- 3 files changed, 20 insertions(+), 37 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultHttpRequestBuilder.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultHttpRequestBuilder.java index dde80dd455..cdbe4c9ed2 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultHttpRequestBuilder.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultHttpRequestBuilder.java @@ -18,7 +18,6 @@ package org.springframework.web.client.reactive; import java.net.URI; -import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -40,6 +39,8 @@ import org.springframework.http.client.reactive.ClientHttpRequest; import org.springframework.http.client.reactive.ClientHttpRequestFactory; import org.springframework.util.Assert; import org.springframework.web.client.RestClientException; +import org.springframework.web.util.DefaultUriTemplateHandler; +import org.springframework.web.util.UriTemplateHandler; /** * Builds a {@link ClientHttpRequest} @@ -51,6 +52,8 @@ import org.springframework.web.client.RestClientException; */ public class DefaultHttpRequestBuilder implements HttpRequestBuilder { + private final UriTemplateHandler uriTemplateHandler = new DefaultUriTemplateHandler(); + protected HttpMethod httpMethod; protected HttpHeaders httpHeaders; @@ -59,8 +62,6 @@ public class DefaultHttpRequestBuilder implements HttpRequestBuilder { protected Publisher contentPublisher; - protected List> messageEncoders; - protected final List cookies = new ArrayList(); protected DefaultHttpRequestBuilder() { @@ -69,7 +70,7 @@ public class DefaultHttpRequestBuilder implements HttpRequestBuilder { public DefaultHttpRequestBuilder(HttpMethod httpMethod, String urlTemplate, Object... urlVariables) throws RestClientException { this.httpMethod = httpMethod; this.httpHeaders = new HttpHeaders(); - this.url = parseURI(urlTemplate); + this.url = this.uriTemplateHandler.expand(urlTemplate, urlVariables); } public DefaultHttpRequestBuilder(HttpMethod httpMethod, URI url) { @@ -78,24 +79,6 @@ public class DefaultHttpRequestBuilder implements HttpRequestBuilder { this.url = url; } - protected DefaultHttpRequestBuilder setMessageEncoders(List> messageEncoders) { - this.messageEncoders = messageEncoders; - return this; - } - - private URI parseURI(String uri) throws RestClientException { - try { - return new URI(uri); - } - catch (URISyntaxException e) { - throw new RestClientException("could not parse URL template", e); - } - } - - public DefaultHttpRequestBuilder param(String name, String... values) { - return this; - } - public DefaultHttpRequestBuilder header(String name, String... values) { Arrays.stream(values).forEach(value -> this.httpHeaders.add(name, value)); return this; @@ -133,7 +116,7 @@ public class DefaultHttpRequestBuilder implements HttpRequestBuilder { return this; } - public DefaultHttpRequestBuilder contentStream(Publisher content) { + public DefaultHttpRequestBuilder contentStream(Publisher content) { this.contentPublisher = Flux.from(content); return this; } @@ -153,7 +136,7 @@ public class DefaultHttpRequestBuilder implements HttpRequestBuilder { return this; } - public ClientHttpRequest build(ClientHttpRequestFactory factory) { + public ClientHttpRequest build(ClientHttpRequestFactory factory, List> messageEncoders) { ClientHttpRequest request = factory.createRequest(this.httpMethod, this.url, this.httpHeaders); request.getHeaders().putAll(this.httpHeaders); @@ -161,7 +144,10 @@ public class DefaultHttpRequestBuilder implements HttpRequestBuilder { ResolvableType requestBodyType = ResolvableType.forInstance(this.contentPublisher); MediaType mediaType = request.getHeaders().getContentType(); - Optional> messageEncoder = resolveEncoder(requestBodyType, mediaType); + Optional> messageEncoder = messageEncoders + .stream() + .filter(e -> e.canEncode(requestBodyType, mediaType)) + .findFirst(); if (messageEncoder.isPresent()) { DataBufferAllocator allocator = request.allocator(); @@ -175,13 +161,7 @@ public class DefaultHttpRequestBuilder implements HttpRequestBuilder { "' for content-type '" + mediaType.toString() + "'"); } } - return request; } - protected Optional> resolveEncoder(ResolvableType type, MediaType mediaType) { - return this.messageEncoders.stream() - .filter(e -> e.canEncode(type, mediaType)).findFirst(); - } - } \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/HttpRequestBuilder.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/HttpRequestBuilder.java index 55d5e18229..32e2527a5e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/HttpRequestBuilder.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/HttpRequestBuilder.java @@ -16,6 +16,9 @@ package org.springframework.web.client.reactive; +import java.util.List; + +import org.springframework.core.codec.Encoder; import org.springframework.http.client.reactive.ClientHttpRequest; import org.springframework.http.client.reactive.ClientHttpRequestFactory; @@ -29,6 +32,9 @@ public interface HttpRequestBuilder { /** * Build a {@link ClientHttpRequest} + * + * @param factory the factory that creates the actual {@link ClientHttpRequest} + * @param messageEncoders the {@link Encoder}s to use for encoding the request body */ - ClientHttpRequest build(ClientHttpRequestFactory factory); + ClientHttpRequest build(ClientHttpRequestFactory factory, List> messageEncoders); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebClient.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebClient.java index ff825bc4ba..50f9205c2f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebClient.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebClient.java @@ -31,8 +31,6 @@ import org.springframework.core.codec.support.JacksonJsonEncoder; import org.springframework.core.codec.support.JsonObjectDecoder; import org.springframework.core.codec.support.StringDecoder; import org.springframework.core.codec.support.StringEncoder; -import org.springframework.core.io.buffer.DataBufferAllocator; -import org.springframework.core.io.buffer.DefaultDataBufferAllocator; import org.springframework.http.HttpStatus; import org.springframework.http.client.reactive.ClientHttpRequest; import org.springframework.http.client.reactive.ClientHttpRequestFactory; @@ -85,7 +83,6 @@ public final class WebClient { */ public WebClient(ClientHttpRequestFactory requestFactory) { this.requestFactory = requestFactory; - DataBufferAllocator allocator = new DefaultDataBufferAllocator(); this.messageEncoders = Arrays.asList(new ByteBufferEncoder(), new StringEncoder(), new JacksonJsonEncoder()); this.messageDecoders = Arrays.asList(new ByteBufferDecoder(), new StringDecoder(), @@ -116,9 +113,9 @@ public final class WebClient { *
  • returning the response with a publisher of the body
  • * */ - public WebResponseActions perform(DefaultHttpRequestBuilder builder) { + public WebResponseActions perform(HttpRequestBuilder builder) { - ClientHttpRequest request = builder.setMessageEncoders(messageEncoders).build(requestFactory); + ClientHttpRequest request = builder.build(this.requestFactory, this.messageEncoders); final Mono clientResponse = request.execute() .log("org.springframework.http.client.reactive"); From 09327181c4d07031e4ed8aa7a822c34075de935a Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Mon, 4 Apr 2016 22:51:25 +0200 Subject: [PATCH 279/498] Improve ClientHttpRequestFactory javadoc This commit underlines the fact that the request created by a ClientHttpRequestFactory should be augmented with a message body using the `ClientHttpRequest#setBody` method before it is executed. See #82 --- .../http/client/reactive/ClientHttpRequestFactory.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ClientHttpRequestFactory.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ClientHttpRequestFactory.java index 4eea06aeec..af8c437825 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ClientHttpRequestFactory.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ClientHttpRequestFactory.java @@ -17,6 +17,8 @@ package org.springframework.http.client.reactive; import java.net.URI; +import org.reactivestreams.Publisher; + import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -29,6 +31,8 @@ public interface ClientHttpRequestFactory { /** * Create a new {@link ClientHttpRequest} for the specified HTTP method, URI and headers + *

    The returned request can be {@link ClientHttpRequest#setBody(Publisher) written to}, + * and then executed by calling {@link ClientHttpRequest#execute()} * * @param httpMethod the HTTP method to execute * @param uri the URI to create a request for From e3f87b3e3a9c1084ed24b2956a20fc4df7ce027b Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Mon, 4 Apr 2016 22:53:25 +0200 Subject: [PATCH 280/498] Polish --- .../reactive/ReactorClientHttpResponse.java | 4 ++-- .../reactive/WebClientIntegrationTests.java | 18 ++++++++++++------ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpResponse.java index df2d58aded..fda2260dbc 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpResponse.java @@ -52,7 +52,7 @@ public class ReactorClientHttpResponse implements ClientHttpResponse { @Override public Flux getBody() { - return Flux.from(channel.input()).map(b -> allocator.wrap(b.byteBuffer())); + return channel.input().map(b -> allocator.wrap(b.byteBuffer())); } @Override @@ -87,7 +87,7 @@ public class ReactorClientHttpResponse implements ClientHttpResponse { @Override public String toString() { return "ReactorClientHttpResponse{" + - "request=" + this.channel.method() + " " + this.channel.uri() + "," + + "request=" + this.channel.method().getName() + " " + this.channel.uri() + "," + "status=" + getStatusCode() + '}'; } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/client/reactive/WebClientIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/client/reactive/WebClientIntegrationTests.java index 24017c1b8f..0d3f15c7cd 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/client/reactive/WebClientIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/client/reactive/WebClientIntegrationTests.java @@ -16,6 +16,10 @@ package org.springframework.web.client.reactive; +import static org.junit.Assert.*; +import static org.springframework.web.client.reactive.HttpRequestBuilders.*; +import static org.springframework.web.client.reactive.WebResponseExtractors.*; + import java.util.function.Consumer; import okhttp3.HttpUrl; @@ -36,12 +40,6 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.http.client.reactive.ReactorHttpClientRequestFactory; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThat; -import static org.springframework.web.client.reactive.HttpRequestBuilders.get; -import static org.springframework.web.client.reactive.HttpRequestBuilders.post; -import static org.springframework.web.client.reactive.WebResponseExtractors.*; - /** * @author Brian Clozel */ @@ -77,6 +75,7 @@ public class WebClientIntegrationTests { ).assertComplete(); RecordedRequest request = server.takeRequest(); + assertEquals(1, server.getRequestCount()); assertEquals("*/*", request.getHeader(HttpHeaders.ACCEPT)); assertEquals("/greeting?name=Spring", request.getPath()); } @@ -98,6 +97,7 @@ public class WebClientIntegrationTests { ts.awaitAndAssertNextValues("Hello Spring!").assertComplete(); RecordedRequest request = server.takeRequest(); + assertEquals(1, server.getRequestCount()); assertEquals("testvalue", request.getHeader("X-Test-Header")); assertEquals("*/*", request.getHeader(HttpHeaders.ACCEPT)); assertEquals("/greeting?name=Spring", request.getPath()); @@ -125,6 +125,7 @@ public class WebClientIntegrationTests { } }); RecordedRequest request = server.takeRequest(); + assertEquals(1, server.getRequestCount()); assertEquals("/greeting?name=Spring", request.getPath()); assertEquals("text/plain", request.getHeader(HttpHeaders.ACCEPT)); } @@ -145,6 +146,7 @@ public class WebClientIntegrationTests { result.subscribe(ts); ts.awaitAndAssertNextValuesWith(p -> assertEquals("barbar", p.getBar())).assertComplete(); RecordedRequest request = server.takeRequest(); + assertEquals(1, server.getRequestCount()); assertEquals("/pojo", request.getPath()); assertEquals("application/json", request.getHeader(HttpHeaders.ACCEPT)); } @@ -168,6 +170,7 @@ public class WebClientIntegrationTests { p -> assertThat(p.getBar(), Matchers.is("bar2")) ).assertValueCount(2).assertComplete(); RecordedRequest request = server.takeRequest(); + assertEquals(1, server.getRequestCount()); assertEquals("/pojos", request.getPath()); assertEquals("application/json", request.getHeader(HttpHeaders.ACCEPT)); } @@ -193,6 +196,7 @@ public class WebClientIntegrationTests { } ).assertComplete(); RecordedRequest request = server.takeRequest(); + assertEquals(1, server.getRequestCount()); assertEquals("/pojos", request.getPath()); assertEquals("application/json", request.getHeader(HttpHeaders.ACCEPT)); } @@ -216,6 +220,7 @@ public class WebClientIntegrationTests { ts.awaitAndAssertNextValuesWith(p -> assertEquals("BARBAR", p.getBar())).assertComplete(); RecordedRequest request = server.takeRequest(); + assertEquals(1, server.getRequestCount()); assertEquals("/pojo/capitalize", request.getPath()); assertEquals("{\"foo\":\"foofoo\",\"bar\":\"barbar\"}", request.getBody().readUtf8()); assertEquals("chunked", request.getHeader(HttpHeaders.TRANSFER_ENCODING)); @@ -240,6 +245,7 @@ public class WebClientIntegrationTests { ts.await().assertError(); RecordedRequest request = server.takeRequest(); + assertEquals(1, server.getRequestCount()); assertEquals("*/*", request.getHeader(HttpHeaders.ACCEPT)); assertEquals("/greeting?name=Spring", request.getPath()); } From 023dafa90386ed8ef84fb21a14e07feace0bf75b Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Mon, 4 Apr 2016 23:01:47 +0200 Subject: [PATCH 281/498] Support "text/*" media types in StringDecoder --- .../org/springframework/core/codec/support/StringDecoder.java | 2 +- .../springframework/core/codec/support/StringDecoderTests.java | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java index 51fc665aa9..13d19e0e95 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java @@ -63,7 +63,7 @@ public class StringDecoder extends AbstractDecoder { * and decode a single consolidated String or re-emit items as they are provided */ public StringDecoder(boolean reduceToSingleBuffer) { - super(new MimeType("text", "plain", DEFAULT_CHARSET)); + super(new MimeType("text", "*", DEFAULT_CHARSET)); this.reduceToSingleBuffer = reduceToSingleBuffer; } diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringDecoderTests.java index d8494312b9..33463f96dd 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringDecoderTests.java @@ -47,6 +47,7 @@ public class StringDecoderTests extends AbstractAllocatingTestCase { @Test public void canDecode() { assertTrue(decoder.canDecode(ResolvableType.forClass(String.class), MediaType.TEXT_PLAIN)); + assertTrue(decoder.canDecode(ResolvableType.forClass(String.class), MediaType.TEXT_HTML)); assertFalse(decoder.canDecode(ResolvableType.forClass(Integer.class), MediaType.TEXT_PLAIN)); assertFalse(decoder.canDecode(ResolvableType.forClass(String.class), MediaType.APPLICATION_JSON)); } From 87ce5d641cde0036cc1e476a0092e1aafe64dc4b Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Thu, 7 Apr 2016 20:18:44 +0200 Subject: [PATCH 282/498] Update dependencies - Spring Framework 4.2.3.RELEASE -> 4.3.0.RC1 - Netty 4.1.0.CR3 -> 4.1.0.CR6 - Jackson 2.6.2 -> 2.7.3 - Jetty 9.3.5.v20151012 -> 9.3.8.v20160314 - Undertow 1.3.5.Final -> 1.3.20.Final --- spring-web-reactive/build.gradle | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index 8078bbf5c2..76c8db41f9 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -27,12 +27,13 @@ repositories { } ext { - springVersion = '4.2.3.RELEASE' + springVersion = '4.3.0.RC1' reactorVersion = '2.5.0.BUILD-SNAPSHOT' reactorNettyVersion = '2.5.0.BUILD-SNAPSHOT' tomcatVersion = '8.0.33' - jettyVersion = '9.3.5.v20151012' - nettyVersion = '4.1.0.CR3' + jettyVersion = '9.3.8.v20160314' + nettyVersion = '4.1.0.CR6' + jacksonVersion = '2.7.3' javadocLinks = [ "http://docs.oracle.com/javase/8/docs/api/", @@ -97,11 +98,12 @@ dependencies { optional "org.springframework:spring-context-support:${springVersion}" // for FreeMarker optional 'io.reactivex:rxjava:1.1.0' optional "io.reactivex:rxnetty-http:0.5.2-SNAPSHOT" - optional "com.fasterxml.jackson.core:jackson-databind:2.6.2" + optional "com.fasterxml.jackson.core:jackson-annotations:${jacksonVersion}" + optional "com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}" optional "io.projectreactor:reactor-netty:${reactorNettyVersion}" optional "org.apache.tomcat:tomcat-util:${tomcatVersion}" optional "org.apache.tomcat.embed:tomcat-embed-core:${tomcatVersion}" - optional 'io.undertow:undertow-core:1.3.5.Final' + optional 'io.undertow:undertow-core:1.3.20.Final' optional "org.eclipse.jetty:jetty-server:${jettyVersion}" optional "org.eclipse.jetty:jetty-servlet:${jettyVersion}" optional("org.freemarker:freemarker:2.3.23") From da2276a954fd5bea3388ad0072fe15b6de02a7ee Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Fri, 8 Apr 2016 01:53:18 +0100 Subject: [PATCH 283/498] First wave scheduler contract migration --- .../http/server/reactive/AsyncIntegrationTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java index af58c684e1..fd8bfe8094 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java @@ -24,7 +24,7 @@ import org.junit.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.SchedulerGroup; -import reactor.core.timer.Timer; +import reactor.core.scheduler.Timer; import org.springframework.core.io.buffer.DataBufferAllocator; import org.springframework.core.io.buffer.DefaultDataBufferAllocator; From a3b371bf4bd04ce590dc2b3df0a7e373b015e6d5 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Fri, 8 Apr 2016 22:18:46 +0200 Subject: [PATCH 284/498] Accept all media types in StringDecodere This commit adds the "*/*" media type in the list of compatible media types for the StringDecoder. This allows this decoder to decoder payloads of any media type as long as the target type is String. Fixes #87 --- .../core/codec/support/StringDecoder.java | 3 ++- .../codec/support/StringDecoderTests.java | 3 ++- .../reactive/WebClientIntegrationTests.java | 22 +++++++++++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java index 13d19e0e95..090adca2d0 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java @@ -25,6 +25,7 @@ import reactor.core.publisher.Flux; import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.util.MimeType; +import org.springframework.util.MimeTypeUtils; /** * Decode from a bytes stream to a String stream. @@ -63,7 +64,7 @@ public class StringDecoder extends AbstractDecoder { * and decode a single consolidated String or re-emit items as they are provided */ public StringDecoder(boolean reduceToSingleBuffer) { - super(new MimeType("text", "*", DEFAULT_CHARSET)); + super(new MimeType("text", "*", DEFAULT_CHARSET), MimeTypeUtils.ALL); this.reduceToSingleBuffer = reduceToSingleBuffer; } diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringDecoderTests.java index 33463f96dd..44af9239a5 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringDecoderTests.java @@ -48,8 +48,9 @@ public class StringDecoderTests extends AbstractAllocatingTestCase { public void canDecode() { assertTrue(decoder.canDecode(ResolvableType.forClass(String.class), MediaType.TEXT_PLAIN)); assertTrue(decoder.canDecode(ResolvableType.forClass(String.class), MediaType.TEXT_HTML)); + assertTrue(decoder.canDecode(ResolvableType.forClass(String.class), MediaType.APPLICATION_JSON)); assertFalse(decoder.canDecode(ResolvableType.forClass(Integer.class), MediaType.TEXT_PLAIN)); - assertFalse(decoder.canDecode(ResolvableType.forClass(String.class), MediaType.APPLICATION_JSON)); + assertFalse(decoder.canDecode(ResolvableType.forClass(Pojo.class), MediaType.APPLICATION_JSON)); } @Test diff --git a/spring-web-reactive/src/test/java/org/springframework/web/client/reactive/WebClientIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/client/reactive/WebClientIntegrationTests.java index 0d3f15c7cd..5f37a963fb 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/client/reactive/WebClientIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/client/reactive/WebClientIntegrationTests.java @@ -130,6 +130,28 @@ public class WebClientIntegrationTests { assertEquals("text/plain", request.getHeader(HttpHeaders.ACCEPT)); } + @Test + public void shouldGetJsonAsMonoOfString() throws Exception { + + HttpUrl baseUrl = server.url("/json"); + String content = "{\"bar\":\"barbar\",\"foo\":\"foofoo\"}"; + this.server.enqueue(new MockResponse().setHeader("Content-Type", "application/json") + .setBody(content)); + + Mono result = this.webClient + .perform(get(baseUrl.toString()) + .accept(MediaType.APPLICATION_JSON)) + .extract(body(String.class)); + + TestSubscriber ts = new TestSubscriber(); + result.subscribe(ts); + ts.awaitAndAssertNextValues(content).assertComplete(); + RecordedRequest request = server.takeRequest(); + assertEquals(1, server.getRequestCount()); + assertEquals("/json", request.getPath()); + assertEquals("application/json", request.getHeader(HttpHeaders.ACCEPT)); + } + @Test public void shouldGetJsonAsMonoOfPojo() throws Exception { From ff6b639cf9ea00a4a96f85175e0bc11f1d40c7ea Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Mon, 11 Apr 2016 16:04:01 +0100 Subject: [PATCH 285/498] sync with dispatchOn change in reactor-core --- .../http/server/reactive/AsyncIntegrationTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java index fd8bfe8094..6e99d57c92 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java @@ -67,7 +67,7 @@ public class AsyncIntegrationTests extends AbstractHttpHandlerIntegrationTests { return response.setBody(Flux.just("h", "e", "l", "l", "o") .useTimer(Timer.global()) .delay(Duration.ofMillis(100)) - .dispatchOn(asyncGroup) + .publishOn(asyncGroup) .collect(allocator::allocateBuffer, (buffer, str) -> buffer.write(str.getBytes()))); } From 3460e577ad86c6d660bab066e751911b16182f73 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 14 Apr 2016 14:50:59 -0400 Subject: [PATCH 286/498] Polish "simple" handler support - correct name of HttpHandlerHandlerAdapter to WebHandlerHandlerAdapter - shorten SimpleHandlerResultHandler to SimpleResultHandler - add HandlerResult constructor without Model - update tests --- .../web/reactive/HandlerResult.java | 13 +++++- ...tHandler.java => SimpleResultHandler.java} | 44 ++++++++++++------- ...ter.java => WebHandlerHandlerAdapter.java} | 10 ++--- ...sts.java => SimpleResultHandlerTests.java} | 11 +++-- ...s.java => WebHandlerIntegrationTests.java} | 25 ++++++----- .../RequestMappingIntegrationTests.java | 6 +-- 6 files changed, 65 insertions(+), 44 deletions(-) rename spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/{SimpleHandlerResultHandler.java => SimpleResultHandler.java} (58%) rename spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/{HttpHandlerHandlerAdapter.java => WebHandlerHandlerAdapter.java} (81%) rename spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/{SimpleHandlerResultHandlerTests.java => SimpleResultHandlerTests.java} (92%) rename spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/{SimpleUrlHandlerMappingIntegrationTests.java => WebHandlerIntegrationTests.java} (87%) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java index 6f4bd8220b..3c211e5644 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java @@ -44,6 +44,16 @@ public class HandlerResult { private Function> exceptionHandler; + /** + * Create a new {@code HandlerResult}. + * @param handler the handler that handled the request + * @param returnValue the return value from the handler possibly {@code null} + * @param returnValueType the return value type + */ + public HandlerResult(Object handler, Object returnValue, ResolvableType returnValueType) { + this(handler, returnValue, returnValueType, null); + } + /** * Create a new {@code HandlerResult}. * @param handler the handler that handled the request @@ -54,11 +64,10 @@ public class HandlerResult { public HandlerResult(Object handler, Object returnValue, ResolvableType returnValueType, ModelMap model) { Assert.notNull(handler, "'handler' is required"); Assert.notNull(returnValueType, "'returnValueType' is required"); - Assert.notNull(model, "'model' is required"); this.handler = handler; this.returnValue = Optional.ofNullable(returnValue); this.returnValueType = returnValueType; - this.model = model; + this.model = (model != null ? model : new ExtendedModelMap()); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleResultHandler.java similarity index 58% rename from spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleResultHandler.java index fc46f9d596..e8b56e4ce1 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleResultHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -29,24 +29,28 @@ import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.HandlerResultHandler; import org.springframework.web.server.ServerWebExchange; + /** - * Supports {@link HandlerResult} with a {@code void} or {@code Publisher} value. - * An optional {link ConversionService} can be used to support types that can be converted to - * {@code Publisher}, like {@code Observable} or {@code CompletableFuture}. + * A simple handler for return values of type {@code void}, or + * {@code Publisher}, or if a {link ConversionService} is provided, also + * of any other async return value types that can be converted to + * {@code Publisher} such as {@code Observable} or + * {@code CompletableFuture}. * * @author Sebastien Deleuze + * @author Rossen Stoyanchev */ -public class SimpleHandlerResultHandler implements Ordered, HandlerResultHandler { +public class SimpleResultHandler implements Ordered, HandlerResultHandler { private int order = Ordered.LOWEST_PRECEDENCE; private ConversionService conversionService; - public SimpleHandlerResultHandler() { + public SimpleResultHandler() { } - public SimpleHandlerResultHandler(ConversionService conversionService) { + public SimpleResultHandler(ConversionService conversionService) { Assert.notNull(conversionService, "'conversionService' is required."); this.conversionService = conversionService; } @@ -61,30 +65,36 @@ public class SimpleHandlerResultHandler implements Ordered, HandlerResultHandler return this.order; } + @Override public boolean supports(HandlerResult result) { ResolvableType type = result.getReturnValueType(); - return (type != null && Void.TYPE.equals(type.getRawClass()) || - (isConvertibleToPublisher(type) && Void.class.isAssignableFrom(type.getGeneric(0).getRawClass()))); + return (type != null && (Void.TYPE.equals(type.getRawClass()) || isConvertibleToVoidPublisher(type))); + } + + private boolean isConvertibleToVoidPublisher(ResolvableType type) { + return (isConvertibleToPublisher(type) && + Void.class.isAssignableFrom(type.getGeneric(0).getRawClass())); } private boolean isConvertibleToPublisher(ResolvableType type) { - return Publisher.class.isAssignableFrom(type.getRawClass()) || - ((this.conversionService != null) && - this.conversionService.canConvert(type.getRawClass(), Publisher.class)); + Class clazz = type.getRawClass(); + return (Publisher.class.isAssignableFrom(clazz) || + ((this.conversionService != null) && this.conversionService.canConvert(clazz, Publisher.class))); } @SuppressWarnings("unchecked") @Override public Mono handleResult(ServerWebExchange exchange, HandlerResult result) { - Optional value = result.getReturnValue(); - if (!value.isPresent() || Void.TYPE.equals(result.getReturnValueType().getRawClass())) { + Optional optional = result.getReturnValue(); + if (!optional.isPresent()) { return Mono.empty(); } - if (value.get() instanceof Mono) { - return (Mono) value.get(); + Object returnValue = optional.get(); + if (returnValue instanceof Mono) { + return (Mono) returnValue; } - return Mono.from(this.conversionService.convert(value.get(), Publisher.class)); + return Mono.from(this.conversionService.convert(returnValue, Publisher.class)); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/HttpHandlerHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/WebHandlerHandlerAdapter.java similarity index 81% rename from spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/HttpHandlerHandlerAdapter.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/WebHandlerHandlerAdapter.java index 67cc1fc2cc..1dfec138e9 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/HttpHandlerHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/WebHandlerHandlerAdapter.java @@ -29,13 +29,12 @@ import org.springframework.web.server.WebHandler; import org.springframework.web.server.ServerWebExchange; /** - * Support use of {@link org.springframework.web.server.WebHandler} through the - * {@link DispatcherHandler}. + * Adapter to use a {@link WebHandler} through the {@link DispatcherHandler}. * * @author Rossen Stoyanchev * @author Sebastien Deleuze */ -public class HttpHandlerHandlerAdapter implements HandlerAdapter { +public class WebHandlerHandlerAdapter implements HandlerAdapter { private static final ResolvableType PUBLISHER_VOID = ResolvableType.forClassWithGenerics( Publisher.class, Void.class); @@ -49,9 +48,8 @@ public class HttpHandlerHandlerAdapter implements HandlerAdapter { @Override public Mono handle(ServerWebExchange exchange, Object handler) { WebHandler webHandler = (WebHandler) handler; - Mono completion = webHandler.handle(exchange); - ModelMap model = new ExtendedModelMap(); - return Mono.just(new HandlerResult(webHandler, completion, PUBLISHER_VOID, model)); + Mono mono = webHandler.handle(exchange); + return Mono.just(new HandlerResult(webHandler, mono, PUBLISHER_VOID)); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleResultHandlerTests.java similarity index 92% rename from spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandlerTests.java rename to spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleResultHandlerTests.java index 0056fc6858..9a94d4c2ee 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleHandlerResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleResultHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -27,7 +27,6 @@ import org.springframework.core.ResolvableType; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter; import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter; -import org.springframework.ui.ExtendedModelMap; import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerResult; @@ -37,12 +36,12 @@ import static org.junit.Assert.assertTrue; /** * @author Sebastien Deleuze */ -public class SimpleHandlerResultHandlerTests { +public class SimpleResultHandlerTests { @Test public void supports() throws NoSuchMethodException { - SimpleHandlerResultHandler resultHandler = new SimpleHandlerResultHandler(); + SimpleResultHandler resultHandler = new SimpleResultHandler(); TestController controller = new TestController(); HandlerMethod hm = new HandlerMethod(controller, TestController.class.getMethod("voidReturnValue")); @@ -77,7 +76,7 @@ public class SimpleHandlerResultHandlerTests { GenericConversionService conversionService = new GenericConversionService(); conversionService.addConverter(new ReactiveStreamsToCompletableFutureConverter()); conversionService.addConverter(new ReactiveStreamsToRxJava1Converter()); - SimpleHandlerResultHandler resultHandler = new SimpleHandlerResultHandler(conversionService); + SimpleResultHandler resultHandler = new SimpleResultHandler(conversionService); TestController controller = new TestController(); HandlerMethod hm = new HandlerMethod(controller, TestController.class.getMethod("voidReturnValue")); @@ -106,7 +105,7 @@ public class SimpleHandlerResultHandlerTests { } private HandlerResult createHandlerResult(HandlerMethod hm, ResolvableType type) { - return new HandlerResult(hm, null, type, new ExtendedModelMap()); + return new HandlerResult(hm, null, type); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/WebHandlerIntegrationTests.java similarity index 87% rename from spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java rename to spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/WebHandlerIntegrationTests.java index a8fdd8b0aa..c6c8f1747a 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/WebHandlerIntegrationTests.java @@ -47,9 +47,11 @@ import static org.junit.Assert.assertEquals; /** + * Integration tests with simple WebHandler's processing requests. + * * @author Rossen Stoyanchev */ -public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandlerIntegrationTests { +public class WebHandlerIntegrationTests extends AbstractHttpHandlerIntegrationTests { private static final Charset UTF_8 = Charset.forName("UTF-8"); @@ -59,14 +61,14 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler StaticApplicationContext wac = new StaticApplicationContext(); wac.registerSingleton("hm", TestHandlerMapping.class); - wac.registerSingleton("ha", HttpHandlerHandlerAdapter.class); - wac.registerSingleton("rh", SimpleHandlerResultHandler.class); + wac.registerSingleton("ha", WebHandlerHandlerAdapter.class); + wac.registerSingleton("rh", SimpleResultHandler.class); wac.refresh(); - DispatcherHandler webHandler = new DispatcherHandler(); - webHandler.setApplicationContext(wac); + DispatcherHandler dispatcherHandler = new DispatcherHandler(); + dispatcherHandler.setApplicationContext(wac); - return WebHttpHandlerBuilder.webHandler(webHandler) + return WebHttpHandlerBuilder.webHandler(dispatcherHandler) .exceptionHandlers(new ResponseStatusExceptionHandler()) .build(); } @@ -137,12 +139,16 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler } } + + private static DataBuffer asDataBuffer(String text) { + return new DefaultDataBufferAllocator().allocateBuffer().write(text.getBytes(StandardCharsets.UTF_8)); + } + private static class FooHandler implements WebHandler { @Override public Mono handle(ServerWebExchange exchange) { - DataBuffer buffer = new DefaultDataBufferAllocator().allocateBuffer() - .write("foo".getBytes(StandardCharsets.UTF_8)); + DataBuffer buffer = asDataBuffer("foo"); return exchange.getResponse().setBody(Flux.just(buffer)); } } @@ -151,8 +157,7 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler @Override public Mono handle(ServerWebExchange exchange) { - DataBuffer buffer = new DefaultDataBufferAllocator().allocateBuffer() - .write("bar".getBytes(StandardCharsets.UTF_8)); + DataBuffer buffer = asDataBuffer("bar"); return exchange.getResponse().setBody(Flux.just(buffer)); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java index 6a4ae62956..58bb05524e 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java @@ -67,7 +67,7 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; import org.springframework.web.reactive.DispatcherHandler; import org.springframework.web.reactive.ViewResolver; -import org.springframework.web.reactive.handler.SimpleHandlerResultHandler; +import org.springframework.web.reactive.handler.SimpleResultHandler; import org.springframework.web.reactive.view.ViewResolverResultHandler; import org.springframework.web.reactive.view.freemarker.FreeMarkerConfigurer; import org.springframework.web.reactive.view.freemarker.FreeMarkerViewResolver; @@ -421,8 +421,8 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati } @Bean - public SimpleHandlerResultHandler simpleHandlerResultHandler() { - SimpleHandlerResultHandler resultHandler = new SimpleHandlerResultHandler(conversionService()); + public SimpleResultHandler simpleHandlerResultHandler() { + SimpleResultHandler resultHandler = new SimpleResultHandler(conversionService()); resultHandler.setOrder(2); return resultHandler; } From 341f23e0e666ce0a00090fe1cad91caa7cb5e97c Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 14 Apr 2016 15:56:53 -0400 Subject: [PATCH 287/498] Add "result" package under ~.web.reactive The new package is parallel to the "mvc" package under spring-webmvc and a place to hold the various programming model styles. --- .../http/client/reactive/package-info.java | 16 --------------- .../web/reactive/handler/package-info.java | 19 ++---------------- .../method/annotation/package-info.java | 20 ------------------- .../web/reactive/method/package-info.java | 20 ------------------- .../web/reactive/package-info.java | 18 +---------------- .../SimpleResultHandler.java | 2 +- .../WebHandlerHandlerAdapter.java | 4 +--- .../method/HandlerMethodArgumentResolver.java | 2 +- .../method/InvocableHandlerMethod.java | 2 +- .../annotation/ModelArgumentResolver.java | 4 ++-- .../RequestBodyArgumentResolver.java | 4 ++-- .../RequestMappingHandlerAdapter.java | 6 +++--- .../RequestMappingHandlerMapping.java | 2 +- .../RequestParamArgumentResolver.java | 4 ++-- .../annotation/ResponseBodyResultHandler.java | 2 +- .../method/annotation/package-info.java | 4 ++++ .../reactive/result/method/package-info.java | 4 ++++ .../web/reactive/result/package-info.java | 4 ++++ .../core/codec/support/jaxb/package-info.java | 16 --------------- .../reactive/DispatcherHandlerErrorTests.java | 6 +++--- .../SimpleResultHandlerTests.java | 3 ++- .../WebHandlerIntegrationTests.java | 5 ++++- .../method/InvocableHandlerMethodTests.java | 4 ++-- .../RequestMappingHandlerMappingTests.java | 2 +- .../RequestMappingIntegrationTests.java | 4 ++-- .../ResponseBodyResultHandlerTests.java | 2 +- 26 files changed, 45 insertions(+), 134 deletions(-) delete mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/package-info.java delete mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/method/package-info.java rename spring-web-reactive/src/main/java/org/springframework/web/reactive/{handler => result}/SimpleResultHandler.java (98%) rename spring-web-reactive/src/main/java/org/springframework/web/reactive/{handler => result}/WebHandlerHandlerAdapter.java (92%) rename spring-web-reactive/src/main/java/org/springframework/web/reactive/{ => result}/method/HandlerMethodArgumentResolver.java (96%) rename spring-web-reactive/src/main/java/org/springframework/web/reactive/{ => result}/method/InvocableHandlerMethod.java (99%) rename spring-web-reactive/src/main/java/org/springframework/web/reactive/{ => result}/method/annotation/ModelArgumentResolver.java (89%) rename spring-web-reactive/src/main/java/org/springframework/web/reactive/{ => result}/method/annotation/RequestBodyArgumentResolver.java (95%) rename spring-web-reactive/src/main/java/org/springframework/web/reactive/{ => result}/method/annotation/RequestMappingHandlerAdapter.java (96%) rename spring-web-reactive/src/main/java/org/springframework/web/reactive/{ => result}/method/annotation/RequestMappingHandlerMapping.java (98%) rename spring-web-reactive/src/main/java/org/springframework/web/reactive/{ => result}/method/annotation/RequestParamArgumentResolver.java (92%) rename spring-web-reactive/src/main/java/org/springframework/web/reactive/{ => result}/method/annotation/ResponseBodyResultHandler.java (99%) create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/package-info.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/package-info.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/package-info.java rename spring-web-reactive/src/test/java/org/springframework/web/reactive/{handler => result}/SimpleResultHandlerTests.java (97%) rename spring-web-reactive/src/test/java/org/springframework/web/reactive/{handler => result}/WebHandlerIntegrationTests.java (95%) rename spring-web-reactive/src/test/java/org/springframework/web/reactive/{ => result}/method/InvocableHandlerMethodTests.java (98%) rename spring-web-reactive/src/test/java/org/springframework/web/reactive/{ => result}/method/annotation/RequestMappingHandlerMappingTests.java (98%) rename spring-web-reactive/src/test/java/org/springframework/web/reactive/{ => result}/method/annotation/RequestMappingIntegrationTests.java (99%) rename spring-web-reactive/src/test/java/org/springframework/web/reactive/{ => result}/method/annotation/ResponseBodyResultHandlerTests.java (97%) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/package-info.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/package-info.java index 64e63d30de..99f7e54bb6 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/package-info.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/package-info.java @@ -1,19 +1,3 @@ -/* - * 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. - */ - /** * Core package of the reactive client HTTP support. * Provides {@link org.springframework.http.client.reactive.ClientHttpRequest} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/package-info.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/package-info.java index 9e6c1eba58..7de0c290e4 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/package-info.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/package-info.java @@ -1,20 +1,5 @@ -/* - * 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. - */ - /** - * Provides reactive HandlerMapping implementations. + * Provides standard HandlerMapping implementations, + * including abstract base classes for custom implementations. */ package org.springframework.web.reactive.handler; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/package-info.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/package-info.java deleted file mode 100644 index cd65a7ee26..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/package-info.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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. - */ - -/** - * Reactive infrastructure for annotation-based handler method processing. - */ -package org.springframework.web.reactive.method.annotation; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/package-info.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/package-info.java deleted file mode 100644 index 96a7727322..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/package-info.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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. - */ - -/** - * Reactive infrastructure for handler method processing. - */ -package org.springframework.web.reactive.method; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/package-info.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/package-info.java index 9ec7eabc9a..92e4457779 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/package-info.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/package-info.java @@ -1,20 +1,4 @@ -/* - * 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. - */ - /** - * Provides the core interfaces and classes for the Spring web reactive framework. + * Core interfaces and classes for Spring Web Reactive. */ package org.springframework.web.reactive; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/SimpleResultHandler.java similarity index 98% rename from spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleResultHandler.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/result/SimpleResultHandler.java index e8b56e4ce1..447147e7c3 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/SimpleResultHandler.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.web.reactive.handler; +package org.springframework.web.reactive.result; import java.util.Optional; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/WebHandlerHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/WebHandlerHandlerAdapter.java similarity index 92% rename from spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/WebHandlerHandlerAdapter.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/result/WebHandlerHandlerAdapter.java index 1dfec138e9..c9f8932eb4 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/WebHandlerHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/WebHandlerHandlerAdapter.java @@ -14,14 +14,12 @@ * limitations under the License. */ -package org.springframework.web.reactive.handler; +package org.springframework.web.reactive.result; import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; import org.springframework.core.ResolvableType; -import org.springframework.ui.ExtendedModelMap; -import org.springframework.ui.ModelMap; import org.springframework.web.reactive.DispatcherHandler; import org.springframework.web.reactive.HandlerAdapter; import org.springframework.web.reactive.HandlerResult; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/HandlerMethodArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/HandlerMethodArgumentResolver.java similarity index 96% rename from spring-web-reactive/src/main/java/org/springframework/web/reactive/method/HandlerMethodArgumentResolver.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/HandlerMethodArgumentResolver.java index 2c2e7c0c72..2876124e93 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/HandlerMethodArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/HandlerMethodArgumentResolver.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.web.reactive.method; +package org.springframework.web.reactive.result.method; import reactor.core.publisher.Mono; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java similarity index 99% rename from spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java index 860995a962..fc63515005 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.web.reactive.method; +package org.springframework.web.reactive.result.method; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ModelArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelArgumentResolver.java similarity index 89% rename from spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ModelArgumentResolver.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelArgumentResolver.java index 9365172f87..dbcd43b3d6 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ModelArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelArgumentResolver.java @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.web.reactive.method.annotation; +package org.springframework.web.reactive.result.method.annotation; import reactor.core.publisher.Mono; import org.springframework.core.MethodParameter; import org.springframework.ui.Model; import org.springframework.ui.ModelMap; -import org.springframework.web.reactive.method.HandlerMethodArgumentResolver; +import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver; import org.springframework.web.server.ServerWebExchange; /** diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestBodyArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java similarity index 95% rename from spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestBodyArgumentResolver.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java index c39aa0da50..216a62cc4c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestBodyArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.web.reactive.method.annotation; +package org.springframework.web.reactive.result.method.annotation; import java.util.List; @@ -31,7 +31,7 @@ import org.springframework.http.MediaType; import org.springframework.ui.ModelMap; import org.springframework.util.Assert; import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.reactive.method.HandlerMethodArgumentResolver; +import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver; import org.springframework.web.server.ServerWebExchange; /** diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java similarity index 96% rename from spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java index 4f369a6577..e906bf7a83 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.web.reactive.method.annotation; +package org.springframework.web.reactive.result.method.annotation; import java.lang.reflect.Method; import java.util.ArrayList; @@ -45,8 +45,8 @@ import org.springframework.web.method.HandlerMethod; import org.springframework.web.method.annotation.ExceptionHandlerMethodResolver; import org.springframework.web.reactive.HandlerAdapter; import org.springframework.web.reactive.HandlerResult; -import org.springframework.web.reactive.method.HandlerMethodArgumentResolver; -import org.springframework.web.reactive.method.InvocableHandlerMethod; +import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver; +import org.springframework.web.reactive.result.method.InvocableHandlerMethod; import org.springframework.web.server.ServerWebExchange; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java similarity index 98% rename from spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMapping.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java index d2d86dc76c..e6861bd8c2 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMapping.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.web.reactive.method.annotation; +package org.springframework.web.reactive.result.method.annotation; import java.util.Arrays; import java.util.Collection; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestParamArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestParamArgumentResolver.java similarity index 92% rename from spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestParamArgumentResolver.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestParamArgumentResolver.java index 8eceba031f..b518e3f628 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestParamArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestParamArgumentResolver.java @@ -14,14 +14,14 @@ * limitations under the License. */ -package org.springframework.web.reactive.method.annotation; +package org.springframework.web.reactive.result.method.annotation; import reactor.core.publisher.Mono; import org.springframework.core.MethodParameter; import org.springframework.ui.ModelMap; import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.reactive.method.HandlerMethodArgumentResolver; +import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java similarity index 99% rename from spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java index 10f4470754..e0b5c2fdb8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.web.reactive.method.annotation; +package org.springframework.web.reactive.result.method.annotation; import java.util.ArrayList; import java.util.Collections; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/package-info.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/package-info.java new file mode 100644 index 0000000000..eee90dcffd --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/package-info.java @@ -0,0 +1,4 @@ +/** + * Infrastructure for annotation-based handler method processing. + */ +package org.springframework.web.reactive.result.method.annotation; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/package-info.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/package-info.java new file mode 100644 index 0000000000..9761f929e6 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/package-info.java @@ -0,0 +1,4 @@ +/** + * Infrastructure for handler method processing. + */ +package org.springframework.web.reactive.result.method; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/package-info.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/package-info.java new file mode 100644 index 0000000000..3c4228f0fd --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/package-info.java @@ -0,0 +1,4 @@ +/** + * Provides various controller styles for request handling. + */ +package org.springframework.web.reactive.result; diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/package-info.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/package-info.java index 0500a2aae5..e02f2405c1 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/package-info.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/package-info.java @@ -1,18 +1,2 @@ -/* - * 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. - */ - @javax.xml.bind.annotation.XmlSchema(namespace = "namespace") package org.springframework.core.codec.support.jaxb; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java index 92617fda03..99b99b004b 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java @@ -46,9 +46,9 @@ import org.springframework.web.ResponseStatusException; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; -import org.springframework.web.reactive.method.annotation.RequestMappingHandlerAdapter; -import org.springframework.web.reactive.method.annotation.RequestMappingHandlerMapping; -import org.springframework.web.reactive.method.annotation.ResponseBodyResultHandler; +import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter; +import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping; +import org.springframework.web.reactive.result.method.annotation.ResponseBodyResultHandler; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebExceptionHandler; import org.springframework.web.server.WebFilter; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/SimpleResultHandlerTests.java similarity index 97% rename from spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleResultHandlerTests.java rename to spring-web-reactive/src/test/java/org/springframework/web/reactive/result/SimpleResultHandlerTests.java index 9a94d4c2ee..c58f189ca5 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/SimpleResultHandlerTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.web.reactive.handler; +package org.springframework.web.reactive.result; import java.util.concurrent.CompletableFuture; @@ -29,6 +29,7 @@ import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutu import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter; import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerResult; +import org.springframework.web.reactive.result.SimpleResultHandler; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/WebHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/WebHandlerIntegrationTests.java similarity index 95% rename from spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/WebHandlerIntegrationTests.java rename to spring-web-reactive/src/test/java/org/springframework/web/reactive/result/WebHandlerIntegrationTests.java index c6c8f1747a..49d4aa1e26 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/WebHandlerIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/WebHandlerIntegrationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.web.reactive.handler; +package org.springframework.web.reactive.result; import java.net.URI; import java.nio.charset.Charset; @@ -38,6 +38,9 @@ import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestTemplate; import org.springframework.web.reactive.DispatcherHandler; import org.springframework.web.reactive.ResponseStatusExceptionHandler; +import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping; +import org.springframework.web.reactive.result.SimpleResultHandler; +import org.springframework.web.reactive.result.WebHandlerHandlerAdapter; import org.springframework.web.server.WebHandler; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.WebHttpHandlerBuilder; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/InvocableHandlerMethodTests.java similarity index 98% rename from spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java rename to spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/InvocableHandlerMethodTests.java index cf4b5da07a..3d9ae0bbef 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/InvocableHandlerMethodTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/InvocableHandlerMethodTests.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.web.reactive.method; +package org.springframework.web.reactive.result.method; import java.lang.reflect.Method; import java.net.URI; @@ -35,7 +35,7 @@ import org.springframework.ui.ModelMap; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerResult; -import org.springframework.web.reactive.method.annotation.RequestParamArgumentResolver; +import org.springframework.web.reactive.result.method.annotation.RequestParamArgumentResolver; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange; import org.springframework.web.server.session.WebSessionManager; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMappingTests.java similarity index 98% rename from spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java rename to spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMappingTests.java index d29d0d7f2e..d9bba5aefb 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerMappingTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMappingTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.web.reactive.method.annotation; +package org.springframework.web.reactive.result.method.annotation; import java.net.URI; import java.util.List; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java similarity index 99% rename from spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java rename to spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java index 58bb05524e..66d595e495 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.web.reactive.method.annotation; +package org.springframework.web.reactive.result.method.annotation; import java.net.URI; import java.nio.ByteBuffer; @@ -67,7 +67,7 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; import org.springframework.web.reactive.DispatcherHandler; import org.springframework.web.reactive.ViewResolver; -import org.springframework.web.reactive.handler.SimpleResultHandler; +import org.springframework.web.reactive.result.SimpleResultHandler; import org.springframework.web.reactive.view.ViewResolverResultHandler; import org.springframework.web.reactive.view.freemarker.FreeMarkerConfigurer; import org.springframework.web.reactive.view.freemarker.FreeMarkerViewResolver; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java similarity index 97% rename from spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandlerTests.java rename to spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java index 8216843c3f..f290593a8d 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/ResponseBodyResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.web.reactive.method.annotation; +package org.springframework.web.reactive.result.method.annotation; import java.util.Collections; From 33a7b91e57b98bbb9a8afc669d07213d8e7fd418 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 14 Apr 2016 12:56:57 -0400 Subject: [PATCH 288/498] Add abstract case class HandlerMappings This commit adds AbstractHandlerMethodMapping, a starting point for AbstractHandlerMapping, and HttpRequestPathHelper with a similar purpose to UrlPathHelper but based with ServerWebExchange as input. --- .../handler/AbstractHandlerMapping.java | 104 ++++ .../method/AbstractHandlerMethodMapping.java | 562 ++++++++++++++++++ .../web/util/HttpRequestPathHelper.java | 66 ++ .../method/HandlerMethodMappingTests.java | 204 +++++++ 4 files changed, 936 insertions(+) create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/AbstractHandlerMapping.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/AbstractHandlerMethodMapping.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/util/HttpRequestPathHelper.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/HandlerMethodMappingTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/AbstractHandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/AbstractHandlerMapping.java new file mode 100644 index 0000000000..8cb5267337 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/AbstractHandlerMapping.java @@ -0,0 +1,104 @@ +/* + * 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.web.reactive.handler; + +import org.springframework.context.support.ApplicationObjectSupport; +import org.springframework.core.Ordered; +import org.springframework.util.AntPathMatcher; +import org.springframework.util.Assert; +import org.springframework.util.PathMatcher; +import org.springframework.web.reactive.HandlerMapping; +import org.springframework.web.util.HttpRequestPathHelper; + +/** + * Abstract base class for {@link org.springframework.web.reactive.HandlerMapping} + * implementations. + * + * @author Rossen Stoyanchev + */ +public abstract class AbstractHandlerMapping extends ApplicationObjectSupport + implements HandlerMapping, Ordered { + + private int order = Integer.MAX_VALUE; // default: same as non-Ordered + + private HttpRequestPathHelper pathHelper = new HttpRequestPathHelper(); + + private PathMatcher pathMatcher = new AntPathMatcher(); + + + // TODO: CORS + + /** + * Specify the order value for this HandlerMapping bean. + *

    Default value is {@code Integer.MAX_VALUE}, meaning that it's non-ordered. + * @see org.springframework.core.Ordered#getOrder() + */ + public final void setOrder(int order) { + this.order = order; + } + + @Override + public final int getOrder() { + return this.order; + } + + /** + * Set if the path should be URL-decoded. This sets the same property on the + * underlying path helper. + * @see HttpRequestPathHelper#setUrlDecode(boolean) + */ + public void setUrlDecode(boolean urlDecode) { + this.pathHelper.setUrlDecode(urlDecode); + } + + /** + * Set the {@link HttpRequestPathHelper} to use for resolution of lookup + * paths. Use this to override the default implementation with a custom + * subclass or to share common path helper settings across multiple + * HandlerMappings. + */ + public void setPathHelper(HttpRequestPathHelper pathHelper) { + this.pathHelper = pathHelper; + } + + /** + * Return the {@link HttpRequestPathHelper} implementation to use for + * resolution of lookup paths. + */ + public HttpRequestPathHelper getPathHelper() { + return this.pathHelper; + } + + /** + * Set the PathMatcher implementation to use for matching URL paths + * against registered URL patterns. Default is AntPathMatcher. + * @see org.springframework.util.AntPathMatcher + */ + public void setPathMatcher(PathMatcher pathMatcher) { + Assert.notNull(pathMatcher, "PathMatcher must not be null"); + this.pathMatcher = pathMatcher; + // this.corsConfigSource.setPathMatcher(pathMatcher); + } + + /** + * Return the PathMatcher implementation to use for matching URL paths + * against registered URL patterns. + */ + public PathMatcher getPathMatcher() { + return this.pathMatcher; + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/AbstractHandlerMethodMapping.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/AbstractHandlerMethodMapping.java new file mode 100644 index 0000000000..6b23f30ee9 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/AbstractHandlerMethodMapping.java @@ -0,0 +1,562 @@ +/* + * 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.web.reactive.result.method; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import reactor.core.publisher.Mono; + +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.MethodIntrospector; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.reactive.HandlerMapping; +import org.springframework.web.reactive.handler.AbstractHandlerMapping; +import org.springframework.web.server.ServerWebExchange; + +/** + * Abstract base class for {@link HandlerMapping} implementations that define + * a mapping between a request and a {@link HandlerMethod}. + * + *

    For each registered handler method, a unique mapping is maintained with + * subclasses defining the details of the mapping type {@code }. + * + * @author Rossen Stoyanchev + * @param The mapping for a {@link HandlerMethod} containing the conditions + * needed to match the handler method to incoming request. + */ +public abstract class AbstractHandlerMethodMapping extends AbstractHandlerMapping implements InitializingBean { + + /** + * Bean name prefix for target beans behind scoped proxies. Used to exclude those + * targets from handler method detection, in favor of the corresponding proxies. + *

    We're not checking the autowire-candidate status here, which is how the + * proxy target filtering problem is being handled at the autowiring level, + * since autowire-candidate may have been turned to {@code false} for other + * reasons, while still expecting the bean to be eligible for handler methods. + *

    Originally defined in {@link org.springframework.aop.scope.ScopedProxyUtils} + * but duplicated here to avoid a hard dependency on the spring-aop module. + */ + private static final String SCOPED_TARGET_NAME_PREFIX = "scopedTarget."; + + + private final MappingRegistry mappingRegistry = new MappingRegistry(); + + + // TODO: handlerMethodMappingNamingStrategy + + /** + * Return a (read-only) map with all mappings and HandlerMethod's. + */ + public Map getHandlerMethods() { + this.mappingRegistry.acquireReadLock(); + try { + return Collections.unmodifiableMap(this.mappingRegistry.getMappings()); + } + finally { + this.mappingRegistry.releaseReadLock(); + } + } + + /** + * Return the internal mapping registry. Provided for testing purposes. + */ + MappingRegistry getMappingRegistry() { + return this.mappingRegistry; + } + + /** + * Register the given mapping. + *

    This method may be invoked at runtime after initialization has completed. + * @param mapping the mapping for the handler method + * @param handler the handler + * @param method the method + */ + public void registerMapping(T mapping, Object handler, Method method) { + this.mappingRegistry.register(mapping, handler, method); + } + + /** + * Un-register the given mapping. + *

    This method may be invoked at runtime after initialization has completed. + * @param mapping the mapping to unregister + */ + public void unregisterMapping(T mapping) { + this.mappingRegistry.unregister(mapping); + } + + + // Handler method detection + + /** + * Detects handler methods at initialization. + */ + @Override + public void afterPropertiesSet() { + initHandlerMethods(); + } + + /** + * Scan beans in the ApplicationContext, detect and register handler methods. + * @see #isHandler(Class) + * @see #getMappingForMethod(Method, Class) + * @see #handlerMethodsInitialized(Map) + */ + protected void initHandlerMethods() { + if (logger.isDebugEnabled()) { + logger.debug("Looking for request mappings in application context: " + getApplicationContext()); + } + String[] beanNames = getApplicationContext().getBeanNamesForType(Object.class); + + for (String beanName : beanNames) { + if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) { + Class beanType = null; + try { + beanType = getApplicationContext().getType(beanName); + } + catch (Throwable ex) { + // An unresolvable bean type, probably from a lazy bean - let's ignore it. + if (logger.isDebugEnabled()) { + logger.debug("Could not resolve target class for bean with name '" + beanName + "'", ex); + } + } + if (beanType != null && isHandler(beanType)) { + detectHandlerMethods(beanName); + } + } + } + handlerMethodsInitialized(getHandlerMethods()); + } + + /** + * Look for handler methods in a handler. + * @param handler the bean name of a handler or a handler instance + */ + protected void detectHandlerMethods(final Object handler) { + Class handlerType = (handler instanceof String ? + getApplicationContext().getType((String) handler) : handler.getClass()); + final Class userType = ClassUtils.getUserClass(handlerType); + + Map methods = MethodIntrospector.selectMethods(userType, + (MethodIntrospector.MetadataLookup) method -> getMappingForMethod(method, userType)); + + if (logger.isDebugEnabled()) { + logger.debug(methods.size() + " request handler methods found on " + userType + ": " + methods); + } + for (Map.Entry entry : methods.entrySet()) { + Method invocableMethod = AopUtils.selectInvocableMethod(entry.getKey(), userType); + T mapping = entry.getValue(); + registerHandlerMethod(handler, invocableMethod, mapping); + } + } + + /** + * Register a handler method and its unique mapping. Invoked at startup for + * each detected handler method. + * @param handler the bean name of the handler or the handler instance + * @param method the method to register + * @param mapping the mapping conditions associated with the handler method + * @throws IllegalStateException if another method was already registered + * under the same mapping + */ + protected void registerHandlerMethod(Object handler, Method method, T mapping) { + this.mappingRegistry.register(mapping, handler, method); + } + + /** + * Create the HandlerMethod instance. + * @param handler either a bean name or an actual handler instance + * @param method the target method + * @return the created HandlerMethod + */ + protected HandlerMethod createHandlerMethod(Object handler, Method method) { + HandlerMethod handlerMethod; + if (handler instanceof String) { + String beanName = (String) handler; + handlerMethod = new HandlerMethod(beanName, + getApplicationContext().getAutowireCapableBeanFactory(), method); + } + else { + handlerMethod = new HandlerMethod(handler, method); + } + return handlerMethod; + } + + /** + * Invoked after all handler methods have been detected. + * @param handlerMethods a read-only map with handler methods and mappings. + */ + protected void handlerMethodsInitialized(Map handlerMethods) { + } + + + // Handler method lookup + + /** + * Look up a handler method for the given request. + * @param exchange the current exchange + */ + @Override + public Mono getHandler(ServerWebExchange exchange) { + String lookupPath = getPathHelper().getLookupPathForRequest(exchange); + if (logger.isDebugEnabled()) { + logger.debug("Looking up handler method for path " + lookupPath); + } + this.mappingRegistry.acquireReadLock(); + try { + HandlerMethod handlerMethod = null; + try { + handlerMethod = lookupHandlerMethod(lookupPath, exchange); + } + catch (Exception ex) { + return Mono.error(ex); + } + if (logger.isDebugEnabled()) { + if (handlerMethod != null) { + logger.debug("Returning handler method [" + handlerMethod + "]"); + } + else { + logger.debug("Did not find handler method for [" + lookupPath + "]"); + } + } + return (handlerMethod != null ? Mono.just(handlerMethod.createWithResolvedBean()) : Mono.empty()); + } + finally { + this.mappingRegistry.releaseReadLock(); + } + } + + /** + * Look up the best-matching handler method for the current request. + * If multiple matches are found, the best match is selected. + * @param lookupPath mapping lookup path within the current servlet mapping + * @param exchange the current exchange + * @return the best-matching handler method, or {@code null} if no match + * @see #handleMatch(Object, String, ServerWebExchange) + * @see #handleNoMatch(Set, String, ServerWebExchange) + */ + protected HandlerMethod lookupHandlerMethod(String lookupPath, ServerWebExchange exchange) + throws Exception { + + List matches = new ArrayList(); + List directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath); + if (directPathMatches != null) { + addMatchingMappings(directPathMatches, matches, exchange); + } + if (matches.isEmpty()) { + // No choice but to go through all mappings... + addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, exchange); + } + + if (!matches.isEmpty()) { + Comparator comparator = new MatchComparator(getMappingComparator(exchange)); + Collections.sort(matches, comparator); + if (logger.isTraceEnabled()) { + logger.trace("Found " + matches.size() + " matching mapping(s) for [" + + lookupPath + "] : " + matches); + } + Match bestMatch = matches.get(0); + if (matches.size() > 1) { + Match secondBestMatch = matches.get(1); + if (comparator.compare(bestMatch, secondBestMatch) == 0) { + Method m1 = bestMatch.handlerMethod.getMethod(); + Method m2 = secondBestMatch.handlerMethod.getMethod(); + throw new IllegalStateException("Ambiguous handler methods mapped for HTTP path '" + + lookupPath + "': {" + m1 + ", " + m2 + "}"); + } + } + handleMatch(bestMatch.mapping, lookupPath, exchange); + return bestMatch.handlerMethod; + } + else { + return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, exchange); + } + } + + private void addMatchingMappings(Collection mappings, List matches, ServerWebExchange exchange) { + for (T mapping : mappings) { + T match = getMatchingMapping(mapping, exchange); + if (match != null) { + matches.add(new Match(match, this.mappingRegistry.getMappings().get(mapping))); + } + } + } + + /** + * Invoked when a matching mapping is found. + * @param mapping the matching mapping + * @param lookupPath mapping lookup path within the current servlet mapping + * @param exchange the current exchange + */ + protected void handleMatch(T mapping, String lookupPath, ServerWebExchange exchange) { + } + + /** + * Invoked when no matching mapping is not found. + * @param mappings all registered mappings + * @param lookupPath mapping lookup path within the current servlet mapping + * @param exchange the current exchange + * @return an alternative HandlerMethod or {@code null} + * @throws Exception provides details that can be translated into an error status code + */ + protected HandlerMethod handleNoMatch(Set mappings, String lookupPath, ServerWebExchange exchange) + throws Exception { + + return null; + } + + + // Abstract template methods + + /** + * Whether the given type is a handler with handler methods. + * @param beanType the type of the bean being checked + * @return "true" if this a handler type, "false" otherwise. + */ + protected abstract boolean isHandler(Class beanType); + + /** + * Provide the mapping for a handler method. A method for which no + * mapping can be provided is not a handler method. + * @param method the method to provide a mapping for + * @param handlerType the handler type, possibly a sub-type of the method's + * declaring class + * @return the mapping, or {@code null} if the method is not mapped + */ + protected abstract T getMappingForMethod(Method method, Class handlerType); + + /** + * Extract and return the URL paths contained in a mapping. + */ + protected abstract Set getMappingPathPatterns(T mapping); + + /** + * Check if a mapping matches the current request and return a (potentially + * new) mapping with conditions relevant to the current request. + * @param mapping the mapping to get a match for + * @param exchange the current exchange + * @return the match, or {@code null} if the mapping doesn't match + */ + protected abstract T getMatchingMapping(T mapping, ServerWebExchange exchange); + + /** + * Return a comparator for sorting matching mappings. + * The returned comparator should sort 'better' matches higher. + * @param exchange the current exchange + * @return the comparator (never {@code null}) + */ + protected abstract Comparator getMappingComparator(ServerWebExchange exchange); + + + /** + * A registry that maintains all mappings to handler methods, exposing methods + * to perform lookups and providing concurrent access. + * + *

    Package-private for testing purposes. + */ + class MappingRegistry { + + private final Map> registry = new HashMap<>(); + + private final Map mappingLookup = new LinkedHashMap<>(); + + private final MultiValueMap urlLookup = new LinkedMultiValueMap<>(); + + private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); + + /** + * Return all mappings and handler methods. Not thread-safe. + * @see #acquireReadLock() + */ + public Map getMappings() { + return this.mappingLookup; + } + + /** + * Return matches for the given URL path. Not thread-safe. + * @see #acquireReadLock() + */ + public List getMappingsByUrl(String urlPath) { + return this.urlLookup.get(urlPath); + } + + /** + * Acquire the read lock when using getMappings and getMappingsByUrl. + */ + public void acquireReadLock() { + this.readWriteLock.readLock().lock(); + } + + /** + * Release the read lock after using getMappings and getMappingsByUrl. + */ + public void releaseReadLock() { + this.readWriteLock.readLock().unlock(); + } + + public void register(T mapping, Object handler, Method method) { + this.readWriteLock.writeLock().lock(); + try { + HandlerMethod handlerMethod = createHandlerMethod(handler, method); + assertUniqueMethodMapping(handlerMethod, mapping); + + if (logger.isInfoEnabled()) { + logger.info("Mapped \"" + mapping + "\" onto " + handlerMethod); + } + this.mappingLookup.put(mapping, handlerMethod); + + List directUrls = getDirectUrls(mapping); + for (String url : directUrls) { + this.urlLookup.add(url, mapping); + } + + this.registry.put(mapping, new MappingRegistration<>(mapping, handlerMethod, directUrls)); + } + finally { + this.readWriteLock.writeLock().unlock(); + } + } + + private void assertUniqueMethodMapping(HandlerMethod newHandlerMethod, T mapping) { + HandlerMethod handlerMethod = this.mappingLookup.get(mapping); + if (handlerMethod != null && !handlerMethod.equals(newHandlerMethod)) { + throw new IllegalStateException( + "Ambiguous mapping. Cannot map '" + newHandlerMethod.getBean() + "' method \n" + + newHandlerMethod + "\nto " + mapping + ": There is already '" + + handlerMethod.getBean() + "' bean method\n" + handlerMethod + " mapped."); + } + } + + private List getDirectUrls(T mapping) { + List urls = new ArrayList<>(1); + for (String path : getMappingPathPatterns(mapping)) { + if (!getPathMatcher().isPattern(path)) { + urls.add(path); + } + } + return urls; + } + + public void unregister(T mapping) { + this.readWriteLock.writeLock().lock(); + try { + MappingRegistration definition = this.registry.remove(mapping); + if (definition == null) { + return; + } + + this.mappingLookup.remove(definition.getMapping()); + + for (String url : definition.getDirectUrls()) { + List list = this.urlLookup.get(url); + if (list != null) { + list.remove(definition.getMapping()); + if (list.isEmpty()) { + this.urlLookup.remove(url); + } + } + } + } + finally { + this.readWriteLock.writeLock().unlock(); + } + } + } + + + private static class MappingRegistration { + + private final T mapping; + + private final HandlerMethod handlerMethod; + + private final List directUrls; + + + public MappingRegistration(T mapping, HandlerMethod handlerMethod, List directUrls) { + Assert.notNull(mapping); + Assert.notNull(handlerMethod); + this.mapping = mapping; + this.handlerMethod = handlerMethod; + this.directUrls = (directUrls != null ? directUrls : Collections.emptyList()); + } + + public T getMapping() { + return this.mapping; + } + + public HandlerMethod getHandlerMethod() { + return this.handlerMethod; + } + + public List getDirectUrls() { + return this.directUrls; + } + } + + + /** + * A thin wrapper around a matched HandlerMethod and its mapping, for the purpose of + * comparing the best match with a comparator in the context of the current request. + */ + private class Match { + + private final T mapping; + + private final HandlerMethod handlerMethod; + + public Match(T mapping, HandlerMethod handlerMethod) { + this.mapping = mapping; + this.handlerMethod = handlerMethod; + } + + @Override + public String toString() { + return this.mapping.toString(); + } + } + + + private class MatchComparator implements Comparator { + + private final Comparator comparator; + + public MatchComparator(Comparator comparator) { + this.comparator = comparator; + } + + @Override + public int compare(Match match1, Match match2) { + return this.comparator.compare(match1.mapping, match2.mapping); + } + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/util/HttpRequestPathHelper.java b/spring-web-reactive/src/main/java/org/springframework/web/util/HttpRequestPathHelper.java new file mode 100644 index 0000000000..45c893d3e8 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/util/HttpRequestPathHelper.java @@ -0,0 +1,66 @@ +/* + * 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.web.util; + +import java.io.UnsupportedEncodingException; + +import org.springframework.web.server.ServerWebExchange; + +/** + * A helper class to obtain the lookup path for path matching purposes. + * + * @author Rossen Stoyanchev + */ +public class HttpRequestPathHelper { + + private boolean urlDecode = true; + + + // TODO: sanitize path, default/request encoding?, remove path params? + + /** + * Set if the request path should be URL-decoded. + *

    Default is "true". + * @see UriUtils#decode(String, String) + */ + public void setUrlDecode(boolean urlDecode) { + this.urlDecode = urlDecode; + } + + /** + * Whether the request path should be URL decoded. + */ + public boolean shouldUrlDecode() { + return this.urlDecode; + } + + + public String getLookupPathForRequest(ServerWebExchange exchange) { + String path = exchange.getRequest().getURI().getPath(); + return (this.shouldUrlDecode() ? decode(path) : path); + } + + private String decode(String path) { + try { + return UriUtils.decode(path, "UTF-8"); + } + catch (UnsupportedEncodingException ex) { + // Should not happen + throw new IllegalStateException("Could not decode request string [" + path + "]"); + } + } + +} \ No newline at end of file diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/HandlerMethodMappingTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/HandlerMethodMappingTests.java new file mode 100644 index 0000000000..0bcda250d9 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/HandlerMethodMappingTests.java @@ -0,0 +1,204 @@ +/* + * 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.web.reactive.result.method; + +import java.lang.reflect.Method; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Set; + +import org.junit.Before; +import org.junit.Test; +import reactor.core.publisher.Mono; +import reactor.core.test.TestSubscriber; + +import org.springframework.http.HttpMethod; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.stereotype.Controller; +import org.springframework.util.AntPathMatcher; +import org.springframework.util.PathMatcher; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.reactive.result.method.AbstractHandlerMethodMapping; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.WebSessionManager; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.mock; + +/** + * Unit tests for {@link AbstractHandlerMethodMapping}. + * + * @author Rossen Stoyanchev + */ +public class HandlerMethodMappingTests { + + private AbstractHandlerMethodMapping mapping; + + private MyHandler handler; + + private Method method1; + + private Method method2; + + + @Before + public void setUp() throws Exception { + this.mapping = new MyHandlerMethodMapping(); + this.handler = new MyHandler(); + this.method1 = handler.getClass().getMethod("handlerMethod1"); + this.method2 = handler.getClass().getMethod("handlerMethod2"); + } + + + @Test(expected = IllegalStateException.class) + public void registerDuplicates() { + this.mapping.registerMapping("foo", this.handler, this.method1); + this.mapping.registerMapping("foo", this.handler, this.method2); + } + + @Test + public void directMatch() throws Exception { + String key = "foo"; + this.mapping.registerMapping(key, this.handler, this.method1); + + Mono result = this.mapping.getHandler(createExchange(HttpMethod.GET, key)); + assertEquals(this.method1, ((HandlerMethod) result.get()).getMethod()); + } + + @Test + public void patternMatch() throws Exception { + this.mapping.registerMapping("/fo*", this.handler, this.method1); + this.mapping.registerMapping("/f*", this.handler, this.method2); + + Mono result = this.mapping.getHandler(createExchange(HttpMethod.GET, "/foo")); + assertEquals(this.method1, ((HandlerMethod) result.get()).getMethod()); + } + + @Test + public void ambiguousMatch() throws Exception { + this.mapping.registerMapping("/f?o", this.handler, this.method1); + this.mapping.registerMapping("/fo?", this.handler, this.method2); + Mono result = this.mapping.getHandler(createExchange(HttpMethod.GET, "/foo")); + + TestSubscriber subscriber = new TestSubscriber<>(); + result.subscribeWith(subscriber); + subscriber.assertError(IllegalStateException.class); + } + + @Test + public void registerMapping() throws Exception { + String key1 = "/foo"; + String key2 = "/foo*"; + this.mapping.registerMapping(key1, this.handler, this.method1); + this.mapping.registerMapping(key2, this.handler, this.method2); + + List directUrlMatches = this.mapping.getMappingRegistry().getMappingsByUrl(key1); + assertNotNull(directUrlMatches); + assertEquals(1, directUrlMatches.size()); + assertEquals(key1, directUrlMatches.get(0)); + } + + @Test + public void registerMappingWithSameMethodAndTwoHandlerInstances() throws Exception { + String key1 = "foo"; + String key2 = "bar"; + MyHandler handler1 = new MyHandler(); + MyHandler handler2 = new MyHandler(); + this.mapping.registerMapping(key1, handler1, this.method1); + this.mapping.registerMapping(key2, handler2, this.method1); + + List directUrlMatches = this.mapping.getMappingRegistry().getMappingsByUrl(key1); + assertNotNull(directUrlMatches); + assertEquals(1, directUrlMatches.size()); + assertEquals(key1, directUrlMatches.get(0)); + } + + @Test + public void unregisterMapping() throws Exception { + String key = "foo"; + this.mapping.registerMapping(key, this.handler, this.method1); + Mono result = this.mapping.getHandler(createExchange(HttpMethod.GET, key)); + assertNotNull(result.get()); + + this.mapping.unregisterMapping(key); + result = this.mapping.getHandler(createExchange(HttpMethod.GET, key)); + assertNull(result.get()); + assertNull(this.mapping.getMappingRegistry().getMappingsByUrl(key)); + } + + private ServerWebExchange createExchange(HttpMethod httpMethod, String path) throws URISyntaxException { + ServerHttpRequest request = new MockServerHttpRequest(httpMethod, new URI(path)); + WebSessionManager sessionManager = mock(WebSessionManager.class); + return new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); + } + + + private static class MyHandlerMethodMapping extends AbstractHandlerMethodMapping { + + private PathMatcher pathMatcher = new AntPathMatcher(); + + @Override + protected boolean isHandler(Class beanType) { + return true; + } + + @Override + protected String getMappingForMethod(Method method, Class handlerType) { + String methodName = method.getName(); + return methodName.startsWith("handler") ? methodName : null; + } + + @Override + protected Set getMappingPathPatterns(String key) { + return (this.pathMatcher.isPattern(key) ? Collections.emptySet() : Collections.singleton(key)); + } + + @Override + protected String getMatchingMapping(String pattern, ServerWebExchange exchange) { + String lookupPath = exchange.getRequest().getURI().getPath(); + return (this.pathMatcher.match(pattern, lookupPath) ? pattern : null); + } + + @Override + protected Comparator getMappingComparator(ServerWebExchange exchange) { + String lookupPath = exchange.getRequest().getURI().getPath(); + return this.pathMatcher.getPatternComparator(lookupPath); + } + + } + + @Controller + private static class MyHandler { + + @RequestMapping @SuppressWarnings("unused") + public void handlerMethod1() { + } + + @RequestMapping @SuppressWarnings("unused") + public void handlerMethod2() { + } + } +} \ No newline at end of file From 4a0adc2b2d17109473b8c4f78d30ca643018d575 Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Fri, 15 Apr 2016 19:55:33 +0100 Subject: [PATCH 289/498] Sync with reactor-netty and reactor-ipc --- .../reactive/ReactorClientHttpRequest.java | 94 ++++++++++++++++--- .../reactive/ReactorClientHttpResponse.java | 14 ++- .../reactive/AbstractServerHttpResponse.java | 2 +- ...Operator.java => ChannelSendOperator.java} | 4 +- .../reactive/ReactorHttpHandlerAdapter.java | 4 +- .../reactive/ReactorServerHttpRequest.java | 19 ++-- .../reactive/ReactorServerHttpResponse.java | 73 ++++++++++---- .../reactive/boot/ReactorHttpServer.java | 2 +- ...sts.java => ChannelSendOperatorTests.java} | 21 ++--- 9 files changed, 169 insertions(+), 64 deletions(-) rename spring-web-reactive/src/main/java/org/springframework/http/server/reactive/{WriteWithOperator.java => ChannelSendOperator.java} (97%) rename spring-web-reactive/src/test/java/org/springframework/http/server/reactive/{WriteWithOperatorTests.java => ChannelSendOperatorTests.java} (86%) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java index 4179184c28..b288f72593 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java @@ -17,21 +17,23 @@ package org.springframework.http.client.reactive; import java.net.URI; +import java.time.Duration; import java.util.Collection; +import java.util.Optional; +import io.netty.handler.codec.http.cookie.Cookie; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.io.buffer.Buffer; import reactor.io.netty.http.HttpClient; -import reactor.io.netty.http.model.Cookie; -import reactor.io.netty.http.model.Method; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferAllocator; import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseCookie; /** * {@link ClientHttpRequest} implementation for the Reactor Net HTTP client @@ -47,7 +49,7 @@ public class ReactorClientHttpRequest extends AbstractClientHttpRequest { private final URI uri; - private final HttpClient httpClient; + private final HttpClient httpClient; private Flux body; @@ -97,23 +99,23 @@ public class ReactorClientHttpRequest extends AbstractClientHttpRequest { @Override public Mono execute() { - return this.httpClient.request(new Method(httpMethod.toString()), uri.toString(), + return this.httpClient.request(new io.netty.handler.codec.http.HttpMethod(httpMethod.toString()), uri.toString(), channel -> { // see https://github.com/reactor/reactor-io/pull/8 if (body == null) { - channel.headers().removeTransferEncodingChunked(); + channel.removeTransferEncodingChunked(); } return applyBeforeCommit() .after(() -> { getHeaders().entrySet().stream().forEach(e -> channel.headers().set(e.getKey(), e.getValue())); getCookies().values().stream().flatMap(Collection::stream).forEach(cookie -> - channel.addCookie(cookie.getName(), new ReactorCookie(cookie))); + channel.addCookie(cookie.getName(), new NettyCookie(cookie))); return Mono.empty(); }) .after(() -> { if (body != null) { - return channel.writeBufferWith(body); + return channel.send(body); } else { return channel.writeHeaders(); @@ -123,16 +125,12 @@ public class ReactorClientHttpRequest extends AbstractClientHttpRequest { .map(httpChannel -> new ReactorClientHttpResponse(httpChannel, allocator)); } - - /** - * At present Reactor does not provide a {@link Cookie} implementation. - */ - private final static class ReactorCookie extends Cookie { + private final static class NettyCookie implements Cookie { private final HttpCookie httpCookie; - public ReactorCookie(HttpCookie httpCookie) { + public NettyCookie(HttpCookie httpCookie) { this.httpCookie = httpCookie; } @@ -145,6 +143,76 @@ public class ReactorClientHttpRequest extends AbstractClientHttpRequest { public String value() { return this.httpCookie.getValue(); } + + @Override + public boolean isHttpOnly() { + return true; + } + + @Override + public long maxAge() { + return -1; + } + + @Override + public String domain() { + return null; + } + + @Override + public String path() { + return null; + } + + @Override + public void setValue(String value) { + throw new UnsupportedOperationException("Read-Only Cookie"); + } + + @Override + public boolean wrap() { + return false; + } + + @Override + public void setWrap(boolean wrap) { + throw new UnsupportedOperationException("Read-Only Cookie"); + } + + @Override + public void setDomain(String domain) { + throw new UnsupportedOperationException("Read-Only Cookie"); + } + + @Override + public void setPath(String path) { + throw new UnsupportedOperationException("Read-Only Cookie"); + } + + @Override + public void setMaxAge(long maxAge) { + throw new UnsupportedOperationException("Read-Only Cookie"); + } + + @Override + public void setSecure(boolean secure) { + throw new UnsupportedOperationException("Read-Only Cookie"); + } + + @Override + public void setHttpOnly(boolean httpOnly) { + throw new UnsupportedOperationException("Read-Only Cookie"); + } + + @Override + public int compareTo(Cookie o) { + return httpCookie.getName().compareTo(o.name()); + } + + @Override + public boolean isSecure() { + return false; + } } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpResponse.java index fda2260dbc..019256ac77 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpResponse.java @@ -16,11 +16,9 @@ package org.springframework.http.client.reactive; -import java.nio.ByteBuffer; import java.util.Collection; import reactor.core.publisher.Flux; -import reactor.io.buffer.Buffer; import reactor.io.netty.http.HttpChannel; import org.springframework.core.io.buffer.DataBuffer; @@ -42,7 +40,7 @@ public class ReactorClientHttpResponse implements ClientHttpResponse { private final DataBufferAllocator allocator; - private final HttpChannel channel; + private final HttpChannel channel; public ReactorClientHttpResponse(HttpChannel channel, DataBufferAllocator allocator) { @@ -52,7 +50,7 @@ public class ReactorClientHttpResponse implements ClientHttpResponse { @Override public Flux getBody() { - return channel.input().map(b -> allocator.wrap(b.byteBuffer())); + return channel.receive().map(b -> allocator.wrap(b.byteBuffer())); } @Override @@ -64,7 +62,7 @@ public class ReactorClientHttpResponse implements ClientHttpResponse { @Override public HttpStatus getStatusCode() { - return HttpStatus.valueOf(this.channel.responseStatus().getCode()); + return HttpStatus.valueOf(this.channel.responseStatus().code()); } @Override @@ -76,8 +74,8 @@ public class ReactorClientHttpResponse implements ClientHttpResponse { .domain(cookie.domain()) .path(cookie.path()) .maxAge(cookie.maxAge()) - .secure(cookie.secure()) - .httpOnly(cookie.httpOnly()) + .secure(cookie.isSecure()) + .httpOnly(cookie.isHttpOnly()) .build(); result.add(cookie.name(), responseCookie); }); @@ -87,7 +85,7 @@ public class ReactorClientHttpResponse implements ClientHttpResponse { @Override public String toString() { return "ReactorClientHttpResponse{" + - "request=" + this.channel.method().getName() + " " + this.channel.uri() + "," + + "request=" + this.channel.method().name() + " " + this.channel.uri() + "," + "status=" + getStatusCode() + '}'; } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java index a06bf640c3..edf2a51a23 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java @@ -90,7 +90,7 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse { @Override public Mono setBody(Publisher publisher) { - return new WriteWithOperator<>(publisher, writePublisher -> + return new ChannelSendOperator<>(publisher, writePublisher -> applyBeforeCommit().after(() -> setBodyInternal(writePublisher))); } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/WriteWithOperator.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ChannelSendOperator.java similarity index 97% rename from spring-web-reactive/src/main/java/org/springframework/http/server/reactive/WriteWithOperator.java rename to spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ChannelSendOperator.java index 44d15830c8..39ded1e28f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/WriteWithOperator.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ChannelSendOperator.java @@ -37,12 +37,12 @@ import org.springframework.util.Assert; * @author Rossen Stoyanchev * @author Stephane Maldini */ -public class WriteWithOperator extends MonoSource { +public class ChannelSendOperator extends MonoSource { private final Function, Publisher> writeFunction; - public WriteWithOperator(Publisher source, + public ChannelSendOperator(Publisher source, Function, Publisher> writeFunction) { super(source); this.writeFunction = writeFunction; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorHttpHandlerAdapter.java index 7c5442b805..ca3cddb280 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorHttpHandlerAdapter.java @@ -28,7 +28,7 @@ import org.springframework.util.Assert; * @author Stephane Maldini */ public class ReactorHttpHandlerAdapter - implements ChannelFluxHandler> { + implements ChannelFluxHandler { private final HttpHandler httpHandler; @@ -42,7 +42,7 @@ public class ReactorHttpHandlerAdapter } @Override - public Mono apply(HttpChannel channel) { + public Mono apply(HttpChannel channel) { ReactorServerHttpRequest adaptedRequest = new ReactorServerHttpRequest(channel, allocator); ReactorServerHttpResponse adaptedResponse = diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java index cb4f58fc50..2348d995c5 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java @@ -19,10 +19,9 @@ import java.net.URI; import java.net.URISyntaxException; import java.nio.ByteBuffer; +import io.netty.handler.codec.http.cookie.Cookie; import reactor.core.publisher.Flux; -import reactor.io.buffer.Buffer; import reactor.io.netty.http.HttpChannel; -import reactor.io.netty.http.model.Cookie; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferAllocator; @@ -39,11 +38,11 @@ import org.springframework.util.MultiValueMap; */ public class ReactorServerHttpRequest extends AbstractServerHttpRequest { - private final HttpChannel channel; + private final HttpChannel channel; private final DataBufferAllocator allocator; - public ReactorServerHttpRequest(HttpChannel request, + public ReactorServerHttpRequest(HttpChannel request, DataBufferAllocator allocator) { Assert.notNull("'request' must not be null"); Assert.notNull(allocator, "'allocator' must not be null"); @@ -52,13 +51,13 @@ public class ReactorServerHttpRequest extends AbstractServerHttpRequest { } - public HttpChannel getReactorChannel() { + public HttpChannel getReactorChannel() { return this.channel; } @Override public HttpMethod getMethod() { - return HttpMethod.valueOf(this.channel.method().getName()); + return HttpMethod.valueOf(this.channel.method().name()); } @Override @@ -75,17 +74,17 @@ public class ReactorServerHttpRequest extends AbstractServerHttpRequest { @Override protected void initCookies(MultiValueMap cookies) { - for (String name : this.channel.cookies().keySet()) { + for (CharSequence name : this.channel.cookies().keySet()) { for (Cookie cookie : this.channel.cookies().get(name)) { - HttpCookie httpCookie = new HttpCookie(name, cookie.value()); - cookies.add(name, httpCookie); + HttpCookie httpCookie = new HttpCookie(name.toString(), cookie.value()); + cookies.add(name.toString(), httpCookie); } } } @Override public Flux getBody() { - return Flux.from(this.channel.input()).map(bytes -> { + return Flux.from(this.channel.receive()).map(bytes -> { ByteBuffer byteBuffer = bytes.byteBuffer(); return allocator.wrap(byteBuffer); }); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java index 8951934d55..2ee8db19c7 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java @@ -19,13 +19,13 @@ package org.springframework.http.server.reactive; import java.time.Duration; import java.util.Optional; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.cookie.Cookie; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.io.buffer.Buffer; import reactor.io.netty.http.HttpChannel; -import reactor.io.netty.http.model.Cookie; -import reactor.io.netty.http.model.Status; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferAllocator; @@ -41,9 +41,9 @@ import org.springframework.util.Assert; */ public class ReactorServerHttpResponse extends AbstractServerHttpResponse { - private final HttpChannel channel; + private final HttpChannel channel; - public ReactorServerHttpResponse(HttpChannel response, + public ReactorServerHttpResponse(HttpChannel response, DataBufferAllocator allocator) { super(allocator); Assert.notNull("'response' must not be null."); @@ -51,18 +51,18 @@ public class ReactorServerHttpResponse extends AbstractServerHttpResponse { } - public HttpChannel getReactorChannel() { + public HttpChannel getReactorChannel() { return this.channel; } @Override public void setStatusCode(HttpStatus status) { - getReactorChannel().responseStatus(Status.valueOf(status.value())); + getReactorChannel().responseStatus(HttpResponseStatus.valueOf(status.value())); } @Override protected Mono setBodyInternal(Publisher publisher) { - return Mono.from(this.channel.writeWith( + return Mono.from(this.channel.send( Flux.from(publisher).map(buffer -> new Buffer(buffer.asByteBuffer())))); } @@ -79,22 +79,18 @@ public class ReactorServerHttpResponse extends AbstractServerHttpResponse { protected void writeCookies() { for (String name : getCookies().keySet()) { for (ResponseCookie httpCookie : getCookies().get(name)) { - Cookie cookie = new ReactorCookie(httpCookie); + Cookie cookie = new NettyCookie(httpCookie); this.channel.addResponseCookie(name, cookie); } } } - - /** - * At present Reactor does not provide a {@link Cookie} implementation. - */ - private final static class ReactorCookie extends Cookie { + private final static class NettyCookie implements Cookie { private final ResponseCookie httpCookie; - public ReactorCookie(ResponseCookie httpCookie) { + public NettyCookie(ResponseCookie httpCookie) { this.httpCookie = httpCookie; } @@ -109,7 +105,7 @@ public class ReactorServerHttpResponse extends AbstractServerHttpResponse { } @Override - public boolean httpOnly() { + public boolean isHttpOnly() { return this.httpCookie.isHttpOnly(); } @@ -132,7 +128,52 @@ public class ReactorServerHttpResponse extends AbstractServerHttpResponse { } @Override - public boolean secure() { + public void setValue(String value) { + + } + + @Override + public boolean wrap() { + return false; + } + + @Override + public void setWrap(boolean wrap) { + throw new UnsupportedOperationException("Read-Only Cookie"); + } + + @Override + public void setDomain(String domain) { + throw new UnsupportedOperationException("Read-Only Cookie"); + } + + @Override + public void setPath(String path) { + throw new UnsupportedOperationException("Read-Only Cookie"); + } + + @Override + public void setMaxAge(long maxAge) { + throw new UnsupportedOperationException("Read-Only Cookie"); + } + + @Override + public void setSecure(boolean secure) { + throw new UnsupportedOperationException("Read-Only Cookie"); + } + + @Override + public void setHttpOnly(boolean httpOnly) { + throw new UnsupportedOperationException("Read-Only Cookie"); + } + + @Override + public int compareTo(Cookie o) { + return httpCookie.getName().compareTo(o.name()); + } + + @Override + public boolean isSecure() { return this.httpCookie.isSecure(); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java index 4887a6741d..82b1e08dec 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java @@ -33,7 +33,7 @@ public class ReactorHttpServer extends HttpServerSupport private ReactorHttpHandlerAdapter reactorHandler; - private reactor.io.netty.http.HttpServer reactorServer; + private reactor.io.netty.http.HttpServer reactorServer; private DataBufferAllocator allocator = new DefaultDataBufferAllocator(); diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/WriteWithOperatorTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ChannelSendOperatorTests.java similarity index 86% rename from spring-web-reactive/src/test/java/org/springframework/http/server/reactive/WriteWithOperatorTests.java rename to spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ChannelSendOperatorTests.java index 965d6a94e2..70fdc8b20f 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/WriteWithOperatorTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ChannelSendOperatorTests.java @@ -39,7 +39,7 @@ import static org.junit.Assert.*; * @author Stephane Maldini */ @SuppressWarnings("ThrowableResultOfMethodCallIgnored") -public class WriteWithOperatorTests { +public class ChannelSendOperatorTests { private OneByOneAsyncWriter writer; @@ -49,14 +49,14 @@ public class WriteWithOperatorTests { this.writer = new OneByOneAsyncWriter(); } - private Mono writeWithOperator(Publisher source){ - return new WriteWithOperator<>(source, writer::writeWith); + private Mono sendOperator(Publisher source){ + return new ChannelSendOperator<>(source, writer::send); } @Test public void errorBeforeFirstItem() throws Exception { IllegalStateException error = new IllegalStateException("boo"); - Mono completion = Mono.error(error).as(this::writeWithOperator); + Mono completion = Mono.error(error).as(this::sendOperator); Signal signal = completion.materialize().get(); assertNotNull(signal); @@ -65,7 +65,7 @@ public class WriteWithOperatorTests { @Test public void completionBeforeFirstItem() throws Exception { - Mono completion = Flux.empty().as(this::writeWithOperator); + Mono completion = Flux.empty().as(this::sendOperator); Signal signal = completion.materialize().get(); assertNotNull(signal); @@ -77,7 +77,7 @@ public class WriteWithOperatorTests { @Test public void writeOneItem() throws Exception { - Mono completion = Flux.just("one").as(this::writeWithOperator); + Mono completion = Flux.just("one").as(this::sendOperator); Signal signal = completion.materialize().get(); assertNotNull(signal); @@ -92,7 +92,7 @@ public class WriteWithOperatorTests { @Test public void writeMultipleItems() throws Exception { List items = Arrays.asList("one", "two", "three"); - Mono completion = Flux.fromIterable(items).as(this::writeWithOperator); + Mono completion = Flux.fromIterable(items).as(this::sendOperator); Signal signal = completion.materialize().get(); assertNotNull(signal); @@ -115,7 +115,7 @@ public class WriteWithOperatorTests { subscriber.onError(error); } }, subscriber -> new AtomicInteger()); - Mono completion = publisher.as(this::writeWithOperator); + Mono completion = publisher.as(this::sendOperator); Signal signal = completion.materialize().get(); assertNotNull(signal); @@ -138,10 +138,9 @@ public class WriteWithOperatorTests { private Throwable error; - public Publisher writeWith(Publisher publisher) { + public Publisher send(Publisher publisher) { return subscriber -> { - Executors.newSingleThreadScheduledExecutor().schedule( - (Runnable) () -> publisher.subscribe(new WriteSubscriber(subscriber)), + Executors.newSingleThreadScheduledExecutor().schedule(() -> publisher.subscribe(new WriteSubscriber(subscriber)), 50, TimeUnit.MILLISECONDS); }; } From 4ddd9d289562682208c5877ea6db7963c2253aeb Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Fri, 15 Apr 2016 20:04:52 +0100 Subject: [PATCH 290/498] fix change issue on HttpChannel#writeHeaders to sendHeaders --- .../http/client/reactive/ReactorClientHttpRequest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java index b288f72593..6ab0654123 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java @@ -118,7 +118,7 @@ public class ReactorClientHttpRequest extends AbstractClientHttpRequest { return channel.send(body); } else { - return channel.writeHeaders(); + return channel.sendHeaders(); } }); }) From 61136e614e3ceb1d42b1a3eca68a5a95f4f1bb64 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 15 Apr 2016 16:20:55 -0400 Subject: [PATCH 291/498] Fix issue with Cookies in ReactorServerHttpResponse --- .../reactive/ReactorServerHttpResponse.java | 105 ++---------------- 1 file changed, 9 insertions(+), 96 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java index 2ee8db19c7..1578aa0490 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java @@ -16,11 +16,9 @@ package org.springframework.http.server.reactive; -import java.time.Duration; -import java.util.Optional; - import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.cookie.Cookie; +import io.netty.handler.codec.http.cookie.DefaultCookie; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -79,102 +77,17 @@ public class ReactorServerHttpResponse extends AbstractServerHttpResponse { protected void writeCookies() { for (String name : getCookies().keySet()) { for (ResponseCookie httpCookie : getCookies().get(name)) { - Cookie cookie = new NettyCookie(httpCookie); + Cookie cookie = new DefaultCookie(name, httpCookie.getValue()); + if (!httpCookie.getMaxAge().isNegative()) { + cookie.setMaxAge(httpCookie.getMaxAge().getSeconds()); + } + httpCookie.getDomain().ifPresent(cookie::setDomain); + httpCookie.getPath().ifPresent(cookie::setPath); + cookie.setSecure(httpCookie.isSecure()); + cookie.setHttpOnly(httpCookie.isHttpOnly()); this.channel.addResponseCookie(name, cookie); } } } - private final static class NettyCookie implements Cookie { - - private final ResponseCookie httpCookie; - - - public NettyCookie(ResponseCookie httpCookie) { - this.httpCookie = httpCookie; - } - - @Override - public String name() { - return this.httpCookie.getName(); - } - - @Override - public String value() { - return this.httpCookie.getValue(); - } - - @Override - public boolean isHttpOnly() { - return this.httpCookie.isHttpOnly(); - } - - @Override - public long maxAge() { - Duration maxAge = this.httpCookie.getMaxAge(); - return (!maxAge.isNegative() ? maxAge.getSeconds() : -1); - } - - @Override - public String domain() { - Optional domain = this.httpCookie.getDomain(); - return (domain.isPresent() ? domain.get() : null); - } - - @Override - public String path() { - Optional path = this.httpCookie.getPath(); - return (path.isPresent() ? path.get() : null); - } - - @Override - public void setValue(String value) { - - } - - @Override - public boolean wrap() { - return false; - } - - @Override - public void setWrap(boolean wrap) { - throw new UnsupportedOperationException("Read-Only Cookie"); - } - - @Override - public void setDomain(String domain) { - throw new UnsupportedOperationException("Read-Only Cookie"); - } - - @Override - public void setPath(String path) { - throw new UnsupportedOperationException("Read-Only Cookie"); - } - - @Override - public void setMaxAge(long maxAge) { - throw new UnsupportedOperationException("Read-Only Cookie"); - } - - @Override - public void setSecure(boolean secure) { - throw new UnsupportedOperationException("Read-Only Cookie"); - } - - @Override - public void setHttpOnly(boolean httpOnly) { - throw new UnsupportedOperationException("Read-Only Cookie"); - } - - @Override - public int compareTo(Cookie o) { - return httpCookie.getName().compareTo(o.name()); - } - - @Override - public boolean isSecure() { - return this.httpCookie.isSecure(); - } - } } From a7f32cd5280f78848017538cd824fc57122f6ce0 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 15 Apr 2016 11:12:08 -0400 Subject: [PATCH 292/498] Add getQueryParams to ServerHttpRequest --- .../reactive/AbstractServerHttpRequest.java | 38 ++++++++- .../server/reactive/ServerHttpRequest.java | 8 +- .../reactive/MockServerHttpRequest.java | 7 ++ .../reactive/ServerHttpRequestTests.java | 79 +++++++++++++++++++ 4 files changed, 125 insertions(+), 7 deletions(-) create mode 100644 spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ServerHttpRequestTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java index f9301eab6d..b88a3e3b7c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java @@ -17,14 +17,15 @@ package org.springframework.http.server.reactive; import java.net.URI; import java.net.URISyntaxException; -import java.util.List; -import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; -import org.springframework.util.LinkedCaseInsensitiveMap; +import org.springframework.util.CollectionUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; /** * Common base class for {@link ServerHttpRequest} implementations. @@ -33,8 +34,13 @@ import org.springframework.util.MultiValueMap; */ public abstract class AbstractServerHttpRequest implements ServerHttpRequest { + private static final Pattern QUERY_PATTERN = Pattern.compile("([^&=]+)(=?)([^&]+)?"); + + private URI uri; + private MultiValueMap queryParams; + private HttpHeaders headers; private MultiValueMap cookies; @@ -60,6 +66,30 @@ public abstract class AbstractServerHttpRequest implements ServerHttpRequest { */ protected abstract URI initUri() throws URISyntaxException; + @Override + public MultiValueMap getQueryParams() { + if (this.queryParams == null) { + this.queryParams = CollectionUtils.unmodifiableMultiValueMap(initQueryParams()); + } + return this.queryParams; + } + + protected MultiValueMap initQueryParams() { + MultiValueMap queryParams = new LinkedMultiValueMap<>(); + String query = getURI().getRawQuery(); + if (query != null) { + Matcher matcher = QUERY_PATTERN.matcher(query); + while (matcher.find()) { + String name = matcher.group(1); + String eq = matcher.group(2); + String value = matcher.group(3); + value = (value != null ? value : (StringUtils.hasLength(eq) ? "" : null)); + queryParams.add(name, value); + } + } + return queryParams; + } + @Override public HttpHeaders getHeaders() { if (this.headers == null) { @@ -79,7 +109,7 @@ public abstract class AbstractServerHttpRequest implements ServerHttpRequest { @Override public MultiValueMap getCookies() { if (this.cookies == null) { - this.cookies = new LinkedMultiValueMap(); + this.cookies = new LinkedMultiValueMap<>(); initCookies(this.cookies); } return this.cookies; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java index 695e944b0a..377721d566 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java @@ -16,9 +16,6 @@ package org.springframework.http.server.reactive; -import java.util.List; -import java.util.Map; - import org.springframework.http.HttpCookie; import org.springframework.http.HttpRequest; import org.springframework.http.ReactiveHttpInputMessage; @@ -31,6 +28,11 @@ import org.springframework.util.MultiValueMap; */ public interface ServerHttpRequest extends HttpRequest, ReactiveHttpInputMessage { + /** + * Return a read-only map with parsed and decoded query parameter values. + */ + MultiValueMap getQueryParams(); + /** * Return a read-only map of cookies sent by the client. */ diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpRequest.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpRequest.java index 7678536a38..3f7864144b 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpRequest.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpRequest.java @@ -36,6 +36,8 @@ public class MockServerHttpRequest implements ServerHttpRequest { private URI uri; + private MultiValueMap queryParams = new LinkedMultiValueMap<>(); + private HttpHeaders headers = new HttpHeaders(); private MultiValueMap cookies = new LinkedMultiValueMap<>(); @@ -79,6 +81,11 @@ public class MockServerHttpRequest implements ServerHttpRequest { return this.headers; } + @Override + public MultiValueMap getQueryParams() { + return this.queryParams; + } + @Override public MultiValueMap getCookies() { return this.cookies; diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ServerHttpRequestTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ServerHttpRequestTests.java new file mode 100644 index 0000000000..6de90e0cff --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ServerHttpRequestTests.java @@ -0,0 +1,79 @@ +/* + * 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.http.server.reactive; + +import java.util.Arrays; +import java.util.Collections; +import javax.servlet.http.HttpServletRequest; + +import org.junit.Test; +import reactor.core.publisher.Flux; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.util.MultiValueMap; + +import static org.junit.Assert.assertEquals; + +/** + * Unit tests for {@link AbstractServerHttpRequest}. + * + * @author Rossen Stoyanchev + */ +public class ServerHttpRequestTests { + + + @Test + public void queryParamsNone() throws Exception { + MultiValueMap params = createHttpRequest("/path").getQueryParams(); + assertEquals(0, params.size()); + } + + @Test + public void queryParams() throws Exception { + MultiValueMap params = createHttpRequest("/path?a=A&b=B").getQueryParams(); + assertEquals(2, params.size()); + assertEquals(Collections.singletonList("A"), params.get("a")); + assertEquals(Collections.singletonList("B"), params.get("b")); + } + + @Test + public void queryParamsWithMulitpleValues() throws Exception { + MultiValueMap params = createHttpRequest("/path?a=1&a=2").getQueryParams(); + assertEquals(1, params.size()); + assertEquals(Arrays.asList("1", "2"), params.get("a")); + } + + @Test + public void queryParamsWithEmptyValue() throws Exception { + MultiValueMap params = createHttpRequest("/path?a=").getQueryParams(); + assertEquals(1, params.size()); + assertEquals(Collections.singletonList(""), params.get("a")); + } + + @Test + public void queryParamsWithNoValue() throws Exception { + MultiValueMap params = createHttpRequest("/path?a").getQueryParams(); + assertEquals(1, params.size()); + assertEquals(Collections.singletonList(null), params.get("a")); + } + + + private ServerHttpRequest createHttpRequest(String path) { + HttpServletRequest servletRequest = new MockHttpServletRequest("GET", path); + return new ServletServerHttpRequest(servletRequest, Flux.empty()); + } + +} From 09c57118620328156f071f9ffc930da4c7f5fffb Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 15 Apr 2016 11:22:51 -0400 Subject: [PATCH 293/498] Polish ServerHttpRequest Consistent initialization of HttpHeaders and the cookies map. - allow sub-classes to create instance - make unmodifiable --- .../server/reactive/AbstractServerHttpRequest.java | 12 ++++-------- .../server/reactive/ReactorServerHttpRequest.java | 9 +++++++-- .../server/reactive/RxNettyServerHttpRequest.java | 9 +++++++-- .../server/reactive/ServletServerHttpRequest.java | 13 +++++++------ .../server/reactive/UndertowServerHttpRequest.java | 9 +++++++-- 5 files changed, 32 insertions(+), 20 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java index b88a3e3b7c..956e9e391d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java @@ -93,8 +93,7 @@ public abstract class AbstractServerHttpRequest implements ServerHttpRequest { @Override public HttpHeaders getHeaders() { if (this.headers == null) { - this.headers = new HttpHeaders(); - initHeaders(this.headers); + this.headers = HttpHeaders.readOnlyHttpHeaders(initHeaders()); } return this.headers; } @@ -102,15 +101,13 @@ public abstract class AbstractServerHttpRequest implements ServerHttpRequest { /** * Initialize the headers from the underlying request. Invoked lazily on the * first call to {@link #getHeaders()} and then cached. - * @param headers the map to add headers to */ - protected abstract void initHeaders(HttpHeaders headers); + protected abstract HttpHeaders initHeaders(); @Override public MultiValueMap getCookies() { if (this.cookies == null) { - this.cookies = new LinkedMultiValueMap<>(); - initCookies(this.cookies); + this.cookies = CollectionUtils.unmodifiableMultiValueMap(initCookies()); } return this.cookies; } @@ -118,8 +115,7 @@ public abstract class AbstractServerHttpRequest implements ServerHttpRequest { /** * Initialize the cookies from the underlying request. Invoked lazily on the * first access to cookies via {@link #getHeaders()} and then cached. - * @param cookies the map to add cookies to */ - protected abstract void initCookies(MultiValueMap cookies); + protected abstract MultiValueMap initCookies(); } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java index 2348d995c5..5183fca307 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java @@ -29,6 +29,7 @@ import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.util.Assert; +import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; /** @@ -66,20 +67,24 @@ public class ReactorServerHttpRequest extends AbstractServerHttpRequest { } @Override - protected void initHeaders(HttpHeaders headers) { + protected HttpHeaders initHeaders() { + HttpHeaders headers = new HttpHeaders(); for (String name : this.channel.headers().names()) { headers.put(name, this.channel.headers().getAll(name)); } + return headers; } @Override - protected void initCookies(MultiValueMap cookies) { + protected MultiValueMap initCookies() { + MultiValueMap cookies = new LinkedMultiValueMap<>(); for (CharSequence name : this.channel.cookies().keySet()) { for (Cookie cookie : this.channel.cookies().get(name)) { HttpCookie httpCookie = new HttpCookie(name.toString(), cookie.value()); cookies.add(name.toString(), httpCookie); } } + return cookies; } @Override diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java index e3b1562005..dab7c23f83 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java @@ -32,6 +32,7 @@ import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.util.Assert; +import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; /** @@ -70,20 +71,24 @@ public class RxNettyServerHttpRequest extends AbstractServerHttpRequest { } @Override - protected void initHeaders(HttpHeaders headers) { + protected HttpHeaders initHeaders() { + HttpHeaders headers = new HttpHeaders(); for (String name : this.request.getHeaderNames()) { headers.put(name, this.request.getAllHeaderValues(name)); } + return headers; } @Override - protected void initCookies(MultiValueMap cookies) { + protected MultiValueMap initCookies() { + MultiValueMap cookies = new LinkedMultiValueMap<>(); for (String name : this.request.getCookies().keySet()) { for (Cookie cookie : this.request.getCookies().get(name)) { HttpCookie httpCookie = new HttpCookie(name, cookie.value()); cookies.add(name, httpCookie); } } + return cookies; } @Override diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java index 952a93cc7c..9cd8586e7d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java @@ -24,8 +24,6 @@ import java.util.Map; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -36,6 +34,7 @@ import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.util.Assert; import org.springframework.util.LinkedCaseInsensitiveMap; +import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; @@ -46,8 +45,6 @@ import org.springframework.util.StringUtils; */ public class ServletServerHttpRequest extends AbstractServerHttpRequest { - private static final Log logger = LogFactory.getLog(ServletServerHttpRequest.class); - private final HttpServletRequest request; private final Flux requestBodyPublisher; @@ -81,7 +78,8 @@ public class ServletServerHttpRequest extends AbstractServerHttpRequest { } @Override - protected void initHeaders(HttpHeaders headers) { + protected HttpHeaders initHeaders() { + HttpHeaders headers = new HttpHeaders(); for (Enumeration names = getServletRequest().getHeaderNames(); names.hasMoreElements(); ) { String name = (String) names.nextElement(); for (Enumeration values = getServletRequest().getHeaders(name); values.hasMoreElements(); ) { @@ -112,10 +110,12 @@ public class ServletServerHttpRequest extends AbstractServerHttpRequest { headers.setContentLength(contentLength); } } + return headers; } @Override - protected void initCookies(MultiValueMap httpCookies) { + protected MultiValueMap initCookies() { + MultiValueMap httpCookies = new LinkedMultiValueMap<>(); Cookie[] cookies = this.request.getCookies(); if (cookies != null) { for (Cookie cookie : cookies) { @@ -124,6 +124,7 @@ public class ServletServerHttpRequest extends AbstractServerHttpRequest { httpCookies.add(name, httpCookie); } } + return httpCookies; } @Override diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java index cebcc776ed..51ef11d445 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java @@ -30,6 +30,7 @@ import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.util.Assert; +import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; /** @@ -70,19 +71,23 @@ public class UndertowServerHttpRequest extends AbstractServerHttpRequest { } @Override - protected void initHeaders(HttpHeaders headers) { + protected HttpHeaders initHeaders() { + HttpHeaders headers = new HttpHeaders(); for (HeaderValues values : this.getUndertowExchange().getRequestHeaders()) { headers.put(values.getHeaderName().toString(), values); } + return headers; } @Override - protected void initCookies(MultiValueMap cookies) { + protected MultiValueMap initCookies() { + MultiValueMap cookies = new LinkedMultiValueMap<>(); for (String name : this.exchange.getRequestCookies().keySet()) { Cookie cookie = this.exchange.getRequestCookies().get(name); HttpCookie httpCookie = new HttpCookie(name, cookie.getValue()); cookies.add(name, httpCookie); } + return cookies; } @Override From 641c6428e88bbdd30fac591bf33b0e94f5751d6a Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 15 Apr 2016 14:53:33 -0400 Subject: [PATCH 294/498] Add ContentTypeResolver strategy A starting point for an alternative to the existing ContentNegotiationStrategy but for use with ServerWebExchange. --- .../reactive/accept/ContentTypeResolver.java | 42 ++++++++++ .../FileExtensionContentTypeResolver.java | 45 ++++++++++ .../accept/HeaderContentTypeResolver.java | 47 +++++++++++ .../web/reactive/accept/package-info.java | 5 ++ .../HeaderContentTypeResolverTests.java | 82 +++++++++++++++++++ 5 files changed, 221 insertions(+) create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/ContentTypeResolver.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/FileExtensionContentTypeResolver.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/HeaderContentTypeResolver.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/package-info.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/accept/HeaderContentTypeResolverTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/ContentTypeResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/ContentTypeResolver.java new file mode 100644 index 0000000000..c91c494a62 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/ContentTypeResolver.java @@ -0,0 +1,42 @@ +/* + * 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.web.reactive.accept; + +import java.util.List; + +import org.springframework.http.MediaType; +import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.server.ServerWebExchange; + +/** + * + * @author Rossen Stoyanchev + */ +public interface ContentTypeResolver { + + /** + * Resolve the given request to a list of requested media types. The returned + * list is ordered by specificity first and by quality parameter second. + * + * @param exchange the current exchange + * @return the requested media types or an empty list + * + * @throws HttpMediaTypeNotAcceptableException if the requested media + * types cannot be parsed + */ + List resolveMediaTypes(ServerWebExchange exchange) throws HttpMediaTypeNotAcceptableException; + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/FileExtensionContentTypeResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/FileExtensionContentTypeResolver.java new file mode 100644 index 0000000000..8343c8da7c --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/FileExtensionContentTypeResolver.java @@ -0,0 +1,45 @@ +/* + * 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.web.reactive.accept; + +import java.util.List; + +import org.springframework.http.MediaType; + +/** + * An extension of {@link ContentTypeResolver} for a resolver that uses file + * extensions and can expose file extension mappings. + * + * @author Rossen Stoyanchev + */ +public interface FileExtensionContentTypeResolver extends ContentTypeResolver { + + /** + * Resolve the given media type to a list of path extensions. + * + * @param mediaType the media type to resolve + * @return a list of extensions or an empty list, never {@code null} + */ + List getFileExtensions(MediaType mediaType); + + /** + * Return all registered file extensions. + * @return a list of extensions or an empty list, never {@code null} + */ + List getAllFileExtensions(); + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/HeaderContentTypeResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/HeaderContentTypeResolver.java new file mode 100644 index 0000000000..de6caac150 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/HeaderContentTypeResolver.java @@ -0,0 +1,47 @@ +/* + * 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.web.reactive.accept; + +import java.util.List; + +import org.springframework.http.InvalidMediaTypeException; +import org.springframework.http.MediaType; +import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.server.ServerWebExchange; + +/** + * A {@link ContentTypeResolver} that checks the 'Accept' request header. + * + * @author Rossen Stoyanchev + */ +public class HeaderContentTypeResolver implements ContentTypeResolver { + + @Override + public List resolveMediaTypes(ServerWebExchange exchange) + throws HttpMediaTypeNotAcceptableException { + + try { + List mediaTypes = exchange.getRequest().getHeaders().getAccept(); + MediaType.sortBySpecificityAndQuality(mediaTypes); + return mediaTypes; + } + catch (InvalidMediaTypeException ex) { + String value = exchange.getRequest().getHeaders().getFirst("Accept"); + throw new HttpMediaTypeNotAcceptableException( + "Could not parse 'Accept' header [" + value + "]: " + ex.getMessage()); + } + } +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/package-info.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/package-info.java new file mode 100644 index 0000000000..428353ad05 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/package-info.java @@ -0,0 +1,5 @@ +/** + * This package provides support for various strategies to resolve the requested + * content type for a given request. + */ +package org.springframework.web.reactive.accept; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/accept/HeaderContentTypeResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/accept/HeaderContentTypeResolverTests.java new file mode 100644 index 0000000000..9b2288b2c4 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/accept/HeaderContentTypeResolverTests.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.web.reactive.accept; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.WebSessionManager; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; + +/** + * Unit tests for {@link HeaderContentTypeResolver}. + * + * @author Rossen Stoyanchev + */ +public class HeaderContentTypeResolverTests { + + private HeaderContentTypeResolver resolver; + + + @Before + public void setup() { + this.resolver = new HeaderContentTypeResolver(); + } + + + @Test + public void resolveMediaTypes() throws Exception { + ServerWebExchange exchange = createExchange("text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c"); + List mediaTypes = this.resolver.resolveMediaTypes(exchange); + + assertEquals(4, mediaTypes.size()); + assertEquals("text/html", mediaTypes.get(0).toString()); + assertEquals("text/x-c", mediaTypes.get(1).toString()); + assertEquals("text/x-dvi;q=0.8", mediaTypes.get(2).toString()); + assertEquals("text/plain;q=0.5", mediaTypes.get(3).toString()); + } + + @Test(expected=HttpMediaTypeNotAcceptableException.class) + public void resolveMediaTypesParseError() throws Exception { + ServerWebExchange exchange = createExchange("textplain; q=0.5"); + this.resolver.resolveMediaTypes(exchange); + } + + + private ServerWebExchange createExchange(String accept) throws URISyntaxException { + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); + if (accept != null) { + request.getHeaders().add("Accept", accept); + } + WebSessionManager sessionManager = mock(WebSessionManager.class); + return new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); + } + +} From b2ca216498f699fb4f3d2e56800024b6bc1f826b Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 15 Apr 2016 16:09:59 -0400 Subject: [PATCH 295/498] Add RequestCondition hierarchy A pretty complete equivalent to the same in spring-webmvc except for CORS checks, and custom HTTP methods. Another notable difference is that the "params" condition works on query params strictly. --- .../AbstractMediaTypeExpression.java | 115 ++++ .../AbstractNameValueExpression.java | 127 ++++ .../condition/AbstractRequestCondition.java | 86 +++ .../condition/CompositeRequestCondition.java | 184 ++++++ .../condition/ConsumesRequestCondition.java | 234 +++++++ .../condition/HeadersRequestCondition.java | 167 +++++ .../result/condition/MediaTypeExpression.java | 35 + .../result/condition/NameValueExpression.java | 35 + .../condition/ParamsRequestCondition.java | 152 +++++ .../condition/PatternsRequestCondition.java | 285 +++++++++ .../condition/ProducesRequestCondition.java | 318 +++++++++ .../result/condition/RequestCondition.java | 68 ++ .../condition/RequestConditionHolder.java | 139 ++++ .../RequestMethodsRequestCondition.java | 162 +++++ .../result/condition/package-info.java | 6 + .../result/method/RequestMappingInfo.java | 602 ++++++++++++++++++ .../CompositeRequestConditionTests.java | 151 +++++ .../ConsumesRequestConditionTests.java | 210 ++++++ .../HeadersRequestConditionTests.java | 173 +++++ .../ParamsRequestConditionTests.java | 145 +++++ .../PatternsRequestConditionTests.java | 234 +++++++ .../ProducesRequestConditionTests.java | 327 ++++++++++ .../RequestConditionHolderTests.java | 141 ++++ .../condition/RequestMappingInfoTests.java | 355 +++++++++++ .../RequestMethodsRequestConditionTests.java | 157 +++++ 25 files changed, 4608 insertions(+) create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/AbstractMediaTypeExpression.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/AbstractNameValueExpression.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/AbstractRequestCondition.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/CompositeRequestCondition.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/ConsumesRequestCondition.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/HeadersRequestCondition.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/MediaTypeExpression.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/NameValueExpression.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/ParamsRequestCondition.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/PatternsRequestCondition.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/ProducesRequestCondition.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/RequestCondition.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/RequestConditionHolder.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/RequestMethodsRequestCondition.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/package-info.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfo.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/CompositeRequestConditionTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/ConsumesRequestConditionTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/HeadersRequestConditionTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/ParamsRequestConditionTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/PatternsRequestConditionTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/ProducesRequestConditionTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/RequestConditionHolderTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/RequestMappingInfoTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/RequestMethodsRequestConditionTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/AbstractMediaTypeExpression.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/AbstractMediaTypeExpression.java new file mode 100644 index 0000000000..8ae3974257 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/AbstractMediaTypeExpression.java @@ -0,0 +1,115 @@ +/* + * 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.web.reactive.result.condition; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.http.MediaType; +import org.springframework.web.HttpMediaTypeException; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.server.ServerWebExchange; + +/** + * Supports media type expressions as described in: + * {@link RequestMapping#consumes()} and {@link RequestMapping#produces()}. + * + * @author Rossen Stoyanchev + */ +abstract class AbstractMediaTypeExpression implements Comparable, MediaTypeExpression { + + protected final Log logger = LogFactory.getLog(getClass()); + + private final MediaType mediaType; + + private final boolean isNegated; + + + AbstractMediaTypeExpression(String expression) { + if (expression.startsWith("!")) { + this.isNegated = true; + expression = expression.substring(1); + } + else { + this.isNegated = false; + } + this.mediaType = MediaType.parseMediaType(expression); + } + + AbstractMediaTypeExpression(MediaType mediaType, boolean negated) { + this.mediaType = mediaType; + this.isNegated = negated; + } + + + @Override + public MediaType getMediaType() { + return this.mediaType; + } + + @Override + public boolean isNegated() { + return this.isNegated; + } + + + public final boolean match(ServerWebExchange exchange) { + try { + boolean match = matchMediaType(exchange); + return (!this.isNegated == match); + } + catch (HttpMediaTypeException ex) { + return false; + } + } + + protected abstract boolean matchMediaType(ServerWebExchange exchange) throws HttpMediaTypeException; + + + @Override + public int compareTo(AbstractMediaTypeExpression other) { + return MediaType.SPECIFICITY_COMPARATOR.compare(this.getMediaType(), other.getMediaType()); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj != null && getClass() == obj.getClass()) { + AbstractMediaTypeExpression other = (AbstractMediaTypeExpression) obj; + return (this.mediaType.equals(other.mediaType) && this.isNegated == other.isNegated); + } + return false; + } + + @Override + public int hashCode() { + return this.mediaType.hashCode(); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + if (this.isNegated) { + builder.append('!'); + } + builder.append(this.mediaType.toString()); + return builder.toString(); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/AbstractNameValueExpression.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/AbstractNameValueExpression.java new file mode 100644 index 0000000000..9fbbdb2ef7 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/AbstractNameValueExpression.java @@ -0,0 +1,127 @@ +/* + * 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.web.reactive.result.condition; + +import org.springframework.web.server.ServerWebExchange; + +/** + * Supports "name=value" style expressions as described in: + * {@link org.springframework.web.bind.annotation.RequestMapping#params()} and + * {@link org.springframework.web.bind.annotation.RequestMapping#headers()}. + * + * @author Rossen Stoyanchev + */ +abstract class AbstractNameValueExpression implements NameValueExpression { + + protected final String name; + + protected final T value; + + protected final boolean isNegated; + + AbstractNameValueExpression(String expression) { + int separator = expression.indexOf('='); + if (separator == -1) { + this.isNegated = expression.startsWith("!"); + this.name = isNegated ? expression.substring(1) : expression; + this.value = null; + } + else { + this.isNegated = (separator > 0) && (expression.charAt(separator - 1) == '!'); + this.name = isNegated ? expression.substring(0, separator - 1) : expression.substring(0, separator); + this.value = parseValue(expression.substring(separator + 1)); + } + } + + @Override + public String getName() { + return this.name; + } + + @Override + public T getValue() { + return this.value; + } + + @Override + public boolean isNegated() { + return this.isNegated; + } + + protected abstract boolean isCaseSensitiveName(); + + protected abstract T parseValue(String valueExpression); + + public final boolean match(ServerWebExchange exchange) { + boolean isMatch; + if (this.value != null) { + isMatch = matchValue(exchange); + } + else { + isMatch = matchName(exchange); + } + return this.isNegated != isMatch; + } + + protected abstract boolean matchName(ServerWebExchange exchange); + + protected abstract boolean matchValue(ServerWebExchange exchange); + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj != null && obj instanceof AbstractNameValueExpression) { + AbstractNameValueExpression other = (AbstractNameValueExpression) obj; + String thisName = isCaseSensitiveName() ? this.name : this.name.toLowerCase(); + String otherName = isCaseSensitiveName() ? other.name : other.name.toLowerCase(); + return ((thisName.equalsIgnoreCase(otherName)) && + (this.value != null ? this.value.equals(other.value) : other.value == null) && + this.isNegated == other.isNegated); + } + return false; + } + + @Override + public int hashCode() { + int result = isCaseSensitiveName() ? name.hashCode() : name.toLowerCase().hashCode(); + result = 31 * result + (value != null ? value.hashCode() : 0); + result = 31 * result + (isNegated ? 1 : 0); + return result; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + if (value != null) { + builder.append(name); + if (isNegated) { + builder.append('!'); + } + builder.append('='); + builder.append(value); + } + else { + if (isNegated) { + builder.append('!'); + } + builder.append(name); + } + return builder.toString(); + } +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/AbstractRequestCondition.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/AbstractRequestCondition.java new file mode 100644 index 0000000000..8b6edb5b68 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/AbstractRequestCondition.java @@ -0,0 +1,86 @@ +/* + * 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.web.reactive.result.condition; + +import java.util.Collection; +import java.util.Iterator; + +/** + * A base class for {@link RequestCondition} types providing implementations of + * {@link #equals(Object)}, {@link #hashCode()}, and {@link #toString()}. + * + * @author Rossen Stoyanchev + */ +public abstract class AbstractRequestCondition> + implements RequestCondition { + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj != null && getClass() == obj.getClass()) { + AbstractRequestCondition other = (AbstractRequestCondition) obj; + return getContent().equals(other.getContent()); + } + return false; + } + + @Override + public int hashCode() { + return getContent().hashCode(); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("["); + for (Iterator iterator = getContent().iterator(); iterator.hasNext();) { + Object expression = iterator.next(); + builder.append(expression.toString()); + if (iterator.hasNext()) { + builder.append(getToStringInfix()); + } + } + builder.append("]"); + return builder.toString(); + } + + /** + * Indicates whether this condition is empty, i.e. whether or not it + * contains any discrete items. + * @return {@code true} if empty; {@code false} otherwise + */ + public boolean isEmpty() { + return getContent().isEmpty(); + } + + + /** + * Return the discrete items a request condition is composed of. + *

    For example URL patterns, HTTP request methods, param expressions, etc. + * @return a collection of objects, never {@code null} + */ + protected abstract Collection getContent(); + + /** + * The notation to use when printing discrete items of content. + *

    For example {@code " || "} for URL patterns or {@code " && "} + * for param expressions. + */ + protected abstract String getToStringInfix(); + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/CompositeRequestCondition.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/CompositeRequestCondition.java new file mode 100644 index 0000000000..8d70918a55 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/CompositeRequestCondition.java @@ -0,0 +1,184 @@ +/* + * 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.web.reactive.result.condition; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.web.server.ServerWebExchange; + +/** + * Implements the {@link RequestCondition} contract by delegating to multiple + * {@code RequestCondition} types and using a logical conjunction (' && ') to + * ensure all conditions match a given request. + * + *

    When {@code CompositeRequestCondition} instances are combined or compared + * they are expected to (a) contain the same number of conditions and (b) that + * conditions in the respective index are of the same type. It is acceptable to + * provide {@code null} conditions or no conditions at all to the constructor. + * + * @author Rossen Stoyanchev + */ +public class CompositeRequestCondition extends AbstractRequestCondition { + + private final RequestConditionHolder[] requestConditions; + + + /** + * Create an instance with 0 or more {@code RequestCondition} types. It is + * important to create {@code CompositeRequestCondition} instances with the + * same number of conditions so they may be compared and combined. + * It is acceptable to provide {@code null} conditions. + */ + public CompositeRequestCondition(RequestCondition... requestConditions) { + this.requestConditions = wrap(requestConditions); + } + + private CompositeRequestCondition(RequestConditionHolder[] requestConditions) { + this.requestConditions = requestConditions; + } + + + private RequestConditionHolder[] wrap(RequestCondition... rawConditions) { + RequestConditionHolder[] wrappedConditions = new RequestConditionHolder[rawConditions.length]; + for (int i = 0; i < rawConditions.length; i++) { + wrappedConditions[i] = new RequestConditionHolder(rawConditions[i]); + } + return wrappedConditions; + } + + /** + * Whether this instance contains 0 conditions or not. + */ + public boolean isEmpty() { + return ObjectUtils.isEmpty(this.requestConditions); + } + + /** + * Return the underlying conditions, possibly empty but never {@code null}. + */ + public List> getConditions() { + return unwrap(); + } + + private List> unwrap() { + List> result = new ArrayList<>(); + for (RequestConditionHolder holder : this.requestConditions) { + result.add(holder.getCondition()); + } + return result; + } + + @Override + protected Collection getContent() { + return (isEmpty()) ? Collections.emptyList() : getConditions(); + } + + @Override + protected String getToStringInfix() { + return " && "; + } + + private int getLength() { + return this.requestConditions.length; + } + + /** + * If one instance is empty, return the other. + * If both instances have conditions, combine the individual conditions + * after ensuring they are of the same type and number. + */ + @Override + public CompositeRequestCondition combine(CompositeRequestCondition other) { + if (isEmpty() && other.isEmpty()) { + return this; + } + else if (other.isEmpty()) { + return this; + } + else if (isEmpty()) { + return other; + } + else { + assertNumberOfConditions(other); + RequestConditionHolder[] combinedConditions = new RequestConditionHolder[getLength()]; + for (int i = 0; i < getLength(); i++) { + combinedConditions[i] = this.requestConditions[i].combine(other.requestConditions[i]); + } + return new CompositeRequestCondition(combinedConditions); + } + } + + private void assertNumberOfConditions(CompositeRequestCondition other) { + Assert.isTrue(getLength() == other.getLength(), + "Cannot combine CompositeRequestConditions with a different number of conditions. " + + ObjectUtils.nullSafeToString(this.requestConditions) + " and " + + ObjectUtils.nullSafeToString(other.requestConditions)); + } + + /** + * Delegate to all contained conditions to match the request and return the + * resulting "matching" condition instances. + *

    An empty {@code CompositeRequestCondition} matches to all requests. + */ + @Override + public CompositeRequestCondition getMatchingCondition(ServerWebExchange exchange) { + if (isEmpty()) { + return this; + } + RequestConditionHolder[] matchingConditions = new RequestConditionHolder[getLength()]; + for (int i = 0; i < getLength(); i++) { + matchingConditions[i] = this.requestConditions[i].getMatchingCondition(exchange); + if (matchingConditions[i] == null) { + return null; + } + } + return new CompositeRequestCondition(matchingConditions); + } + + /** + * If one instance is empty, the other "wins". If both instances have + * conditions, compare them in the order in which they were provided. + */ + @Override + public int compareTo(CompositeRequestCondition other, ServerWebExchange exchange) { + if (isEmpty() && other.isEmpty()) { + return 0; + } + else if (isEmpty()) { + return 1; + } + else if (other.isEmpty()) { + return -1; + } + else { + assertNumberOfConditions(other); + for (int i = 0; i < getLength(); i++) { + int result = this.requestConditions[i].compareTo(other.requestConditions[i], exchange); + if (result != 0) { + return result; + } + } + return 0; + } + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/ConsumesRequestCondition.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/ConsumesRequestCondition.java new file mode 100644 index 0000000000..02c5907e2f --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/ConsumesRequestCondition.java @@ -0,0 +1,234 @@ +/* + * 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.web.reactive.result.condition; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import org.springframework.http.InvalidMediaTypeException; +import org.springframework.http.MediaType; +import org.springframework.web.HttpMediaTypeNotSupportedException; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.server.ServerWebExchange; + +/** + * A logical disjunction (' || ') request condition to match a request's + * 'Content-Type' header to a list of media type expressions. Two kinds of + * media type expressions are supported, which are described in + * {@link RequestMapping#consumes()} and {@link RequestMapping#headers()} + * where the header name is 'Content-Type'. Regardless of which syntax is + * used, the semantics are the same. + * + * @author Rossen Stoyanchev + */ +public final class ConsumesRequestCondition extends AbstractRequestCondition { + +// private final static ConsumesRequestCondition PRE_FLIGHT_MATCH = new ConsumesRequestCondition(); + + + private final List expressions; + + + /** + * Creates a new instance from 0 or more "consumes" expressions. + * @param consumes expressions with the syntax described in + * {@link RequestMapping#consumes()}; if 0 expressions are provided, + * the condition will match to every request + */ + public ConsumesRequestCondition(String... consumes) { + this(consumes, null); + } + + /** + * Creates a new instance with "consumes" and "header" expressions. + * "Header" expressions where the header name is not 'Content-Type' or have + * no header value defined are ignored. If 0 expressions are provided in + * total, the condition will match to every request + * @param consumes as described in {@link RequestMapping#consumes()} + * @param headers as described in {@link RequestMapping#headers()} + */ + public ConsumesRequestCondition(String[] consumes, String[] headers) { + this(parseExpressions(consumes, headers)); + } + + /** + * Private constructor accepting parsed media type expressions. + */ + private ConsumesRequestCondition(Collection expressions) { + this.expressions = new ArrayList<>(expressions); + Collections.sort(this.expressions); + } + + + private static Set parseExpressions(String[] consumes, String[] headers) { + Set result = new LinkedHashSet<>(); + if (headers != null) { + for (String header : headers) { + HeadersRequestCondition.HeaderExpression expr = new HeadersRequestCondition.HeaderExpression(header); + if ("Content-Type".equalsIgnoreCase(expr.name)) { + for (MediaType mediaType : MediaType.parseMediaTypes(expr.value)) { + result.add(new ConsumeMediaTypeExpression(mediaType, expr.isNegated)); + } + } + } + } + if (consumes != null) { + for (String consume : consumes) { + result.add(new ConsumeMediaTypeExpression(consume)); + } + } + return result; + } + + + /** + * Return the contained MediaType expressions. + */ + public Set getExpressions() { + return new LinkedHashSet<>(this.expressions); + } + + /** + * Returns the media types for this condition excluding negated expressions. + */ + public Set getConsumableMediaTypes() { + Set result = new LinkedHashSet<>(); + for (ConsumeMediaTypeExpression expression : this.expressions) { + if (!expression.isNegated()) { + result.add(expression.getMediaType()); + } + } + return result; + } + + /** + * Whether the condition has any media type expressions. + */ + public boolean isEmpty() { + return this.expressions.isEmpty(); + } + + @Override + protected Collection getContent() { + return this.expressions; + } + + @Override + protected String getToStringInfix() { + return " || "; + } + + /** + * Returns the "other" instance if it has any expressions; returns "this" + * instance otherwise. Practically that means a method-level "consumes" + * overrides a type-level "consumes" condition. + */ + @Override + public ConsumesRequestCondition combine(ConsumesRequestCondition other) { + return !other.expressions.isEmpty() ? other : this; + } + + /** + * Checks if any of the contained media type expressions match the given + * request 'Content-Type' header and returns an instance that is guaranteed + * to contain matching expressions only. The match is performed via + * {@link MediaType#includes(MediaType)}. + * @param exchange the current exchange + * @return the same instance if the condition contains no expressions; + * or a new condition with matching expressions only; + * or {@code null} if no expressions match. + */ + @Override + public ConsumesRequestCondition getMatchingCondition(ServerWebExchange exchange) { +// if (CorsUtils.isPreFlightRequest(request)) { +// return PRE_FLIGHT_MATCH; +// } + if (isEmpty()) { + return this; + } + Set result = new LinkedHashSet<>(expressions); + for (Iterator iterator = result.iterator(); iterator.hasNext();) { + ConsumeMediaTypeExpression expression = iterator.next(); + if (!expression.match(exchange)) { + iterator.remove(); + } + } + return (result.isEmpty()) ? null : new ConsumesRequestCondition(result); + } + + /** + * Returns: + *

      + *
    • 0 if the two conditions have the same number of expressions + *
    • Less than 0 if "this" has more or more specific media type expressions + *
    • Greater than 0 if "other" has more or more specific media type expressions + *
    + *

    It is assumed that both instances have been obtained via + * {@link #getMatchingCondition(ServerWebExchange)} and each instance contains + * the matching consumable media type expression only or is otherwise empty. + */ + @Override + public int compareTo(ConsumesRequestCondition other, ServerWebExchange exchange) { + if (this.expressions.isEmpty() && other.expressions.isEmpty()) { + return 0; + } + else if (this.expressions.isEmpty()) { + return 1; + } + else if (other.expressions.isEmpty()) { + return -1; + } + else { + return this.expressions.get(0).compareTo(other.expressions.get(0)); + } + } + + + /** + * Parses and matches a single media type expression to a request's 'Content-Type' header. + */ + static class ConsumeMediaTypeExpression extends AbstractMediaTypeExpression { + + ConsumeMediaTypeExpression(String expression) { + super(expression); + } + + ConsumeMediaTypeExpression(MediaType mediaType, boolean negated) { + super(mediaType, negated); + } + + @Override + protected boolean matchMediaType(ServerWebExchange exchange) throws HttpMediaTypeNotSupportedException { + try { + MediaType contentType = exchange.getRequest().getHeaders().getContentType(); + contentType = (contentType != null ? contentType : MediaType.APPLICATION_OCTET_STREAM); + return getMediaType().includes(contentType); + } + catch (InvalidMediaTypeException ex) { + throw new HttpMediaTypeNotSupportedException("Can't parse Content-Type [" + + exchange.getRequest().getHeaders().getFirst("Content-Type") + + "]: " + ex.getMessage()); + } + } + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/HeadersRequestCondition.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/HeadersRequestCondition.java new file mode 100644 index 0000000000..14c97a8b41 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/HeadersRequestCondition.java @@ -0,0 +1,167 @@ +/* + * 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.web.reactive.result.condition; + +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.server.ServerWebExchange; + +/** + * A logical conjunction (' && ') request condition that matches a request against + * a set of header expressions with syntax defined in {@link RequestMapping#headers()}. + * + *

    Expressions passed to the constructor with header names 'Accept' or + * 'Content-Type' are ignored. See {@link ConsumesRequestCondition} and + * {@link ProducesRequestCondition} for those. + * + * @author Rossen Stoyanchev + */ +public final class HeadersRequestCondition extends AbstractRequestCondition { + +// private final static HeadersRequestCondition PRE_FLIGHT_MATCH = new HeadersRequestCondition(); + + + private final Set expressions; + + + /** + * Create a new instance from the given header expressions. Expressions with + * header names 'Accept' or 'Content-Type' are ignored. See {@link ConsumesRequestCondition} + * and {@link ProducesRequestCondition} for those. + * @param headers media type expressions with syntax defined in {@link RequestMapping#headers()}; + * if 0, the condition will match to every request + */ + public HeadersRequestCondition(String... headers) { + this(parseExpressions(headers)); + } + + private HeadersRequestCondition(Collection conditions) { + this.expressions = Collections.unmodifiableSet(new LinkedHashSet<>(conditions)); + } + + + private static Collection parseExpressions(String... headers) { + Set expressions = new LinkedHashSet(); + if (headers != null) { + for (String header : headers) { + HeaderExpression expr = new HeaderExpression(header); + if ("Accept".equalsIgnoreCase(expr.name) || "Content-Type".equalsIgnoreCase(expr.name)) { + continue; + } + expressions.add(expr); + } + } + return expressions; + } + + /** + * Return the contained request header expressions. + */ + public Set> getExpressions() { + return new LinkedHashSet<>(this.expressions); + } + + @Override + protected Collection getContent() { + return this.expressions; + } + + @Override + protected String getToStringInfix() { + return " && "; + } + + /** + * Returns a new instance with the union of the header expressions + * from "this" and the "other" instance. + */ + @Override + public HeadersRequestCondition combine(HeadersRequestCondition other) { + Set set = new LinkedHashSet<>(this.expressions); + set.addAll(other.expressions); + return new HeadersRequestCondition(set); + } + + /** + * Returns "this" instance if the request matches all expressions; + * or {@code null} otherwise. + */ + @Override + public HeadersRequestCondition getMatchingCondition(ServerWebExchange exchange) { +// if (CorsUtils.isPreFlightRequest(request)) { +// return PRE_FLIGHT_MATCH; +// } + for (HeaderExpression expression : expressions) { + if (!expression.match(exchange)) { + return null; + } + } + return this; + } + + /** + * Returns: + *

      + *
    • 0 if the two conditions have the same number of header expressions + *
    • Less than 0 if "this" instance has more header expressions + *
    • Greater than 0 if the "other" instance has more header expressions + *
    + *

    It is assumed that both instances have been obtained via + * {@link #getMatchingCondition(ServerWebExchange)} and each instance + * contains the matching header expression only or is otherwise empty. + */ + @Override + public int compareTo(HeadersRequestCondition other, ServerWebExchange exchange) { + return other.expressions.size() - this.expressions.size(); + } + + + /** + * Parses and matches a single header expression to a request. + */ + static class HeaderExpression extends AbstractNameValueExpression { + + public HeaderExpression(String expression) { + super(expression); + } + + @Override + protected boolean isCaseSensitiveName() { + return false; + } + + @Override + protected String parseValue(String valueExpression) { + return valueExpression; + } + + @Override + protected boolean matchName(ServerWebExchange exchange) { + return exchange.getRequest().getHeaders().get(name) != null; + } + + @Override + protected boolean matchValue(ServerWebExchange exchange) { + return value.equals(exchange.getRequest().getHeaders().getFirst(name)); + } + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/MediaTypeExpression.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/MediaTypeExpression.java new file mode 100644 index 0000000000..6e9a3265d7 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/MediaTypeExpression.java @@ -0,0 +1,35 @@ +/* + * 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.web.reactive.result.condition; + +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.RequestMapping; + +/** + * A contract for media type expressions (e.g. "text/plain", "!text/plain") as + * defined in the {@code @RequestMapping} annotation for "consumes" and + * "produces" conditions. + * + * @author Rossen Stoyanchev + */ +public interface MediaTypeExpression { + + MediaType getMediaType(); + + boolean isNegated(); + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/NameValueExpression.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/NameValueExpression.java new file mode 100644 index 0000000000..8818330c02 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/NameValueExpression.java @@ -0,0 +1,35 @@ +/* + * 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.web.reactive.result.condition; + +import org.springframework.web.bind.annotation.RequestMapping; + +/** + * A contract for {@code "name!=value"} style expression used to specify request + * parameters and request header conditions in {@code @RequestMapping}. + * + * @author Rossen Stoyanchev + */ +public interface NameValueExpression { + + String getName(); + + T getValue(); + + boolean isNegated(); + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/ParamsRequestCondition.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/ParamsRequestCondition.java new file mode 100644 index 0000000000..b3829d1a6d --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/ParamsRequestCondition.java @@ -0,0 +1,152 @@ +/* + * 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.web.reactive.result.condition; + +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.server.ServerWebExchange; + +/** + * A logical conjunction (' && ') request condition that matches a request against + * a set parameter expressions with syntax defined in {@link RequestMapping#params()}. + * + * @author Rossen Stoyanchev + */ +public final class ParamsRequestCondition extends AbstractRequestCondition { + + private final Set expressions; + + + /** + * Create a new instance from the given param expressions. + * @param params expressions with syntax defined in {@link RequestMapping#params()}; + * if 0, the condition will match to every request. + */ + public ParamsRequestCondition(String... params) { + this(parseExpressions(params)); + } + + private ParamsRequestCondition(Collection conditions) { + this.expressions = Collections.unmodifiableSet(new LinkedHashSet<>(conditions)); + } + + + private static Collection parseExpressions(String... params) { + Set expressions = new LinkedHashSet<>(); + if (params != null) { + for (String param : params) { + expressions.add(new ParamExpression(param)); + } + } + return expressions; + } + + + /** + * Return the contained request parameter expressions. + */ + public Set> getExpressions() { + return new LinkedHashSet<>(this.expressions); + } + + @Override + protected Collection getContent() { + return this.expressions; + } + + @Override + protected String getToStringInfix() { + return " && "; + } + + /** + * Returns a new instance with the union of the param expressions + * from "this" and the "other" instance. + */ + @Override + public ParamsRequestCondition combine(ParamsRequestCondition other) { + Set set = new LinkedHashSet<>(this.expressions); + set.addAll(other.expressions); + return new ParamsRequestCondition(set); + } + + /** + * Returns "this" instance if the request matches all param expressions; + * or {@code null} otherwise. + */ + @Override + public ParamsRequestCondition getMatchingCondition(ServerWebExchange exchange) { + for (ParamExpression expression : expressions) { + if (!expression.match(exchange)) { + return null; + } + } + return this; + } + + /** + * Returns: + *

      + *
    • 0 if the two conditions have the same number of parameter expressions + *
    • Less than 0 if "this" instance has more parameter expressions + *
    • Greater than 0 if the "other" instance has more parameter expressions + *
    + *

    It is assumed that both instances have been obtained via + * {@link #getMatchingCondition(ServerWebExchange)} and each instance + * contains the matching parameter expressions only or is otherwise empty. + */ + @Override + public int compareTo(ParamsRequestCondition other, ServerWebExchange exchange) { + return (other.expressions.size() - this.expressions.size()); + } + + + /** + * Parses and matches a single param expression to a request. + */ + static class ParamExpression extends AbstractNameValueExpression { + + ParamExpression(String expression) { + super(expression); + } + + @Override + protected boolean isCaseSensitiveName() { + return true; + } + + @Override + protected String parseValue(String valueExpression) { + return valueExpression; + } + + @Override + protected boolean matchName(ServerWebExchange exchange) { + return exchange.getRequest().getQueryParams().containsKey(this.name); + } + + @Override + protected boolean matchValue(ServerWebExchange exchange) { + return this.value.equals(exchange.getRequest().getQueryParams().getFirst(this.name)); + } + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/PatternsRequestCondition.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/PatternsRequestCondition.java new file mode 100644 index 0000000000..cba2fe75ef --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/PatternsRequestCondition.java @@ -0,0 +1,285 @@ +/* + * 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.web.reactive.result.condition; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import org.springframework.util.AntPathMatcher; +import org.springframework.util.PathMatcher; +import org.springframework.util.StringUtils; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.util.HttpRequestPathHelper; + +/** + * A logical disjunction (' || ') request condition that matches a request + * against a set of URL path patterns. + * + * @author Rossen Stoyanchev + */ +public final class PatternsRequestCondition extends AbstractRequestCondition { + + private final Set patterns; + + private final HttpRequestPathHelper pathHelper; + + private final PathMatcher pathMatcher; + + private final boolean useSuffixPatternMatch; + + private final boolean useTrailingSlashMatch; + + private final List fileExtensions = new ArrayList(); + + + /** + * Creates a new instance with the given URL patterns. + * Each pattern that is not empty and does not start with "/" is prepended with "/". + * @param patterns 0 or more URL patterns; if 0 the condition will match to every request. + */ + public PatternsRequestCondition(String... patterns) { + this(asList(patterns), null, null, true, true, null); + } + + /** + * Creates a new instance with the given URL patterns. + * Each pattern that is not empty and does not start with "/" is pre-pended with "/". + * @param patterns the URL patterns to use; if 0, the condition will match to every request. + * @param pathHelper to determine the lookup path for a request + * @param pathMatcher for pattern path matching + * @param useSuffixPatternMatch whether to enable matching by suffix (".*") + * @param useTrailingSlashMatch whether to match irrespective of a trailing slash + * @param extensions file extensions to consider for path matching + */ + public PatternsRequestCondition(String[] patterns, HttpRequestPathHelper pathHelper, + PathMatcher pathMatcher, boolean useSuffixPatternMatch, boolean useTrailingSlashMatch, + List extensions) { + + this(asList(patterns), pathHelper, pathMatcher, useSuffixPatternMatch, useTrailingSlashMatch, extensions); + } + + /** + * Private constructor accepting a collection of patterns. + */ + private PatternsRequestCondition(Collection patterns, HttpRequestPathHelper pathHelper, + PathMatcher pathMatcher, boolean useSuffixPatternMatch, boolean useTrailingSlashMatch, + List fileExtensions) { + + this.patterns = Collections.unmodifiableSet(prependLeadingSlash(patterns)); + this.pathHelper = (pathHelper != null ? pathHelper : new HttpRequestPathHelper()); + this.pathMatcher = (pathMatcher != null ? pathMatcher : new AntPathMatcher()); + this.useSuffixPatternMatch = useSuffixPatternMatch; + this.useTrailingSlashMatch = useTrailingSlashMatch; + if (fileExtensions != null) { + for (String fileExtension : fileExtensions) { + if (fileExtension.charAt(0) != '.') { + fileExtension = "." + fileExtension; + } + this.fileExtensions.add(fileExtension); + } + } + } + + + private static List asList(String... patterns) { + return (patterns != null ? Arrays.asList(patterns) : Collections.emptyList()); + } + + private static Set prependLeadingSlash(Collection patterns) { + if (patterns == null) { + return Collections.emptySet(); + } + Set result = new LinkedHashSet<>(patterns.size()); + for (String pattern : patterns) { + if (StringUtils.hasLength(pattern) && !pattern.startsWith("/")) { + pattern = "/" + pattern; + } + result.add(pattern); + } + return result; + } + + public Set getPatterns() { + return this.patterns; + } + + @Override + protected Collection getContent() { + return this.patterns; + } + + @Override + protected String getToStringInfix() { + return " || "; + } + + /** + * Returns a new instance with URL patterns from the current instance ("this") and + * the "other" instance as follows: + *

      + *
    • If there are patterns in both instances, combine the patterns in "this" with + * the patterns in "other" using {@link PathMatcher#combine(String, String)}. + *
    • If only one instance has patterns, use them. + *
    • If neither instance has patterns, use an empty String (i.e. ""). + *
    + */ + @Override + public PatternsRequestCondition combine(PatternsRequestCondition other) { + Set result = new LinkedHashSet<>(); + if (!this.patterns.isEmpty() && !other.patterns.isEmpty()) { + for (String pattern1 : this.patterns) { + for (String pattern2 : other.patterns) { + result.add(this.pathMatcher.combine(pattern1, pattern2)); + } + } + } + else if (!this.patterns.isEmpty()) { + result.addAll(this.patterns); + } + else if (!other.patterns.isEmpty()) { + result.addAll(other.patterns); + } + else { + result.add(""); + } + return new PatternsRequestCondition(result, this.pathHelper, this.pathMatcher, this.useSuffixPatternMatch, + this.useTrailingSlashMatch, this.fileExtensions); + } + + /** + * Checks if any of the patterns match the given request and returns an instance + * that is guaranteed to contain matching patterns, sorted via + * {@link PathMatcher#getPatternComparator(String)}. + *

    A matching pattern is obtained by making checks in the following order: + *

      + *
    • Direct match + *
    • Pattern match with ".*" appended if the pattern doesn't already contain a "." + *
    • Pattern match + *
    • Pattern match with "/" appended if the pattern doesn't already end in "/" + *
    + * @param exchange the current exchange + * @return the same instance if the condition contains no patterns; + * or a new condition with sorted matching patterns; + * or {@code null} if no patterns match. + */ + @Override + public PatternsRequestCondition getMatchingCondition(ServerWebExchange exchange) { + if (this.patterns.isEmpty()) { + return this; + } + + String lookupPath = this.pathHelper.getLookupPathForRequest(exchange); + List matches = getMatchingPatterns(lookupPath); + + return matches.isEmpty() ? null : + new PatternsRequestCondition(matches, this.pathHelper, this.pathMatcher, this.useSuffixPatternMatch, + this.useTrailingSlashMatch, this.fileExtensions); + } + + /** + * Find the patterns matching the given lookup path. Invoking this method should + * yield results equivalent to those of calling + * {@link #getMatchingCondition(ServerWebExchange)}. + * This method is provided as an alternative to be used if no request is available + * (e.g. introspection, tooling, etc). + * @param lookupPath the lookup path to match to existing patterns + * @return a collection of matching patterns sorted with the closest match at the top + */ + public List getMatchingPatterns(String lookupPath) { + List matches = new ArrayList<>(); + for (String pattern : this.patterns) { + String match = getMatchingPattern(pattern, lookupPath); + if (match != null) { + matches.add(match); + } + } + Collections.sort(matches, this.pathMatcher.getPatternComparator(lookupPath)); + return matches; + } + + private String getMatchingPattern(String pattern, String lookupPath) { + if (pattern.equals(lookupPath)) { + return pattern; + } + if (this.useSuffixPatternMatch) { + if (!this.fileExtensions.isEmpty() && lookupPath.indexOf('.') != -1) { + for (String extension : this.fileExtensions) { + if (this.pathMatcher.match(pattern + extension, lookupPath)) { + return pattern + extension; + } + } + } + else { + boolean hasSuffix = pattern.indexOf('.') != -1; + if (!hasSuffix && this.pathMatcher.match(pattern + ".*", lookupPath)) { + return pattern + ".*"; + } + } + } + if (this.pathMatcher.match(pattern, lookupPath)) { + return pattern; + } + if (this.useTrailingSlashMatch) { + if (!pattern.endsWith("/") && this.pathMatcher.match(pattern + "/", lookupPath)) { + return pattern +"/"; + } + } + return null; + } + + /** + * Compare the two conditions based on the URL patterns they contain. + * Patterns are compared one at a time, from top to bottom via + * {@link PathMatcher#getPatternComparator(String)}. If all compared + * patterns match equally, but one instance has more patterns, it is + * considered a closer match. + *

    It is assumed that both instances have been obtained via + * {@link #getMatchingCondition(ServerWebExchange)} to ensure they + * contain only patterns that match the request and are sorted with + * the best matches on top. + */ + @Override + public int compareTo(PatternsRequestCondition other, ServerWebExchange exchange) { + String lookupPath = this.pathHelper.getLookupPathForRequest(exchange); + Comparator patternComparator = this.pathMatcher.getPatternComparator(lookupPath); + Iterator iterator = this.patterns.iterator(); + Iterator iteratorOther = other.patterns.iterator(); + while (iterator.hasNext() && iteratorOther.hasNext()) { + int result = patternComparator.compare(iterator.next(), iteratorOther.next()); + if (result != 0) { + return result; + } + } + if (iterator.hasNext()) { + return -1; + } + else if (iteratorOther.hasNext()) { + return 1; + } + else { + return 0; + } + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/ProducesRequestCondition.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/ProducesRequestCondition.java new file mode 100644 index 0000000000..d9943b009a --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/ProducesRequestCondition.java @@ -0,0 +1,318 @@ +/* + * 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.web.reactive.result.condition; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import org.springframework.http.MediaType; +import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.accept.ContentNegotiationManager; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.reactive.accept.ContentTypeResolver; +import org.springframework.web.reactive.accept.HeaderContentTypeResolver; +import org.springframework.web.server.ServerWebExchange; + +/** + * A logical disjunction (' || ') request condition to match a request's 'Accept' header + * to a list of media type expressions. Two kinds of media type expressions are + * supported, which are described in {@link RequestMapping#produces()} and + * {@link RequestMapping#headers()} where the header name is 'Accept'. + * Regardless of which syntax is used, the semantics are the same. + * + * @author Rossen Stoyanchev + */ +public final class ProducesRequestCondition extends AbstractRequestCondition { + +// private final static ProducesRequestCondition PRE_FLIGHT_MATCH = new ProducesRequestCondition(); + + + private final List MEDIA_TYPE_ALL_LIST = + Collections.singletonList(new ProduceMediaTypeExpression("*/*")); + + private final List expressions; + + private final ContentTypeResolver contentTypeResolver; + + + /** + * Creates a new instance from "produces" expressions. If 0 expressions + * are provided in total, this condition will match to any request. + * @param produces expressions with syntax defined by {@link RequestMapping#produces()} + */ + public ProducesRequestCondition(String... produces) { + this(produces, null); + } + + /** + * Creates a new instance with "produces" and "header" expressions. "Header" + * expressions where the header name is not 'Accept' or have no header value + * defined are ignored. If 0 expressions are provided in total, this condition + * will match to any request. + * @param produces expressions with syntax defined by {@link RequestMapping#produces()} + * @param headers expressions with syntax defined by {@link RequestMapping#headers()} + */ + public ProducesRequestCondition(String[] produces, String[] headers) { + this(produces, headers, null); + } + + /** + * Same as {@link #ProducesRequestCondition(String[], String[])} but also + * accepting a {@link ContentNegotiationManager}. + * @param produces expressions with syntax defined by {@link RequestMapping#produces()} + * @param headers expressions with syntax defined by {@link RequestMapping#headers()} + * @param resolver used to determine requested content type + */ + public ProducesRequestCondition(String[] produces, String[] headers, ContentTypeResolver resolver) { + this.expressions = new ArrayList<>(parseExpressions(produces, headers)); + Collections.sort(this.expressions); + this.contentTypeResolver = (resolver != null ? resolver : new HeaderContentTypeResolver()); + } + + /** + * Private constructor with already parsed media type expressions. + */ + private ProducesRequestCondition(Collection expressions, + ContentTypeResolver resolver) { + + this.expressions = new ArrayList<>(expressions); + Collections.sort(this.expressions); + this.contentTypeResolver = (resolver != null ? resolver : new HeaderContentTypeResolver()); + } + + + private Set parseExpressions(String[] produces, String[] headers) { + Set result = new LinkedHashSet<>(); + if (headers != null) { + for (String header : headers) { + HeadersRequestCondition.HeaderExpression expr = new HeadersRequestCondition.HeaderExpression(header); + if ("Accept".equalsIgnoreCase(expr.name)) { + for (MediaType mediaType : MediaType.parseMediaTypes(expr.value)) { + result.add(new ProduceMediaTypeExpression(mediaType, expr.isNegated)); + } + } + } + } + if (produces != null) { + for (String produce : produces) { + result.add(new ProduceMediaTypeExpression(produce)); + } + } + return result; + } + + /** + * Return the contained "produces" expressions. + */ + public Set getExpressions() { + return new LinkedHashSet<>(this.expressions); + } + + /** + * Return the contained producible media types excluding negated expressions. + */ + public Set getProducibleMediaTypes() { + Set result = new LinkedHashSet<>(); + for (ProduceMediaTypeExpression expression : this.expressions) { + if (!expression.isNegated()) { + result.add(expression.getMediaType()); + } + } + return result; + } + + /** + * Whether the condition has any media type expressions. + */ + public boolean isEmpty() { + return this.expressions.isEmpty(); + } + + @Override + protected List getContent() { + return this.expressions; + } + + @Override + protected String getToStringInfix() { + return " || "; + } + + /** + * Returns the "other" instance if it has any expressions; returns "this" + * instance otherwise. Practically that means a method-level "produces" + * overrides a type-level "produces" condition. + */ + @Override + public ProducesRequestCondition combine(ProducesRequestCondition other) { + return (!other.expressions.isEmpty() ? other : this); + } + + /** + * Checks if any of the contained media type expressions match the given + * request 'Content-Type' header and returns an instance that is guaranteed + * to contain matching expressions only. The match is performed via + * {@link MediaType#isCompatibleWith(MediaType)}. + * @param exchange the current exchange + * @return the same instance if there are no expressions; + * or a new condition with matching expressions; + * or {@code null} if no expressions match. + */ + @Override + public ProducesRequestCondition getMatchingCondition(ServerWebExchange exchange) { +// if (CorsUtils.isPreFlightRequest(request)) { +// return PRE_FLIGHT_MATCH; +// } + if (isEmpty()) { + return this; + } + Set result = new LinkedHashSet<>(expressions); + for (Iterator iterator = result.iterator(); iterator.hasNext();) { + ProduceMediaTypeExpression expression = iterator.next(); + if (!expression.match(exchange)) { + iterator.remove(); + } + } + return (result.isEmpty()) ? null : new ProducesRequestCondition(result, this.contentTypeResolver); + } + + /** + * Compares this and another "produces" condition as follows: + *

      + *
    1. Sort 'Accept' header media types by quality value via + * {@link MediaType#sortByQualityValue(List)} and iterate the list. + *
    2. Get the first index of matching media types in each "produces" + * condition first matching with {@link MediaType#equals(Object)} and + * then with {@link MediaType#includes(MediaType)}. + *
    3. If a lower index is found, the condition at that index wins. + *
    4. If both indexes are equal, the media types at the index are + * compared further with {@link MediaType#SPECIFICITY_COMPARATOR}. + *
    + *

    It is assumed that both instances have been obtained via + * {@link #getMatchingCondition(ServerWebExchange)} and each instance + * contains the matching producible media type expression only or + * is otherwise empty. + */ + @Override + public int compareTo(ProducesRequestCondition other, ServerWebExchange exchange) { + try { + List acceptedMediaTypes = getAcceptedMediaTypes(exchange); + for (MediaType acceptedMediaType : acceptedMediaTypes) { + int thisIndex = this.indexOfEqualMediaType(acceptedMediaType); + int otherIndex = other.indexOfEqualMediaType(acceptedMediaType); + int result = compareMatchingMediaTypes(this, thisIndex, other, otherIndex); + if (result != 0) { + return result; + } + thisIndex = this.indexOfIncludedMediaType(acceptedMediaType); + otherIndex = other.indexOfIncludedMediaType(acceptedMediaType); + result = compareMatchingMediaTypes(this, thisIndex, other, otherIndex); + if (result != 0) { + return result; + } + } + return 0; + } + catch (HttpMediaTypeNotAcceptableException ex) { + // should never happen + throw new IllegalStateException("Cannot compare without having any requested media types", ex); + } + } + + private List getAcceptedMediaTypes(ServerWebExchange exchange) + throws HttpMediaTypeNotAcceptableException { + + List mediaTypes = this.contentTypeResolver.resolveMediaTypes(exchange); + return mediaTypes.isEmpty() ? Collections.singletonList(MediaType.ALL) : mediaTypes; + } + + private int indexOfEqualMediaType(MediaType mediaType) { + for (int i = 0; i < getExpressionsToCompare().size(); i++) { + MediaType currentMediaType = getExpressionsToCompare().get(i).getMediaType(); + if (mediaType.getType().equalsIgnoreCase(currentMediaType.getType()) && + mediaType.getSubtype().equalsIgnoreCase(currentMediaType.getSubtype())) { + return i; + } + } + return -1; + } + + private int indexOfIncludedMediaType(MediaType mediaType) { + for (int i = 0; i < getExpressionsToCompare().size(); i++) { + if (mediaType.includes(getExpressionsToCompare().get(i).getMediaType())) { + return i; + } + } + return -1; + } + + private int compareMatchingMediaTypes(ProducesRequestCondition condition1, int index1, + ProducesRequestCondition condition2, int index2) { + + int result = 0; + if (index1 != index2) { + result = index2 - index1; + } + else if (index1 != -1) { + ProduceMediaTypeExpression expr1 = condition1.getExpressionsToCompare().get(index1); + ProduceMediaTypeExpression expr2 = condition2.getExpressionsToCompare().get(index2); + result = expr1.compareTo(expr2); + result = (result != 0) ? result : expr1.getMediaType().compareTo(expr2.getMediaType()); + } + return result; + } + + /** + * Return the contained "produces" expressions or if that's empty, a list + * with a {@code MediaType_ALL} expression. + */ + private List getExpressionsToCompare() { + return (this.expressions.isEmpty() ? MEDIA_TYPE_ALL_LIST : this.expressions); + } + + + /** + * Parses and matches a single media type expression to a request's 'Accept' header. + */ + class ProduceMediaTypeExpression extends AbstractMediaTypeExpression { + + ProduceMediaTypeExpression(MediaType mediaType, boolean negated) { + super(mediaType, negated); + } + + ProduceMediaTypeExpression(String expression) { + super(expression); + } + + @Override + protected boolean matchMediaType(ServerWebExchange exchange) throws HttpMediaTypeNotAcceptableException { + List acceptedMediaTypes = getAcceptedMediaTypes(exchange); + for (MediaType acceptedMediaType : acceptedMediaTypes) { + if (getMediaType().isCompatibleWith(acceptedMediaType)) { + return true; + } + } + return false; + } + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/RequestCondition.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/RequestCondition.java new file mode 100644 index 0000000000..99bc2f2aa8 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/RequestCondition.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.web.reactive.result.condition; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.web.server.ServerWebExchange; + +/** + * Contract for request mapping conditions. + * + *

    Request conditions can be combined via {@link #combine(Object)}, matched to + * a request via {@link #getMatchingCondition(ServerWebExchange)}, and compared + * to each other via {@link #compareTo(Object, ServerWebExchange)} to determine + * which is a closer match for a given request. + * + * @author Rossen Stoyanchev + * @param the type of objects that this RequestCondition can be combined + * with and compared to + */ +public interface RequestCondition { + + /** + * Combine this condition with another such as conditions from a + * type-level and method-level {@code @RequestMapping} annotation. + * @param other the condition to combine with. + * @return a request condition instance that is the result of combining + * the two condition instances. + */ + T combine(T other); + + /** + * Check if the condition matches the request returning a potentially new + * instance created for the current request. For example a condition with + * multiple URL patterns may return a new instance only with those patterns + * that match the request. + *

    For CORS pre-flight requests, conditions should match to the would-be, + * actual request (e.g. URL pattern, query parameters, and the HTTP method + * from the "Access-Control-Request-Method" header). If a condition cannot + * be matched to a pre-flight request it should return an instance with + * empty content thus not causing a failure to match. + * @return a condition instance in case of a match or {@code null} otherwise. + */ + T getMatchingCondition(ServerWebExchange exchange); + + /** + * Compare this condition to another condition in the context of + * a specific request. This method assumes both instances have + * been obtained via {@link #getMatchingCondition(ServerWebExchange)} + * to ensure they have content relevant to current request only. + */ + int compareTo(T other, ServerWebExchange exchange); + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/RequestConditionHolder.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/RequestConditionHolder.java new file mode 100644 index 0000000000..5e035f46a7 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/RequestConditionHolder.java @@ -0,0 +1,139 @@ +/* + * 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.web.reactive.result.condition; + +import java.util.Collection; +import java.util.Collections; + +import org.springframework.web.server.ServerWebExchange; + +/** + * A holder for a {@link RequestCondition} useful when the type of the request + * condition is not known ahead of time, e.g. custom condition. Since this + * class is also an implementation of {@code RequestCondition}, effectively it + * decorates the held request condition and allows it to be combined and compared + * with other request conditions in a type and null safe way. + * + *

    When two {@code RequestConditionHolder} instances are combined or compared + * with each other, it is expected the conditions they hold are of the same type. + * If they are not, a {@link ClassCastException} is raised. + * + * @author Rossen Stoyanchev + */ +public final class RequestConditionHolder extends AbstractRequestCondition { + + private final RequestCondition condition; + + + /** + * Create a new holder to wrap the given request condition. + * @param requestCondition the condition to hold, may be {@code null} + */ + @SuppressWarnings("unchecked") + public RequestConditionHolder(RequestCondition requestCondition) { + this.condition = (RequestCondition) requestCondition; + } + + + /** + * Return the held request condition, or {@code null} if not holding one. + */ + public RequestCondition getCondition() { + return this.condition; + } + + @Override + protected Collection getContent() { + return (this.condition != null ? Collections.singleton(this.condition) : Collections.emptyList()); + } + + @Override + protected String getToStringInfix() { + return " "; + } + + /** + * Combine the request conditions held by the two RequestConditionHolder + * instances after making sure the conditions are of the same type. + * Or if one holder is empty, the other holder is returned. + */ + @Override + public RequestConditionHolder combine(RequestConditionHolder other) { + if (this.condition == null && other.condition == null) { + return this; + } + else if (this.condition == null) { + return other; + } + else if (other.condition == null) { + return this; + } + else { + assertEqualConditionTypes(other); + RequestCondition combined = (RequestCondition) this.condition.combine(other.condition); + return new RequestConditionHolder(combined); + } + } + + /** + * Ensure the held request conditions are of the same type. + */ + private void assertEqualConditionTypes(RequestConditionHolder other) { + Class clazz = this.condition.getClass(); + Class otherClazz = other.condition.getClass(); + if (!clazz.equals(otherClazz)) { + throw new ClassCastException("Incompatible request conditions: " + clazz + " and " + otherClazz); + } + } + + /** + * Get the matching condition for the held request condition wrap it in a + * new RequestConditionHolder instance. Or otherwise if this is an empty + * holder, return the same holder instance. + */ + @Override + public RequestConditionHolder getMatchingCondition(ServerWebExchange exchange) { + if (this.condition == null) { + return this; + } + RequestCondition match = (RequestCondition) this.condition.getMatchingCondition(exchange); + return (match != null ? new RequestConditionHolder(match) : null); + } + + /** + * Compare the request conditions held by the two RequestConditionHolder + * instances after making sure the conditions are of the same type. + * Or if one holder is empty, the other holder is preferred. + */ + @Override + public int compareTo(RequestConditionHolder other, ServerWebExchange exchange) { + if (this.condition == null && other.condition == null) { + return 0; + } + else if (this.condition == null) { + return 1; + } + else if (other.condition == null) { + return -1; + } + else { + assertEqualConditionTypes(other); + return this.condition.compareTo(other.condition, exchange); + } + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/RequestMethodsRequestCondition.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/RequestMethodsRequestCondition.java new file mode 100644 index 0000000000..f8d8814da2 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/RequestMethodsRequestCondition.java @@ -0,0 +1,162 @@ +/* + * 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.web.reactive.result.condition; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import javax.servlet.http.HttpServletRequest; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.server.ServerWebExchange; + +/** + * A logical disjunction (' || ') request condition that matches a request + * against a set of {@link RequestMethod}s. + * + * @author Rossen Stoyanchev + */ +public final class RequestMethodsRequestCondition extends AbstractRequestCondition { + + private static final RequestMethodsRequestCondition HEAD_CONDITION = + new RequestMethodsRequestCondition(RequestMethod.HEAD); + + + private final Set methods; + + + /** + * Create a new instance with the given request methods. + * @param requestMethods 0 or more HTTP request methods; + * if, 0 the condition will match to every request + */ + public RequestMethodsRequestCondition(RequestMethod... requestMethods) { + this(asList(requestMethods)); + } + + private RequestMethodsRequestCondition(Collection requestMethods) { + this.methods = Collections.unmodifiableSet(new LinkedHashSet<>(requestMethods)); + } + + + private static List asList(RequestMethod... requestMethods) { + return (requestMethods != null ? Arrays.asList(requestMethods) : Collections.emptyList()); + } + + + /** + * Returns all {@link RequestMethod}s contained in this condition. + */ + public Set getMethods() { + return this.methods; + } + + @Override + protected Collection getContent() { + return this.methods; + } + + @Override + protected String getToStringInfix() { + return " || "; + } + + /** + * Returns a new instance with a union of the HTTP request methods + * from "this" and the "other" instance. + */ + @Override + public RequestMethodsRequestCondition combine(RequestMethodsRequestCondition other) { + Set set = new LinkedHashSet<>(this.methods); + set.addAll(other.methods); + return new RequestMethodsRequestCondition(set); + } + + /** + * Check if any of the HTTP request methods match the given request and + * return an instance that contains the matching HTTP request method only. + * @param exchange the current exchange + * @return the same instance if the condition is empty (unless the request + * method is HTTP OPTIONS), a new condition with the matched request method, + * or {@code null} if there is no match or the condition is empty and the + * request method is OPTIONS. + */ + @Override + public RequestMethodsRequestCondition getMatchingCondition(ServerWebExchange exchange) { +// if (CorsUtils.isPreFlightRequest(request)) { +// return matchPreFlight(request); +// } + if (getMethods().isEmpty()) { + if (RequestMethod.OPTIONS.name().equals(exchange.getRequest().getMethod().name())) { + return null; // No implicit match for OPTIONS (we handle it) + } + return this; + } + return matchRequestMethod(exchange.getRequest().getMethod().name()); + } + + /** + * On a pre-flight request match to the would-be, actual request. + * Hence empty conditions is a match, otherwise try to match to the HTTP + * method in the "Access-Control-Request-Method" header. + */ + @SuppressWarnings("unused") + private RequestMethodsRequestCondition matchPreFlight(HttpServletRequest request) { + if (getMethods().isEmpty()) { + return this; + } + String expectedMethod = request.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD); + return matchRequestMethod(expectedMethod); + } + + private RequestMethodsRequestCondition matchRequestMethod(String httpMethodValue) { + HttpMethod httpMethod = HttpMethod.resolve(httpMethodValue); + if (httpMethod != null) { + for (RequestMethod method : getMethods()) { + if (httpMethod.matches(method.name())) { + return new RequestMethodsRequestCondition(method); + } + } + if (httpMethod == HttpMethod.HEAD && getMethods().contains(RequestMethod.GET)) { + return HEAD_CONDITION; + } + } + return null; + } + + /** + * Returns: + *
      + *
    • 0 if the two conditions contain the same number of HTTP request methods + *
    • Less than 0 if "this" instance has an HTTP request method but "other" doesn't + *
    • Greater than 0 "other" has an HTTP request method but "this" doesn't + *
    + *

    It is assumed that both instances have been obtained via + * {@link #getMatchingCondition(ServerWebExchange)} and therefore each instance + * contains the matching HTTP request method only or is otherwise empty. + */ + @Override + public int compareTo(RequestMethodsRequestCondition other, ServerWebExchange exchange) { + return (other.methods.size() - this.methods.size()); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/package-info.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/package-info.java new file mode 100644 index 0000000000..1163fbdafd --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/package-info.java @@ -0,0 +1,6 @@ +/** + * Support for mapping requests based on a + * {@link org.springframework.web.reactive.result.condition.RequestCondition + * RequestCondition} type hierarchy. + */ +package org.springframework.web.reactive.result.condition; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfo.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfo.java new file mode 100644 index 0000000000..15f91ea04d --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfo.java @@ -0,0 +1,602 @@ +/* + * 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.web.reactive.result.method; + +import java.util.List; + +import org.springframework.util.PathMatcher; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.reactive.accept.ContentTypeResolver; +import org.springframework.web.reactive.accept.FileExtensionContentTypeResolver; +import org.springframework.web.reactive.result.condition.ConsumesRequestCondition; +import org.springframework.web.reactive.result.condition.HeadersRequestCondition; +import org.springframework.web.reactive.result.condition.ParamsRequestCondition; +import org.springframework.web.reactive.result.condition.PatternsRequestCondition; +import org.springframework.web.reactive.result.condition.ProducesRequestCondition; +import org.springframework.web.reactive.result.condition.RequestCondition; +import org.springframework.web.reactive.result.condition.RequestConditionHolder; +import org.springframework.web.reactive.result.condition.RequestMethodsRequestCondition; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.util.HttpRequestPathHelper; + +/** + * Encapsulates the following request mapping conditions: + *

      + *
    1. {@link PatternsRequestCondition} + *
    2. {@link RequestMethodsRequestCondition} + *
    3. {@link ParamsRequestCondition} + *
    4. {@link HeadersRequestCondition} + *
    5. {@link ConsumesRequestCondition} + *
    6. {@link ProducesRequestCondition} + *
    7. {@code RequestCondition} (optional, custom request condition) + *
    + * + * @author Rossen Stoyanchev + */ +public final class RequestMappingInfo implements RequestCondition { + + private final String name; + + private final PatternsRequestCondition patternsCondition; + + private final RequestMethodsRequestCondition methodsCondition; + + private final ParamsRequestCondition paramsCondition; + + private final HeadersRequestCondition headersCondition; + + private final ConsumesRequestCondition consumesCondition; + + private final ProducesRequestCondition producesCondition; + + private final RequestConditionHolder customConditionHolder; + + + public RequestMappingInfo(String name, PatternsRequestCondition patterns, RequestMethodsRequestCondition methods, + ParamsRequestCondition params, HeadersRequestCondition headers, ConsumesRequestCondition consumes, + ProducesRequestCondition produces, RequestCondition custom) { + + this.name = (StringUtils.hasText(name) ? name : null); + this.patternsCondition = (patterns != null ? patterns : new PatternsRequestCondition()); + this.methodsCondition = (methods != null ? methods : new RequestMethodsRequestCondition()); + this.paramsCondition = (params != null ? params : new ParamsRequestCondition()); + this.headersCondition = (headers != null ? headers : new HeadersRequestCondition()); + this.consumesCondition = (consumes != null ? consumes : new ConsumesRequestCondition()); + this.producesCondition = (produces != null ? produces : new ProducesRequestCondition()); + this.customConditionHolder = new RequestConditionHolder(custom); + } + + /** + * Creates a new instance with the given request conditions. + */ + public RequestMappingInfo(PatternsRequestCondition patterns, RequestMethodsRequestCondition methods, + ParamsRequestCondition params, HeadersRequestCondition headers, ConsumesRequestCondition consumes, + ProducesRequestCondition produces, RequestCondition custom) { + + this(null, patterns, methods, params, headers, consumes, produces, custom); + } + + /** + * Re-create a RequestMappingInfo with the given custom request condition. + */ + public RequestMappingInfo(RequestMappingInfo info, RequestCondition customRequestCondition) { + this(info.name, info.patternsCondition, info.methodsCondition, info.paramsCondition, info.headersCondition, + info.consumesCondition, info.producesCondition, customRequestCondition); + } + + + /** + * Return the name for this mapping, or {@code null}. + */ + public String getName() { + return this.name; + } + + /** + * Returns the URL patterns of this {@link RequestMappingInfo}; + * or instance with 0 patterns, never {@code null}. + */ + public PatternsRequestCondition getPatternsCondition() { + return this.patternsCondition; + } + + /** + * Returns the HTTP request methods of this {@link RequestMappingInfo}; + * or instance with 0 request methods, never {@code null}. + */ + public RequestMethodsRequestCondition getMethodsCondition() { + return this.methodsCondition; + } + + /** + * Returns the "parameters" condition of this {@link RequestMappingInfo}; + * or instance with 0 parameter expressions, never {@code null}. + */ + public ParamsRequestCondition getParamsCondition() { + return this.paramsCondition; + } + + /** + * Returns the "headers" condition of this {@link RequestMappingInfo}; + * or instance with 0 header expressions, never {@code null}. + */ + public HeadersRequestCondition getHeadersCondition() { + return this.headersCondition; + } + + /** + * Returns the "consumes" condition of this {@link RequestMappingInfo}; + * or instance with 0 consumes expressions, never {@code null}. + */ + public ConsumesRequestCondition getConsumesCondition() { + return this.consumesCondition; + } + + /** + * Returns the "produces" condition of this {@link RequestMappingInfo}; + * or instance with 0 produces expressions, never {@code null}. + */ + public ProducesRequestCondition getProducesCondition() { + return this.producesCondition; + } + + /** + * Returns the "custom" condition of this {@link RequestMappingInfo}; or {@code null}. + */ + public RequestCondition getCustomCondition() { + return this.customConditionHolder.getCondition(); + } + + + /** + * Combines "this" request mapping info (i.e. the current instance) with another request mapping info instance. + *

    Example: combine type- and method-level request mappings. + * @return a new request mapping info instance; never {@code null} + */ + @Override + public RequestMappingInfo combine(RequestMappingInfo other) { + String name = combineNames(other); + PatternsRequestCondition patterns = this.patternsCondition.combine(other.patternsCondition); + RequestMethodsRequestCondition methods = this.methodsCondition.combine(other.methodsCondition); + ParamsRequestCondition params = this.paramsCondition.combine(other.paramsCondition); + HeadersRequestCondition headers = this.headersCondition.combine(other.headersCondition); + ConsumesRequestCondition consumes = this.consumesCondition.combine(other.consumesCondition); + ProducesRequestCondition produces = this.producesCondition.combine(other.producesCondition); + RequestConditionHolder custom = this.customConditionHolder.combine(other.customConditionHolder); + + return new RequestMappingInfo(name, patterns, + methods, params, headers, consumes, produces, custom.getCondition()); + } + + private String combineNames(RequestMappingInfo other) { + if (this.name != null && other.name != null) { + String separator = "#"; + return this.name + separator + other.name; + } + else if (this.name != null) { + return this.name; + } + else { + return (other.name != null ? other.name : null); + } + } + + /** + * Checks if all conditions in this request mapping info match the provided request and returns + * a potentially new request mapping info with conditions tailored to the current request. + *

    For example the returned instance may contain the subset of URL patterns that match to + * the current request, sorted with best matching patterns on top. + * @return a new instance in case all conditions match; or {@code null} otherwise + */ + @Override + public RequestMappingInfo getMatchingCondition(ServerWebExchange exchange) { + RequestMethodsRequestCondition methods = this.methodsCondition.getMatchingCondition(exchange); + ParamsRequestCondition params = this.paramsCondition.getMatchingCondition(exchange); + HeadersRequestCondition headers = this.headersCondition.getMatchingCondition(exchange); + ConsumesRequestCondition consumes = this.consumesCondition.getMatchingCondition(exchange); + ProducesRequestCondition produces = this.producesCondition.getMatchingCondition(exchange); + + if (methods == null || params == null || headers == null || consumes == null || produces == null) { + return null; + } + + PatternsRequestCondition patterns = this.patternsCondition.getMatchingCondition(exchange); + if (patterns == null) { + return null; + } + + RequestConditionHolder custom = this.customConditionHolder.getMatchingCondition(exchange); + if (custom == null) { + return null; + } + + return new RequestMappingInfo(this.name, patterns, + methods, params, headers, consumes, produces, custom.getCondition()); + } + + /** + * Compares "this" info (i.e. the current instance) with another info in the context of a request. + *

    Note: It is assumed both instances have been obtained via + * {@link #getMatchingCondition(ServerWebExchange)} to ensure they have conditions with + * content relevant to current request. + */ + @Override + public int compareTo(RequestMappingInfo other, ServerWebExchange exchange) { + int result = this.patternsCondition.compareTo(other.getPatternsCondition(), exchange); + if (result != 0) { + return result; + } + result = this.paramsCondition.compareTo(other.getParamsCondition(), exchange); + if (result != 0) { + return result; + } + result = this.headersCondition.compareTo(other.getHeadersCondition(), exchange); + if (result != 0) { + return result; + } + result = this.consumesCondition.compareTo(other.getConsumesCondition(), exchange); + if (result != 0) { + return result; + } + result = this.producesCondition.compareTo(other.getProducesCondition(), exchange); + if (result != 0) { + return result; + } + result = this.methodsCondition.compareTo(other.getMethodsCondition(), exchange); + if (result != 0) { + return result; + } + result = this.customConditionHolder.compareTo(other.customConditionHolder, exchange); + if (result != 0) { + return result; + } + return 0; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof RequestMappingInfo)) { + return false; + } + RequestMappingInfo otherInfo = (RequestMappingInfo) other; + return (this.patternsCondition.equals(otherInfo.patternsCondition) && + this.methodsCondition.equals(otherInfo.methodsCondition) && + this.paramsCondition.equals(otherInfo.paramsCondition) && + this.headersCondition.equals(otherInfo.headersCondition) && + this.consumesCondition.equals(otherInfo.consumesCondition) && + this.producesCondition.equals(otherInfo.producesCondition) && + this.customConditionHolder.equals(otherInfo.customConditionHolder)); + } + + @Override + public int hashCode() { + return (this.patternsCondition.hashCode() * 31 + // primary differentiation + this.methodsCondition.hashCode() + this.paramsCondition.hashCode() + + this.headersCondition.hashCode() + this.consumesCondition.hashCode() + + this.producesCondition.hashCode() + this.customConditionHolder.hashCode()); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("{"); + builder.append(this.patternsCondition); + if (!this.methodsCondition.isEmpty()) { + builder.append(",methods=").append(this.methodsCondition); + } + if (!this.paramsCondition.isEmpty()) { + builder.append(",params=").append(this.paramsCondition); + } + if (!this.headersCondition.isEmpty()) { + builder.append(",headers=").append(this.headersCondition); + } + if (!this.consumesCondition.isEmpty()) { + builder.append(",consumes=").append(this.consumesCondition); + } + if (!this.producesCondition.isEmpty()) { + builder.append(",produces=").append(this.producesCondition); + } + if (!this.customConditionHolder.isEmpty()) { + builder.append(",custom=").append(this.customConditionHolder); + } + builder.append('}'); + return builder.toString(); + } + + + /** + * Create a new {@code RequestMappingInfo.Builder} with the given paths. + * @param paths the paths to use + */ + public static Builder paths(String... paths) { + return new DefaultBuilder(paths); + } + + + /** + * Defines a builder for creating a RequestMappingInfo. + */ + public interface Builder { + + /** + * Set the path patterns. + */ + Builder paths(String... paths); + + /** + * Set the request method conditions. + */ + Builder methods(RequestMethod... methods); + + /** + * Set the request param conditions. + */ + Builder params(String... params); + + /** + * Set the header conditions. + *

    By default this is not set. + */ + Builder headers(String... headers); + + /** + * Set the consumes conditions. + */ + Builder consumes(String... consumes); + + /** + * Set the produces conditions. + */ + Builder produces(String... produces); + + /** + * Set the mapping name. + */ + Builder mappingName(String name); + + /** + * Set a custom condition to use. + */ + Builder customCondition(RequestCondition condition); + + /** + * Provide additional configuration needed for request mapping purposes. + */ + Builder options(BuilderConfiguration options); + + /** + * Build the RequestMappingInfo. + */ + RequestMappingInfo build(); + } + + + private static class DefaultBuilder implements Builder { + + private String[] paths; + + private RequestMethod[] methods; + + private String[] params; + + private String[] headers; + + private String[] consumes; + + private String[] produces; + + private String mappingName; + + private RequestCondition customCondition; + + private BuilderConfiguration options = new BuilderConfiguration(); + + public DefaultBuilder(String... paths) { + this.paths = paths; + } + + @Override + public Builder paths(String... paths) { + this.paths = paths; + return this; + } + + @Override + public DefaultBuilder methods(RequestMethod... methods) { + this.methods = methods; + return this; + } + + @Override + public DefaultBuilder params(String... params) { + this.params = params; + return this; + } + + @Override + public DefaultBuilder headers(String... headers) { + this.headers = headers; + return this; + } + + @Override + public DefaultBuilder consumes(String... consumes) { + this.consumes = consumes; + return this; + } + + @Override + public DefaultBuilder produces(String... produces) { + this.produces = produces; + return this; + } + + @Override + public DefaultBuilder mappingName(String name) { + this.mappingName = name; + return this; + } + + @Override + public DefaultBuilder customCondition(RequestCondition condition) { + this.customCondition = condition; + return this; + } + + @Override + public Builder options(BuilderConfiguration options) { + this.options = options; + return this; + } + + @Override + public RequestMappingInfo build() { + ContentTypeResolver contentTypeResolver = this.options.getContentTypeResolver(); + + PatternsRequestCondition patternsCondition = new PatternsRequestCondition( + this.paths, this.options.getPathHelper(), this.options.getPathMatcher(), + this.options.useSuffixPatternMatch(), this.options.useTrailingSlashMatch(), + this.options.getFileExtensions()); + + return new RequestMappingInfo(this.mappingName, patternsCondition, + new RequestMethodsRequestCondition(methods), + new ParamsRequestCondition(this.params), + new HeadersRequestCondition(this.headers), + new ConsumesRequestCondition(this.consumes, this.headers), + new ProducesRequestCondition(this.produces, this.headers, contentTypeResolver), + this.customCondition); + } + } + + + /** + * Container for configuration options used for request mapping purposes. + * Such configuration is required to create RequestMappingInfo instances but + * is typically used across all RequestMappingInfo instances. + * @see Builder#options + */ + public static class BuilderConfiguration { + + private HttpRequestPathHelper pathHelper; + + private PathMatcher pathMatcher; + + private boolean trailingSlashMatch = true; + + private boolean suffixPatternMatch = true; + + private boolean registeredSuffixPatternMatch = false; + + private ContentTypeResolver contentTypeResolver; + + /** + * Set a custom UrlPathHelper to use for the PatternsRequestCondition. + *

    By default this is not set. + */ + public void setPathHelper(HttpRequestPathHelper pathHelper) { + this.pathHelper = pathHelper; + } + + public HttpRequestPathHelper getPathHelper() { + return this.pathHelper; + } + + /** + * Set a custom PathMatcher to use for the PatternsRequestCondition. + *

    By default this is not set. + */ + public void setPathMatcher(PathMatcher pathMatcher) { + this.pathMatcher = pathMatcher; + } + + public PathMatcher getPathMatcher() { + return this.pathMatcher; + } + + /** + * Whether to apply trailing slash matching in PatternsRequestCondition. + *

    By default this is set to 'true'. + */ + public void setTrailingSlashMatch(boolean trailingSlashMatch) { + this.trailingSlashMatch = trailingSlashMatch; + } + + public boolean useTrailingSlashMatch() { + return this.trailingSlashMatch; + } + + /** + * Whether to apply suffix pattern matching in PatternsRequestCondition. + *

    By default this is set to 'true'. + * @see #setRegisteredSuffixPatternMatch(boolean) + */ + public void setSuffixPatternMatch(boolean suffixPatternMatch) { + this.suffixPatternMatch = suffixPatternMatch; + } + + public boolean useSuffixPatternMatch() { + return this.suffixPatternMatch; + } + + /** + * Whether suffix pattern matching should be restricted to registered + * file extensions only. Setting this property also sets + * suffixPatternMatch=true and requires that a + * {@link #setContentTypeResolver} is also configured in order to + * obtain the registered file extensions. + */ + public void setRegisteredSuffixPatternMatch(boolean registeredSuffixPatternMatch) { + this.registeredSuffixPatternMatch = registeredSuffixPatternMatch; + this.suffixPatternMatch = (registeredSuffixPatternMatch || this.suffixPatternMatch); + } + + public boolean useRegisteredSuffixPatternMatch() { + return this.registeredSuffixPatternMatch; + } + + /** + * Return the file extensions to use for suffix pattern matching. If + * {@code registeredSuffixPatternMatch=true}, the extensions are obtained + * from the configured {@code contentTypeResolver}. + */ + public List getFileExtensions() { + ContentTypeResolver resolver = getContentTypeResolver(); + if (useRegisteredSuffixPatternMatch() && resolver != null) { + if (resolver instanceof FileExtensionContentTypeResolver) { + return ((FileExtensionContentTypeResolver) resolver).getAllFileExtensions(); + } + } + return null; + } + + /** + * Set the ContentNegotiationManager to use for the ProducesRequestCondition. + *

    By default this is not set. + */ + public void setContentTypeResolver(ContentTypeResolver resolver) { + this.contentTypeResolver = resolver; + } + + public ContentTypeResolver getContentTypeResolver() { + return this.contentTypeResolver; + } + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/CompositeRequestConditionTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/CompositeRequestConditionTests.java new file mode 100644 index 0000000000..88b92ed960 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/CompositeRequestConditionTests.java @@ -0,0 +1,151 @@ +/* + * 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.web.reactive.result.condition; + +import java.net.URI; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.http.HttpMethod; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.WebSessionManager; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.mockito.Mockito.mock; + +/** + * Unit tests for {@link CompositeRequestCondition}. + * + * @author Rossen Stoyanchev + */ +public class CompositeRequestConditionTests { + + private ServerWebExchange exchange; + + private ServerHttpRequest request; + + private ParamsRequestCondition param1; + private ParamsRequestCondition param2; + private ParamsRequestCondition param3; + + private HeadersRequestCondition header1; + private HeadersRequestCondition header2; + private HeadersRequestCondition header3; + + + @Before + public void setup() throws Exception { + WebSessionManager sessionManager = mock(WebSessionManager.class); + this.request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); + this.exchange = new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); + + this.param1 = new ParamsRequestCondition("param1"); + this.param2 = new ParamsRequestCondition("param2"); + this.param3 = this.param1.combine(this.param2); + + this.header1 = new HeadersRequestCondition("header1"); + this.header2 = new HeadersRequestCondition("header2"); + this.header3 = this.header1.combine(this.header2); + } + + + @Test + public void combine() { + CompositeRequestCondition cond1 = new CompositeRequestCondition(this.param1, this.header1); + CompositeRequestCondition cond2 = new CompositeRequestCondition(this.param2, this.header2); + CompositeRequestCondition cond3 = new CompositeRequestCondition(this.param3, this.header3); + + assertEquals(cond3, cond1.combine(cond2)); + } + + @Test + public void combineEmpty() { + CompositeRequestCondition empty = new CompositeRequestCondition(); + CompositeRequestCondition notEmpty = new CompositeRequestCondition(this.param1); + + assertSame(empty, empty.combine(empty)); + assertSame(notEmpty, notEmpty.combine(empty)); + assertSame(notEmpty, empty.combine(notEmpty)); + } + + @Test(expected=IllegalArgumentException.class) + public void combineDifferentLength() { + CompositeRequestCondition cond1 = new CompositeRequestCondition(this.param1); + CompositeRequestCondition cond2 = new CompositeRequestCondition(this.param1, this.header1); + cond1.combine(cond2); + } + + @Test + public void match() { + this.request.getQueryParams().add("param1", "paramValue1"); + + RequestCondition condition1 = new RequestMethodsRequestCondition(RequestMethod.GET, RequestMethod.POST); + RequestCondition condition2 = new RequestMethodsRequestCondition(RequestMethod.GET); + + CompositeRequestCondition composite1 = new CompositeRequestCondition(this.param1, condition1); + CompositeRequestCondition composite2 = new CompositeRequestCondition(this.param1, condition2); + + assertEquals(composite2, composite1.getMatchingCondition(this.exchange)); + } + + @Test + public void noMatch() { + CompositeRequestCondition cond = new CompositeRequestCondition(this.param1); + assertNull(cond.getMatchingCondition(this.exchange)); + } + + @Test + public void matchEmpty() { + CompositeRequestCondition empty = new CompositeRequestCondition(); + assertSame(empty, empty.getMatchingCondition(this.exchange)); + } + + @Test + public void compare() { + CompositeRequestCondition cond1 = new CompositeRequestCondition(this.param1); + CompositeRequestCondition cond3 = new CompositeRequestCondition(this.param3); + + assertEquals(1, cond1.compareTo(cond3, this.exchange)); + assertEquals(-1, cond3.compareTo(cond1, this.exchange)); + } + + @Test + public void compareEmpty() { + CompositeRequestCondition empty = new CompositeRequestCondition(); + CompositeRequestCondition notEmpty = new CompositeRequestCondition(this.param1); + + assertEquals(0, empty.compareTo(empty, this.exchange)); + assertEquals(-1, notEmpty.compareTo(empty, this.exchange)); + assertEquals(1, empty.compareTo(notEmpty, this.exchange)); + } + + @Test(expected=IllegalArgumentException.class) + public void compareDifferentLength() { + CompositeRequestCondition cond1 = new CompositeRequestCondition(this.param1); + CompositeRequestCondition cond2 = new CompositeRequestCondition(this.param1, this.header1); + cond1.compareTo(cond2, this.exchange); + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/ConsumesRequestConditionTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/ConsumesRequestConditionTests.java new file mode 100644 index 0000000000..03c4acf57c --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/ConsumesRequestConditionTests.java @@ -0,0 +1,210 @@ +/* + * Copyright 2002-2012 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.web.reactive.result.condition; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Collection; +import java.util.Collections; + +import org.junit.Test; + +import org.springframework.http.HttpMethod; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.web.reactive.result.condition.ConsumesRequestCondition.ConsumeMediaTypeExpression; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.WebSessionManager; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; + +/** + * @author Arjen Poutsma + */ +public class ConsumesRequestConditionTests { + + @Test + public void consumesMatch() throws Exception { + ServerWebExchange exchange = createExchange("text/plain"); + ConsumesRequestCondition condition = new ConsumesRequestCondition("text/plain"); + + assertNotNull(condition.getMatchingCondition(exchange)); + } + + @Test + public void negatedConsumesMatch() throws Exception { + ServerWebExchange exchange = createExchange("text/plain"); + ConsumesRequestCondition condition = new ConsumesRequestCondition("!text/plain"); + + assertNull(condition.getMatchingCondition(exchange)); + } + + @Test + public void getConsumableMediaTypesNegatedExpression() throws Exception { + ConsumesRequestCondition condition = new ConsumesRequestCondition("!application/xml"); + assertEquals(Collections.emptySet(), condition.getConsumableMediaTypes()); + } + + @Test + public void consumesWildcardMatch() throws Exception { + ServerWebExchange exchange = createExchange("text/plain"); + ConsumesRequestCondition condition = new ConsumesRequestCondition("text/*"); + + assertNotNull(condition.getMatchingCondition(exchange)); + } + + @Test + public void consumesMultipleMatch() throws Exception { + ServerWebExchange exchange = createExchange("text/plain"); + ConsumesRequestCondition condition = new ConsumesRequestCondition("text/plain", "application/xml"); + + assertNotNull(condition.getMatchingCondition(exchange)); + } + + @Test + public void consumesSingleNoMatch() throws Exception { + ServerWebExchange exchange = createExchange("application/xml"); + ConsumesRequestCondition condition = new ConsumesRequestCondition("text/plain"); + + assertNull(condition.getMatchingCondition(exchange)); + } + + @Test + public void consumesParseError() throws Exception { + ServerWebExchange exchange = createExchange("01"); + ConsumesRequestCondition condition = new ConsumesRequestCondition("text/plain"); + + assertNull(condition.getMatchingCondition(exchange)); + } + + @Test + public void consumesParseErrorWithNegation() throws Exception { + ServerWebExchange exchange = createExchange("01"); + ConsumesRequestCondition condition = new ConsumesRequestCondition("!text/plain"); + + assertNull(condition.getMatchingCondition(exchange)); + } + + @Test + public void compareToSingle() throws Exception { + ServerWebExchange exchange = createExchange(); + + ConsumesRequestCondition condition1 = new ConsumesRequestCondition("text/plain"); + ConsumesRequestCondition condition2 = new ConsumesRequestCondition("text/*"); + + int result = condition1.compareTo(condition2, exchange); + assertTrue("Invalid comparison result: " + result, result < 0); + + result = condition2.compareTo(condition1, exchange); + assertTrue("Invalid comparison result: " + result, result > 0); + } + + @Test + public void compareToMultiple() throws Exception { + ServerWebExchange exchange = createExchange(); + + ConsumesRequestCondition condition1 = new ConsumesRequestCondition("*/*", "text/plain"); + ConsumesRequestCondition condition2 = new ConsumesRequestCondition("text/*", "text/plain;q=0.7"); + + int result = condition1.compareTo(condition2, exchange); + assertTrue("Invalid comparison result: " + result, result < 0); + + result = condition2.compareTo(condition1, exchange); + assertTrue("Invalid comparison result: " + result, result > 0); + } + + + @Test + public void combine() throws Exception { + ConsumesRequestCondition condition1 = new ConsumesRequestCondition("text/plain"); + ConsumesRequestCondition condition2 = new ConsumesRequestCondition("application/xml"); + + ConsumesRequestCondition result = condition1.combine(condition2); + assertEquals(condition2, result); + } + + @Test + public void combineWithDefault() throws Exception { + ConsumesRequestCondition condition1 = new ConsumesRequestCondition("text/plain"); + ConsumesRequestCondition condition2 = new ConsumesRequestCondition(); + + ConsumesRequestCondition result = condition1.combine(condition2); + assertEquals(condition1, result); + } + + @Test + public void parseConsumesAndHeaders() throws Exception { + String[] consumes = new String[] {"text/plain"}; + String[] headers = new String[]{"foo=bar", "content-type=application/xml,application/pdf"}; + ConsumesRequestCondition condition = new ConsumesRequestCondition(consumes, headers); + + assertConditions(condition, "text/plain", "application/xml", "application/pdf"); + } + + @Test + public void getMatchingCondition() throws Exception { + ServerWebExchange exchange = createExchange("text/plain"); + ConsumesRequestCondition condition = new ConsumesRequestCondition("text/plain", "application/xml"); + + ConsumesRequestCondition result = condition.getMatchingCondition(exchange); + assertConditions(result, "text/plain"); + + condition = new ConsumesRequestCondition("application/xml"); + result = condition.getMatchingCondition(exchange); + assertNull(result); + } + + private void assertConditions(ConsumesRequestCondition condition, String... expected) { + Collection expressions = condition.getContent(); + assertEquals("Invalid amount of conditions", expressions.size(), expected.length); + for (String s : expected) { + boolean found = false; + for (ConsumeMediaTypeExpression expr : expressions) { + String conditionMediaType = expr.getMediaType().toString(); + if (conditionMediaType.equals(s)) { + found = true; + break; + + } + } + if (!found) { + fail("Condition [" + s + "] not found"); + } + } + } + + private ServerWebExchange createExchange() throws URISyntaxException { + return createExchange(null); + } + + private ServerWebExchange createExchange(String contentType) throws URISyntaxException { + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); + if (contentType != null) { + request.getHeaders().add("Content-Type", contentType); + } + WebSessionManager sessionManager = mock(WebSessionManager.class); + return new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/HeadersRequestConditionTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/HeadersRequestConditionTests.java new file mode 100644 index 0000000000..b5dab36223 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/HeadersRequestConditionTests.java @@ -0,0 +1,173 @@ +/* + * Copyright 2002-2012 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.web.reactive.result.condition; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Collection; + +import org.junit.Test; + +import org.springframework.http.HttpMethod; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.WebSessionManager; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +/** + * Unit tests for {@link HeadersRequestCondition}. + * + * @author Rossen Stoyanchev + */ +public class HeadersRequestConditionTests { + + @Test + public void headerEquals() { + assertEquals(new HeadersRequestCondition("foo"), new HeadersRequestCondition("foo")); + assertEquals(new HeadersRequestCondition("foo"), new HeadersRequestCondition("FOO")); + assertFalse(new HeadersRequestCondition("foo").equals(new HeadersRequestCondition("bar"))); + assertEquals(new HeadersRequestCondition("foo=bar"), new HeadersRequestCondition("foo=bar")); + assertEquals(new HeadersRequestCondition("foo=bar"), new HeadersRequestCondition("FOO=bar")); + } + + @Test + public void headerPresent() throws Exception { + ServerWebExchange exchange = createExchange("Accept", ""); + HeadersRequestCondition condition = new HeadersRequestCondition("accept"); + + assertNotNull(condition.getMatchingCondition(exchange)); + } + + @Test + public void headerPresentNoMatch() throws Exception { + ServerWebExchange exchange = createExchange("bar", ""); + HeadersRequestCondition condition = new HeadersRequestCondition("foo"); + + assertNull(condition.getMatchingCondition(exchange)); + } + + @Test + public void headerNotPresent() throws Exception { + ServerWebExchange exchange = createExchange(); + HeadersRequestCondition condition = new HeadersRequestCondition("!accept"); + + assertNotNull(condition.getMatchingCondition(exchange)); + } + + @Test + public void headerValueMatch() throws Exception { + ServerWebExchange exchange = createExchange("foo", "bar"); + HeadersRequestCondition condition = new HeadersRequestCondition("foo=bar"); + + assertNotNull(condition.getMatchingCondition(exchange)); + } + + @Test + public void headerValueNoMatch() throws Exception { + ServerWebExchange exchange = createExchange("foo", "bazz"); + HeadersRequestCondition condition = new HeadersRequestCondition("foo=bar"); + + assertNull(condition.getMatchingCondition(exchange)); + } + + @Test + public void headerCaseSensitiveValueMatch() throws Exception { + ServerWebExchange exchange = createExchange("foo", "bar"); + HeadersRequestCondition condition = new HeadersRequestCondition("foo=Bar"); + + assertNull(condition.getMatchingCondition(exchange)); + } + + @Test + public void headerValueMatchNegated() throws Exception { + ServerWebExchange exchange = createExchange("foo", "baz"); + HeadersRequestCondition condition = new HeadersRequestCondition("foo!=bar"); + + assertNotNull(condition.getMatchingCondition(exchange)); + } + + @Test + public void headerValueNoMatchNegated() throws Exception { + ServerWebExchange exchange = createExchange("foo", "bar"); + HeadersRequestCondition condition = new HeadersRequestCondition("foo!=bar"); + + assertNull(condition.getMatchingCondition(exchange)); + } + + @Test + public void compareTo() throws Exception { + ServerWebExchange exchange = createExchange(); + + HeadersRequestCondition condition1 = new HeadersRequestCondition("foo", "bar", "baz"); + HeadersRequestCondition condition2 = new HeadersRequestCondition("foo", "bar"); + + int result = condition1.compareTo(condition2, exchange); + assertTrue("Invalid comparison result: " + result, result < 0); + + result = condition2.compareTo(condition1, exchange); + assertTrue("Invalid comparison result: " + result, result > 0); + } + + + @Test + public void combine() { + HeadersRequestCondition condition1 = new HeadersRequestCondition("foo=bar"); + HeadersRequestCondition condition2 = new HeadersRequestCondition("foo=baz"); + + HeadersRequestCondition result = condition1.combine(condition2); + Collection conditions = result.getContent(); + assertEquals(2, conditions.size()); + } + + @Test + public void getMatchingCondition() throws Exception { + ServerWebExchange exchange = createExchange("foo", "bar"); + HeadersRequestCondition condition = new HeadersRequestCondition("foo"); + + HeadersRequestCondition result = condition.getMatchingCondition(exchange); + assertEquals(condition, result); + + condition = new HeadersRequestCondition("bar"); + + result = condition.getMatchingCondition(exchange); + assertNull(result); + } + + + private ServerWebExchange createExchange() throws URISyntaxException { + return createExchange(null, null); + } + + private ServerWebExchange createExchange(String headerName, String headerValue) throws URISyntaxException { + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); + if (headerName != null) { + request.getHeaders().add(headerName, headerValue); + } + WebSessionManager sessionManager = mock(WebSessionManager.class); + return new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/ParamsRequestConditionTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/ParamsRequestConditionTests.java new file mode 100644 index 0000000000..f28ce748fb --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/ParamsRequestConditionTests.java @@ -0,0 +1,145 @@ +/* + * Copyright 2002-2012 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.web.reactive.result.condition; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Collection; + +import org.junit.Test; + +import org.springframework.http.HttpMethod; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.WebSessionManager; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +/** + * @author Arjen Poutsma + */ +public class ParamsRequestConditionTests { + + @Test + public void paramEquals() { + assertEquals(new ParamsRequestCondition("foo"), new ParamsRequestCondition("foo")); + assertFalse(new ParamsRequestCondition("foo").equals(new ParamsRequestCondition("bar"))); + assertFalse(new ParamsRequestCondition("foo").equals(new ParamsRequestCondition("FOO"))); + assertEquals(new ParamsRequestCondition("foo=bar"), new ParamsRequestCondition("foo=bar")); + assertFalse(new ParamsRequestCondition("foo=bar").equals(new ParamsRequestCondition("FOO=bar"))); + } + + @Test + public void paramPresent() throws Exception { + ServerWebExchange exchange = createExchange("foo", ""); + ParamsRequestCondition condition = new ParamsRequestCondition("foo"); + + assertNotNull(condition.getMatchingCondition(exchange)); + } + + @Test + public void paramPresentNoMatch() throws Exception { + ServerWebExchange exchange = createExchange("bar", ""); + ParamsRequestCondition condition = new ParamsRequestCondition("foo"); + + assertNull(condition.getMatchingCondition(exchange)); + } + + @Test + public void paramNotPresent() throws Exception { + ServerWebExchange exchange = createExchange(); + ParamsRequestCondition condition = new ParamsRequestCondition("!foo"); + + assertNotNull(condition.getMatchingCondition(exchange)); + } + + @Test + public void paramValueMatch() throws Exception { + ServerWebExchange exchange = createExchange("foo", "bar"); + ParamsRequestCondition condition = new ParamsRequestCondition("foo=bar"); + + assertNotNull(condition.getMatchingCondition(exchange)); + } + + @Test + public void paramValueNoMatch() throws Exception { + ServerWebExchange exchange = createExchange("foo", "bazz"); + ParamsRequestCondition condition = new ParamsRequestCondition("foo=bar"); + + assertNull(condition.getMatchingCondition(exchange)); + } + + @Test + public void compareTo() throws Exception { + ServerWebExchange exchange = createExchange(); + + ParamsRequestCondition condition1 = new ParamsRequestCondition("foo", "bar", "baz"); + ParamsRequestCondition condition2 = new ParamsRequestCondition("foo", "bar"); + + int result = condition1.compareTo(condition2, exchange); + assertTrue("Invalid comparison result: " + result, result < 0); + + result = condition2.compareTo(condition1, exchange); + assertTrue("Invalid comparison result: " + result, result > 0); + } + + @Test + public void combine() { + ParamsRequestCondition condition1 = new ParamsRequestCondition("foo=bar"); + ParamsRequestCondition condition2 = new ParamsRequestCondition("foo=baz"); + + ParamsRequestCondition result = condition1.combine(condition2); + Collection conditions = result.getContent(); + assertEquals(2, conditions.size()); + } + + @Test + public void getMatchingCondition() throws Exception { + ServerWebExchange exchange = createExchange("foo", "bar"); + ParamsRequestCondition condition = new ParamsRequestCondition("foo"); + + ParamsRequestCondition result = condition.getMatchingCondition(exchange); + assertEquals(condition, result); + + condition = new ParamsRequestCondition("bar"); + + result = condition.getMatchingCondition(exchange); + assertNull(result); + } + + private ServerWebExchange createExchange() throws URISyntaxException { + return createExchange(null, null); + } + + private ServerWebExchange createExchange(String paramName, String paramValue) throws URISyntaxException { + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); + if (paramName != null) { + request.getQueryParams().add(paramName, paramValue); + } + WebSessionManager sessionManager = mock(WebSessionManager.class); + return new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/PatternsRequestConditionTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/PatternsRequestConditionTests.java new file mode 100644 index 0000000000..715b247425 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/PatternsRequestConditionTests.java @@ -0,0 +1,234 @@ +/* + * 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.web.reactive.result.condition; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Collections; +import java.util.List; + +import org.junit.Test; + +import org.springframework.http.HttpMethod; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.WebSessionManager; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.mock; + +/** + * Unit tests for {@link PatternsRequestCondition}. + * + * @author Rossen Stoyanchev + */ +public class PatternsRequestConditionTests { + + @Test + public void prependSlash() { + PatternsRequestCondition c = new PatternsRequestCondition("foo"); + assertEquals("/foo", c.getPatterns().iterator().next()); + } + + @Test + public void prependNonEmptyPatternsOnly() { + PatternsRequestCondition c = new PatternsRequestCondition(""); + assertEquals("Do not prepend empty patterns (SPR-8255)", "", c.getPatterns().iterator().next()); + } + + @Test + public void combineEmptySets() { + PatternsRequestCondition c1 = new PatternsRequestCondition(); + PatternsRequestCondition c2 = new PatternsRequestCondition(); + + assertEquals(new PatternsRequestCondition(""), c1.combine(c2)); + } + + @Test + public void combineOnePatternWithEmptySet() { + PatternsRequestCondition c1 = new PatternsRequestCondition("/type1", "/type2"); + PatternsRequestCondition c2 = new PatternsRequestCondition(); + + assertEquals(new PatternsRequestCondition("/type1", "/type2"), c1.combine(c2)); + + c1 = new PatternsRequestCondition(); + c2 = new PatternsRequestCondition("/method1", "/method2"); + + assertEquals(new PatternsRequestCondition("/method1", "/method2"), c1.combine(c2)); + } + + @Test + public void combineMultiplePatterns() { + PatternsRequestCondition c1 = new PatternsRequestCondition("/t1", "/t2"); + PatternsRequestCondition c2 = new PatternsRequestCondition("/m1", "/m2"); + + assertEquals(new PatternsRequestCondition("/t1/m1", "/t1/m2", "/t2/m1", "/t2/m2"), c1.combine(c2)); + } + + @Test + public void matchDirectPath() throws Exception { + PatternsRequestCondition condition = new PatternsRequestCondition("/foo"); + PatternsRequestCondition match = condition.getMatchingCondition(createExchange("/foo")); + + assertNotNull(match); + } + + @Test + public void matchPattern() throws Exception { + PatternsRequestCondition condition = new PatternsRequestCondition("/foo/*"); + PatternsRequestCondition match = condition.getMatchingCondition(createExchange("/foo/bar")); + + assertNotNull(match); + } + + @Test + public void matchSortPatterns() throws Exception { + PatternsRequestCondition condition = new PatternsRequestCondition("/**", "/foo/bar", "/foo/*"); + PatternsRequestCondition match = condition.getMatchingCondition(createExchange("/foo/bar")); + PatternsRequestCondition expected = new PatternsRequestCondition("/foo/bar", "/foo/*", "/**"); + + assertEquals(expected, match); + } + + @Test + public void matchSuffixPattern() throws Exception { + ServerWebExchange exchange = createExchange("/foo.html"); + + PatternsRequestCondition condition = new PatternsRequestCondition("/{foo}"); + PatternsRequestCondition match = condition.getMatchingCondition(exchange); + + assertNotNull(match); + assertEquals("/{foo}.*", match.getPatterns().iterator().next()); + + condition = new PatternsRequestCondition(new String[] {"/{foo}"}, null, null, false, false, null); + match = condition.getMatchingCondition(exchange); + + assertNotNull(match); + assertEquals("/{foo}", match.getPatterns().iterator().next()); + } + + // SPR-8410 + + @Test + public void matchSuffixPatternUsingFileExtensions() throws Exception { + String[] patterns = new String[] {"/jobs/{jobName}"}; + List extensions = Collections.singletonList("json"); + PatternsRequestCondition condition = new PatternsRequestCondition(patterns, null, null, true, false, extensions); + + ServerWebExchange exchange = createExchange("/jobs/my.job"); + PatternsRequestCondition match = condition.getMatchingCondition(exchange); + + assertNotNull(match); + assertEquals("/jobs/{jobName}", match.getPatterns().iterator().next()); + + exchange = createExchange("/jobs/my.job.json"); + match = condition.getMatchingCondition(exchange); + + assertNotNull(match); + assertEquals("/jobs/{jobName}.json", match.getPatterns().iterator().next()); + } + + @Test + public void matchSuffixPatternUsingFileExtensions2() throws Exception { + PatternsRequestCondition condition1 = new PatternsRequestCondition( + new String[] {"/prefix"}, null, null, true, false, Collections.singletonList("json")); + + PatternsRequestCondition condition2 = new PatternsRequestCondition( + new String[] {"/suffix"}, null, null, true, false, null); + + PatternsRequestCondition combined = condition1.combine(condition2); + + ServerWebExchange exchange = createExchange("/prefix/suffix.json"); + PatternsRequestCondition match = combined.getMatchingCondition(exchange); + + assertNotNull(match); + } + + @Test + public void matchTrailingSlash() throws Exception { + ServerWebExchange exchange = createExchange("/foo/"); + + PatternsRequestCondition condition = new PatternsRequestCondition("/foo"); + PatternsRequestCondition match = condition.getMatchingCondition(exchange); + + assertNotNull(match); + assertEquals("Should match by default", "/foo/", match.getPatterns().iterator().next()); + + condition = new PatternsRequestCondition(new String[] {"/foo"}, null, null, false, true, null); + match = condition.getMatchingCondition(exchange); + + assertNotNull(match); + assertEquals("Trailing slash should be insensitive to useSuffixPatternMatch settings (SPR-6164, SPR-5636)", + "/foo/", match.getPatterns().iterator().next()); + + condition = new PatternsRequestCondition(new String[] {"/foo"}, null, null, false, false, null); + match = condition.getMatchingCondition(exchange); + + assertNull(match); + } + + @Test + public void matchPatternContainsExtension() throws Exception { + PatternsRequestCondition condition = new PatternsRequestCondition("/foo.jpg"); + PatternsRequestCondition match = condition.getMatchingCondition(createExchange("/foo.html")); + + assertNull(match); + } + + @Test + public void compareEqualPatterns() throws Exception { + PatternsRequestCondition c1 = new PatternsRequestCondition("/foo*"); + PatternsRequestCondition c2 = new PatternsRequestCondition("/foo*"); + + assertEquals(0, c1.compareTo(c2, createExchange("/foo"))); + } + + @Test + public void comparePatternSpecificity() throws Exception { + PatternsRequestCondition c1 = new PatternsRequestCondition("/fo*"); + PatternsRequestCondition c2 = new PatternsRequestCondition("/foo"); + + assertEquals(1, c1.compareTo(c2, createExchange("/foo"))); + } + + @Test + public void compareNumberOfMatchingPatterns() throws Exception { + ServerWebExchange exchange = createExchange("/foo.html"); + + PatternsRequestCondition c1 = new PatternsRequestCondition("/foo", "*.jpeg"); + PatternsRequestCondition c2 = new PatternsRequestCondition("/foo", "*.html"); + + PatternsRequestCondition match1 = c1.getMatchingCondition(exchange); + PatternsRequestCondition match2 = c2.getMatchingCondition(exchange); + + assertNotNull(match1); + assertEquals(1, match1.compareTo(match2, exchange)); + } + + + private ServerWebExchange createExchange(String path) throws URISyntaxException { + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI(path)); + WebSessionManager sessionManager = mock(WebSessionManager.class); + return new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/ProducesRequestConditionTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/ProducesRequestConditionTests.java new file mode 100644 index 0000000000..4ae8d19363 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/ProducesRequestConditionTests.java @@ -0,0 +1,327 @@ +/* + * 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.web.reactive.result.condition; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Collection; +import java.util.Collections; + +import org.junit.Test; + +import org.springframework.http.HttpMethod; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.WebSessionManager; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; + +/** + * Unit tests for {@link ProducesRequestCondition}. + * + * @author Rossen Stoyanchev + */ +public class ProducesRequestConditionTests { + + @Test + public void match() throws Exception { + ServerWebExchange exchange = createExchange("text/plain"); + ProducesRequestCondition condition = new ProducesRequestCondition("text/plain"); + + assertNotNull(condition.getMatchingCondition(exchange)); + } + + @Test + public void matchNegated() throws Exception { + ServerWebExchange exchange = createExchange("text/plain"); + ProducesRequestCondition condition = new ProducesRequestCondition("!text/plain"); + + assertNull(condition.getMatchingCondition(exchange)); + } + + @Test + public void getProducibleMediaTypes() throws Exception { + ProducesRequestCondition condition = new ProducesRequestCondition("!application/xml"); + assertEquals(Collections.emptySet(), condition.getProducibleMediaTypes()); + } + + @Test + public void matchWildcard() throws Exception { + ServerWebExchange exchange = createExchange("text/plain"); + ProducesRequestCondition condition = new ProducesRequestCondition("text/*"); + + assertNotNull(condition.getMatchingCondition(exchange)); + } + + @Test + public void matchMultiple() throws Exception { + ServerWebExchange exchange = createExchange("text/plain"); + ProducesRequestCondition condition = new ProducesRequestCondition("text/plain", "application/xml"); + + assertNotNull(condition.getMatchingCondition(exchange)); + } + + @Test + public void matchSingle() throws Exception { + ServerWebExchange exchange = createExchange("application/xml"); + ProducesRequestCondition condition = new ProducesRequestCondition("text/plain"); + + assertNull(condition.getMatchingCondition(exchange)); + } + + @Test + public void matchParseError() throws Exception { + ServerWebExchange exchange = createExchange("bogus"); + ProducesRequestCondition condition = new ProducesRequestCondition("text/plain"); + + assertNull(condition.getMatchingCondition(exchange)); + } + + @Test + public void matchParseErrorWithNegation() throws Exception { + ServerWebExchange exchange = createExchange("bogus"); + ProducesRequestCondition condition = new ProducesRequestCondition("!text/plain"); + + assertNull(condition.getMatchingCondition(exchange)); + } + + @Test + public void compareTo() throws Exception { + ProducesRequestCondition html = new ProducesRequestCondition("text/html"); + ProducesRequestCondition xml = new ProducesRequestCondition("application/xml"); + ProducesRequestCondition none = new ProducesRequestCondition(); + + ServerWebExchange exchange = createExchange("application/xml, text/html"); + + assertTrue(html.compareTo(xml, exchange) > 0); + assertTrue(xml.compareTo(html, exchange) < 0); + assertTrue(xml.compareTo(none, exchange) < 0); + assertTrue(none.compareTo(xml, exchange) > 0); + assertTrue(html.compareTo(none, exchange) < 0); + assertTrue(none.compareTo(html, exchange) > 0); + + exchange = createExchange("application/xml, text/*"); + + assertTrue(html.compareTo(xml, exchange) > 0); + assertTrue(xml.compareTo(html, exchange) < 0); + + exchange = createExchange("application/pdf"); + + assertTrue(html.compareTo(xml, exchange) == 0); + assertTrue(xml.compareTo(html, exchange) == 0); + + // See SPR-7000 + exchange = createExchange("text/html;q=0.9,application/xml"); + + assertTrue(html.compareTo(xml, exchange) > 0); + assertTrue(xml.compareTo(html, exchange) < 0); + } + + @Test + public void compareToWithSingleExpression() throws Exception { + ServerWebExchange exchange = createExchange("text/plain"); + + ProducesRequestCondition condition1 = new ProducesRequestCondition("text/plain"); + ProducesRequestCondition condition2 = new ProducesRequestCondition("text/*"); + + int result = condition1.compareTo(condition2, exchange); + assertTrue("Invalid comparison result: " + result, result < 0); + + result = condition2.compareTo(condition1, exchange); + assertTrue("Invalid comparison result: " + result, result > 0); + } + + @Test + public void compareToMultipleExpressions() throws Exception { + ProducesRequestCondition condition1 = new ProducesRequestCondition("*/*", "text/plain"); + ProducesRequestCondition condition2 = new ProducesRequestCondition("text/*", "text/plain;q=0.7"); + + ServerWebExchange exchange = createExchange("text/plain"); + + int result = condition1.compareTo(condition2, exchange); + assertTrue("Invalid comparison result: " + result, result < 0); + + result = condition2.compareTo(condition1, exchange); + assertTrue("Invalid comparison result: " + result, result > 0); + } + + @Test + public void compareToMultipleExpressionsAndMultipeAcceptHeaderValues() throws Exception { + ProducesRequestCondition condition1 = new ProducesRequestCondition("text/*", "text/plain"); + ProducesRequestCondition condition2 = new ProducesRequestCondition("application/*", "application/xml"); + + ServerWebExchange exchange = createExchange("text/plain", "application/xml"); + + int result = condition1.compareTo(condition2, exchange); + assertTrue("Invalid comparison result: " + result, result < 0); + + result = condition2.compareTo(condition1, exchange); + assertTrue("Invalid comparison result: " + result, result > 0); + + exchange = createExchange("application/xml", "text/plain"); + + result = condition1.compareTo(condition2, exchange); + assertTrue("Invalid comparison result: " + result, result > 0); + + result = condition2.compareTo(condition1, exchange); + assertTrue("Invalid comparison result: " + result, result < 0); + } + + // SPR-8536 + + @Test + public void compareToMediaTypeAll() throws Exception { + ServerWebExchange exchange = createExchange(); + + ProducesRequestCondition condition1 = new ProducesRequestCondition(); + ProducesRequestCondition condition2 = new ProducesRequestCondition("application/json"); + + assertTrue("Should have picked '*/*' condition as an exact match", + condition1.compareTo(condition2, exchange) < 0); + assertTrue("Should have picked '*/*' condition as an exact match", + condition2.compareTo(condition1, exchange) > 0); + + condition1 = new ProducesRequestCondition("*/*"); + condition2 = new ProducesRequestCondition("application/json"); + + assertTrue(condition1.compareTo(condition2, exchange) < 0); + assertTrue(condition2.compareTo(condition1, exchange) > 0); + + exchange = createExchange("*/*"); + + condition1 = new ProducesRequestCondition(); + condition2 = new ProducesRequestCondition("application/json"); + + assertTrue(condition1.compareTo(condition2, exchange) < 0); + assertTrue(condition2.compareTo(condition1, exchange) > 0); + + condition1 = new ProducesRequestCondition("*/*"); + condition2 = new ProducesRequestCondition("application/json"); + + assertTrue(condition1.compareTo(condition2, exchange) < 0); + assertTrue(condition2.compareTo(condition1, exchange) > 0); + } + + // SPR-9021 + + @Test + public void compareToMediaTypeAllWithParameter() throws Exception { + ServerWebExchange exchange = createExchange("*/*;q=0.9"); + + ProducesRequestCondition condition1 = new ProducesRequestCondition(); + ProducesRequestCondition condition2 = new ProducesRequestCondition("application/json"); + + assertTrue(condition1.compareTo(condition2, exchange) < 0); + assertTrue(condition2.compareTo(condition1, exchange) > 0); + } + + @Test + public void compareToEqualMatch() throws Exception { + ServerWebExchange exchange = createExchange("text/*"); + + ProducesRequestCondition condition1 = new ProducesRequestCondition("text/plain"); + ProducesRequestCondition condition2 = new ProducesRequestCondition("text/xhtml"); + + int result = condition1.compareTo(condition2, exchange); + assertTrue("Should have used MediaType.equals(Object) to break the match", result < 0); + + result = condition2.compareTo(condition1, exchange); + assertTrue("Should have used MediaType.equals(Object) to break the match", result > 0); + } + + @Test + public void combine() throws Exception { + ProducesRequestCondition condition1 = new ProducesRequestCondition("text/plain"); + ProducesRequestCondition condition2 = new ProducesRequestCondition("application/xml"); + + ProducesRequestCondition result = condition1.combine(condition2); + assertEquals(condition2, result); + } + + @Test + public void combineWithDefault() throws Exception { + ProducesRequestCondition condition1 = new ProducesRequestCondition("text/plain"); + ProducesRequestCondition condition2 = new ProducesRequestCondition(); + + ProducesRequestCondition result = condition1.combine(condition2); + assertEquals(condition1, result); + } + + @Test + public void instantiateWithProducesAndHeaderConditions() throws Exception { + String[] produces = new String[] {"text/plain"}; + String[] headers = new String[]{"foo=bar", "accept=application/xml,application/pdf"}; + ProducesRequestCondition condition = new ProducesRequestCondition(produces, headers); + + assertConditions(condition, "text/plain", "application/xml", "application/pdf"); + } + + @Test + public void getMatchingCondition() throws Exception { + ServerWebExchange exchange = createExchange("text/plain"); + + ProducesRequestCondition condition = new ProducesRequestCondition("text/plain", "application/xml"); + + ProducesRequestCondition result = condition.getMatchingCondition(exchange); + assertConditions(result, "text/plain"); + + condition = new ProducesRequestCondition("application/xml"); + + result = condition.getMatchingCondition(exchange); + assertNull(result); + } + + private void assertConditions(ProducesRequestCondition condition, String... expected) { + Collection expressions = condition.getContent(); + assertEquals("Invalid number of conditions", expressions.size(), expected.length); + for (String s : expected) { + boolean found = false; + for (ProducesRequestCondition.ProduceMediaTypeExpression expr : expressions) { + String conditionMediaType = expr.getMediaType().toString(); + if (conditionMediaType.equals(s)) { + found = true; + break; + + } + } + if (!found) { + fail("Condition [" + s + "] not found"); + } + } + } + + + private ServerWebExchange createExchange(String... accept) throws URISyntaxException { + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); + if (accept != null) { + for (String value : accept) { + request.getHeaders().add("Accept", value); + } + } + WebSessionManager sessionManager = mock(WebSessionManager.class); + return new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/RequestConditionHolderTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/RequestConditionHolderTests.java new file mode 100644 index 0000000000..d5686381b7 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/RequestConditionHolderTests.java @@ -0,0 +1,141 @@ +/* + * 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.web.reactive.result.condition; + +import java.net.URI; +import java.net.URISyntaxException; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.http.HttpMethod; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.WebSessionManager; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.mockito.Mockito.mock; + +/** + * Unit tests for {@link RequestConditionHolder}. + * + * @author Rossen Stoyanchev + */ +public class RequestConditionHolderTests { + + private ServerWebExchange exchange; + + + @Before + public void setUp() throws Exception { + this.exchange = createExchange(); + } + + private ServerWebExchange createExchange() throws URISyntaxException { + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); + WebSessionManager sessionManager = mock(WebSessionManager.class); + return new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); + } + + + @Test + public void combine() { + RequestConditionHolder params1 = new RequestConditionHolder(new ParamsRequestCondition("name1")); + RequestConditionHolder params2 = new RequestConditionHolder(new ParamsRequestCondition("name2")); + RequestConditionHolder expected = new RequestConditionHolder(new ParamsRequestCondition("name1", "name2")); + + assertEquals(expected, params1.combine(params2)); + } + + @Test + public void combineEmpty() { + RequestConditionHolder empty = new RequestConditionHolder(null); + RequestConditionHolder notEmpty = new RequestConditionHolder(new ParamsRequestCondition("name")); + + assertSame(empty, empty.combine(empty)); + assertSame(notEmpty, notEmpty.combine(empty)); + assertSame(notEmpty, empty.combine(notEmpty)); + } + + @Test(expected=ClassCastException.class) + public void combineIncompatible() { + RequestConditionHolder params = new RequestConditionHolder(new ParamsRequestCondition("name")); + RequestConditionHolder headers = new RequestConditionHolder(new HeadersRequestCondition("name")); + params.combine(headers); + } + + @Test + public void match() { + RequestMethodsRequestCondition rm = new RequestMethodsRequestCondition(RequestMethod.GET, RequestMethod.POST); + RequestConditionHolder custom = new RequestConditionHolder(rm); + RequestMethodsRequestCondition expected = new RequestMethodsRequestCondition(RequestMethod.GET); + + RequestConditionHolder holder = custom.getMatchingCondition(this.exchange); + assertNotNull(holder); + assertEquals(expected, holder.getCondition()); + } + + @Test + public void noMatch() { + RequestMethodsRequestCondition rm = new RequestMethodsRequestCondition(RequestMethod.POST); + RequestConditionHolder custom = new RequestConditionHolder(rm); + + assertNull(custom.getMatchingCondition(this.exchange)); + } + + @Test + public void matchEmpty() { + RequestConditionHolder empty = new RequestConditionHolder(null); + assertSame(empty, empty.getMatchingCondition(this.exchange)); + } + + @Test + public void compare() { + RequestConditionHolder params11 = new RequestConditionHolder(new ParamsRequestCondition("1")); + RequestConditionHolder params12 = new RequestConditionHolder(new ParamsRequestCondition("1", "2")); + + assertEquals(1, params11.compareTo(params12, this.exchange)); + assertEquals(-1, params12.compareTo(params11, this.exchange)); + } + + @Test + public void compareEmpty() { + RequestConditionHolder empty = new RequestConditionHolder(null); + RequestConditionHolder empty2 = new RequestConditionHolder(null); + RequestConditionHolder notEmpty = new RequestConditionHolder(new ParamsRequestCondition("name")); + + assertEquals(0, empty.compareTo(empty2, this.exchange)); + assertEquals(-1, notEmpty.compareTo(empty, this.exchange)); + assertEquals(1, empty.compareTo(notEmpty, this.exchange)); + } + + @Test(expected=ClassCastException.class) + public void compareIncompatible() { + RequestConditionHolder params = new RequestConditionHolder(new ParamsRequestCondition("name")); + RequestConditionHolder headers = new RequestConditionHolder(new HeadersRequestCondition("name")); + params.compareTo(headers, this.exchange); + } + + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/RequestMappingInfoTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/RequestMappingInfoTests.java new file mode 100644 index 0000000000..7ebc75be9e --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/RequestMappingInfoTests.java @@ -0,0 +1,355 @@ +/* + * 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.web.reactive.result.condition; + +import java.net.URI; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.reactive.result.method.RequestMappingInfo; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.WebSessionManager; + +import static java.util.Arrays.asList; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.mock; + +/** + * Unit tests for {@link RequestMappingInfo}. + * + * @author Rossen Stoyanchev + */ +public class RequestMappingInfoTests { + + private ServerWebExchange exchange; + + private ServerHttpRequest request; + + + // TODO: CORS pre-flight (see @Ignored) + + @Before + public void setUp() throws Exception { + WebSessionManager sessionManager = mock(WebSessionManager.class); + this.request = new MockServerHttpRequest(HttpMethod.GET, new URI("/foo")); + this.exchange = new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); + } + + + @Test + public void createEmpty() { + RequestMappingInfo info = new RequestMappingInfo(null, null, null, null, null, null, null); + + assertEquals(0, info.getPatternsCondition().getPatterns().size()); + assertEquals(0, info.getMethodsCondition().getMethods().size()); + assertEquals(true, info.getConsumesCondition().isEmpty()); + assertEquals(true, info.getProducesCondition().isEmpty()); + assertNotNull(info.getParamsCondition()); + assertNotNull(info.getHeadersCondition()); + assertNull(info.getCustomCondition()); + } + + @Test + public void matchPatternsCondition() { + RequestMappingInfo info = new RequestMappingInfo( + new PatternsRequestCondition("/foo*", "/bar"), null, null, null, null, null, null); + RequestMappingInfo expected = new RequestMappingInfo( + new PatternsRequestCondition("/foo*"), null, null, null, null, null, null); + + assertEquals(expected, info.getMatchingCondition(this.exchange)); + + info = new RequestMappingInfo( + new PatternsRequestCondition("/**", "/foo*", "/foo"), null, null, null, null, null, null); + expected = new RequestMappingInfo( + new PatternsRequestCondition("/foo", "/foo*", "/**"), null, null, null, null, null, null); + + assertEquals(expected, info.getMatchingCondition(this.exchange)); + } + + @Test + public void matchParamsCondition() { + this.request.getQueryParams().add("foo", "bar"); + + RequestMappingInfo info = + new RequestMappingInfo( + new PatternsRequestCondition("/foo"), null, + new ParamsRequestCondition("foo=bar"), null, null, null, null); + RequestMappingInfo match = info.getMatchingCondition(this.exchange); + + assertNotNull(match); + + info = new RequestMappingInfo( + new PatternsRequestCondition("/foo"), null, + new ParamsRequestCondition("foo!=bar"), null, null, null, null); + match = info.getMatchingCondition(this.exchange); + + assertNull(match); + } + + @Test + public void matchHeadersCondition() { + this.request.getHeaders().add("foo", "bar"); + + RequestMappingInfo info = + new RequestMappingInfo( + new PatternsRequestCondition("/foo"), null, null, + new HeadersRequestCondition("foo=bar"), null, null, null); + RequestMappingInfo match = info.getMatchingCondition(this.exchange); + + assertNotNull(match); + + info = new RequestMappingInfo( + new PatternsRequestCondition("/foo"), null, null, + new HeadersRequestCondition("foo!=bar"), null, null, null); + match = info.getMatchingCondition(this.exchange); + + assertNull(match); + } + + @Test + public void matchConsumesCondition() { + this.request.getHeaders().setContentType(MediaType.TEXT_PLAIN); + + RequestMappingInfo info = + new RequestMappingInfo( + new PatternsRequestCondition("/foo"), null, null, null, + new ConsumesRequestCondition("text/plain"), null, null); + RequestMappingInfo match = info.getMatchingCondition(this.exchange); + + assertNotNull(match); + + info = new RequestMappingInfo( + new PatternsRequestCondition("/foo"), null, null, null, + new ConsumesRequestCondition("application/xml"), null, null); + match = info.getMatchingCondition(this.exchange); + + assertNull(match); + } + + @Test + public void matchProducesCondition() { + this.request.getHeaders().setAccept(Collections.singletonList(MediaType.TEXT_PLAIN)); + + RequestMappingInfo info = + new RequestMappingInfo( + new PatternsRequestCondition("/foo"), null, null, null, null, + new ProducesRequestCondition("text/plain"), null); + RequestMappingInfo match = info.getMatchingCondition(this.exchange); + + assertNotNull(match); + + info = new RequestMappingInfo( + new PatternsRequestCondition("/foo"), null, null, null, null, + new ProducesRequestCondition("application/xml"), null); + match = info.getMatchingCondition(this.exchange); + + assertNull(match); + } + + @Test + public void matchCustomCondition() { + this.request.getQueryParams().add("foo", "bar"); + + RequestMappingInfo info = + new RequestMappingInfo( + new PatternsRequestCondition("/foo"), null, null, null, null, null, + new ParamsRequestCondition("foo=bar")); + RequestMappingInfo match = info.getMatchingCondition(this.exchange); + + assertNotNull(match); + + info = new RequestMappingInfo( + new PatternsRequestCondition("/foo"), null, + new ParamsRequestCondition("foo!=bar"), null, null, null, + new ParamsRequestCondition("foo!=bar")); + match = info.getMatchingCondition(this.exchange); + + assertNull(match); + } + + @Test + public void compareTwoHttpMethodsOneParam() { + RequestMappingInfo none = new RequestMappingInfo(null, null, null, null, null, null, null); + RequestMappingInfo oneMethod = + new RequestMappingInfo(null, + new RequestMethodsRequestCondition(RequestMethod.GET), null, null, null, null, null); + RequestMappingInfo oneMethodOneParam = + new RequestMappingInfo(null, + new RequestMethodsRequestCondition(RequestMethod.GET), + new ParamsRequestCondition("foo"), null, null, null, null); + + Comparator comparator = (info, otherInfo) -> info.compareTo(otherInfo, exchange); + + List list = asList(none, oneMethod, oneMethodOneParam); + Collections.shuffle(list); + Collections.sort(list, comparator); + + assertEquals(oneMethodOneParam, list.get(0)); + assertEquals(oneMethod, list.get(1)); + assertEquals(none, list.get(2)); + } + + @Test + public void equals() { + RequestMappingInfo info1 = new RequestMappingInfo( + new PatternsRequestCondition("/foo"), + new RequestMethodsRequestCondition(RequestMethod.GET), + new ParamsRequestCondition("foo=bar"), + new HeadersRequestCondition("foo=bar"), + new ConsumesRequestCondition("text/plain"), + new ProducesRequestCondition("text/plain"), + new ParamsRequestCondition("customFoo=customBar")); + + RequestMappingInfo info2 = new RequestMappingInfo( + new PatternsRequestCondition("/foo"), + new RequestMethodsRequestCondition(RequestMethod.GET), + new ParamsRequestCondition("foo=bar"), + new HeadersRequestCondition("foo=bar"), + new ConsumesRequestCondition("text/plain"), + new ProducesRequestCondition("text/plain"), + new ParamsRequestCondition("customFoo=customBar")); + + assertEquals(info1, info2); + assertEquals(info1.hashCode(), info2.hashCode()); + + info2 = new RequestMappingInfo( + new PatternsRequestCondition("/foo", "/NOOOOOO"), + new RequestMethodsRequestCondition(RequestMethod.GET), + new ParamsRequestCondition("foo=bar"), + new HeadersRequestCondition("foo=bar"), + new ConsumesRequestCondition("text/plain"), + new ProducesRequestCondition("text/plain"), + new ParamsRequestCondition("customFoo=customBar")); + + assertFalse(info1.equals(info2)); + assertNotEquals(info1.hashCode(), info2.hashCode()); + + info2 = new RequestMappingInfo( + new PatternsRequestCondition("/foo"), + new RequestMethodsRequestCondition(RequestMethod.GET, RequestMethod.POST), + new ParamsRequestCondition("foo=bar"), + new HeadersRequestCondition("foo=bar"), + new ConsumesRequestCondition("text/plain"), + new ProducesRequestCondition("text/plain"), + new ParamsRequestCondition("customFoo=customBar")); + + assertFalse(info1.equals(info2)); + assertNotEquals(info1.hashCode(), info2.hashCode()); + + info2 = new RequestMappingInfo( + new PatternsRequestCondition("/foo"), + new RequestMethodsRequestCondition(RequestMethod.GET), + new ParamsRequestCondition("/NOOOOOO"), + new HeadersRequestCondition("foo=bar"), + new ConsumesRequestCondition("text/plain"), + new ProducesRequestCondition("text/plain"), + new ParamsRequestCondition("customFoo=customBar")); + + assertFalse(info1.equals(info2)); + assertNotEquals(info1.hashCode(), info2.hashCode()); + + info2 = new RequestMappingInfo( + new PatternsRequestCondition("/foo"), + new RequestMethodsRequestCondition(RequestMethod.GET), + new ParamsRequestCondition("foo=bar"), + new HeadersRequestCondition("/NOOOOOO"), + new ConsumesRequestCondition("text/plain"), + new ProducesRequestCondition("text/plain"), + new ParamsRequestCondition("customFoo=customBar")); + + assertFalse(info1.equals(info2)); + assertNotEquals(info1.hashCode(), info2.hashCode()); + + info2 = new RequestMappingInfo( + new PatternsRequestCondition("/foo"), + new RequestMethodsRequestCondition(RequestMethod.GET), + new ParamsRequestCondition("foo=bar"), + new HeadersRequestCondition("foo=bar"), + new ConsumesRequestCondition("text/NOOOOOO"), + new ProducesRequestCondition("text/plain"), + new ParamsRequestCondition("customFoo=customBar")); + + assertFalse(info1.equals(info2)); + assertNotEquals(info1.hashCode(), info2.hashCode()); + + info2 = new RequestMappingInfo( + new PatternsRequestCondition("/foo"), + new RequestMethodsRequestCondition(RequestMethod.GET), + new ParamsRequestCondition("foo=bar"), + new HeadersRequestCondition("foo=bar"), + new ConsumesRequestCondition("text/plain"), + new ProducesRequestCondition("text/NOOOOOO"), + new ParamsRequestCondition("customFoo=customBar")); + + assertFalse(info1.equals(info2)); + assertNotEquals(info1.hashCode(), info2.hashCode()); + + info2 = new RequestMappingInfo( + new PatternsRequestCondition("/foo"), + new RequestMethodsRequestCondition(RequestMethod.GET), + new ParamsRequestCondition("foo=bar"), + new HeadersRequestCondition("foo=bar"), + new ConsumesRequestCondition("text/plain"), + new ProducesRequestCondition("text/plain"), + new ParamsRequestCondition("customFoo=NOOOOOO")); + + assertFalse(info1.equals(info2)); + assertNotEquals(info1.hashCode(), info2.hashCode()); + } + + @Test + @Ignore + public void preFlightRequest() throws Exception { + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.OPTIONS, new URI("/foo")); + request.getHeaders().add(HttpHeaders.ORIGIN, "http://domain.com"); + request.getHeaders().add(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "POST"); + + WebSessionManager manager = mock(WebSessionManager.class); + MockServerHttpResponse response = new MockServerHttpResponse(); + ServerWebExchange exchange = new DefaultServerWebExchange(request, response, manager); + + RequestMappingInfo info = new RequestMappingInfo( + new PatternsRequestCondition("/foo"), new RequestMethodsRequestCondition(RequestMethod.POST), null, + null, null, null, null); + RequestMappingInfo match = info.getMatchingCondition(exchange); + assertNotNull(match); + + info = new RequestMappingInfo( + new PatternsRequestCondition("/foo"), new RequestMethodsRequestCondition(RequestMethod.OPTIONS), null, + null, null, null, null); + match = info.getMatchingCondition(exchange); + assertNull("Pre-flight should match the ACCESS_CONTROL_REQUEST_METHOD", match); + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/RequestMethodsRequestConditionTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/RequestMethodsRequestConditionTests.java new file mode 100644 index 0000000000..cce2b92ee0 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/RequestMethodsRequestConditionTests.java @@ -0,0 +1,157 @@ +/* + * 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.web.reactive.result.condition; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Collections; + +import org.junit.Ignore; +import org.junit.Test; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.WebSessionManager; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.springframework.web.bind.annotation.RequestMethod.DELETE; +import static org.springframework.web.bind.annotation.RequestMethod.GET; +import static org.springframework.web.bind.annotation.RequestMethod.HEAD; +import static org.springframework.web.bind.annotation.RequestMethod.OPTIONS; +import static org.springframework.web.bind.annotation.RequestMethod.POST; +import static org.springframework.web.bind.annotation.RequestMethod.PUT; + +/** + * Unit tests for {@link RequestMethodsRequestCondition}. + * + * @author Rossen Stoyanchev + */ +public class RequestMethodsRequestConditionTests { + + // TODO: custom method, CORS pre-flight (see @Ignored) + + @Test + public void getMatchingCondition() throws Exception { + testMatch(new RequestMethodsRequestCondition(GET), GET); + testMatch(new RequestMethodsRequestCondition(GET, POST), GET); + testNoMatch(new RequestMethodsRequestCondition(GET), POST); + } + + @Test + public void getMatchingConditionWithHttpHead() throws Exception { + testMatch(new RequestMethodsRequestCondition(HEAD), HEAD); + testMatch(new RequestMethodsRequestCondition(GET), HEAD); + testNoMatch(new RequestMethodsRequestCondition(POST), HEAD); + } + + @Test + public void getMatchingConditionWithEmptyConditions() throws Exception { + RequestMethodsRequestCondition condition = new RequestMethodsRequestCondition(); + for (RequestMethod method : RequestMethod.values()) { + if (!OPTIONS.equals(method)) { + ServerWebExchange exchange = createExchange(method.name()); + assertNotNull(condition.getMatchingCondition(exchange)); + } + } + testNoMatch(condition, OPTIONS); + } + + @Test + @Ignore + public void getMatchingConditionWithCustomMethod() throws Exception { + ServerWebExchange exchange = createExchange("PROPFIND"); + assertNotNull(new RequestMethodsRequestCondition().getMatchingCondition(exchange)); + assertNull(new RequestMethodsRequestCondition(GET, POST).getMatchingCondition(exchange)); + } + + @Test + @Ignore + public void getMatchingConditionWithCorsPreFlight() throws Exception { + ServerWebExchange exchange = createExchange("OPTIONS"); + exchange.getRequest().getHeaders().add("Origin", "http://example.com"); + exchange.getRequest().getHeaders().add(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + + assertNotNull(new RequestMethodsRequestCondition().getMatchingCondition(exchange)); + assertNotNull(new RequestMethodsRequestCondition(PUT).getMatchingCondition(exchange)); + assertNull(new RequestMethodsRequestCondition(DELETE).getMatchingCondition(exchange)); + } + + @Test + public void compareTo() throws Exception { + RequestMethodsRequestCondition c1 = new RequestMethodsRequestCondition(GET, HEAD); + RequestMethodsRequestCondition c2 = new RequestMethodsRequestCondition(POST); + RequestMethodsRequestCondition c3 = new RequestMethodsRequestCondition(); + + ServerWebExchange exchange = createExchange(); + + int result = c1.compareTo(c2, exchange); + assertTrue("Invalid comparison result: " + result, result < 0); + + result = c2.compareTo(c1, exchange); + assertTrue("Invalid comparison result: " + result, result > 0); + + result = c2.compareTo(c3, exchange); + assertTrue("Invalid comparison result: " + result, result < 0); + + result = c1.compareTo(c1, exchange); + assertEquals("Invalid comparison result ", 0, result); + } + + @Test + public void combine() { + RequestMethodsRequestCondition condition1 = new RequestMethodsRequestCondition(GET); + RequestMethodsRequestCondition condition2 = new RequestMethodsRequestCondition(POST); + + RequestMethodsRequestCondition result = condition1.combine(condition2); + assertEquals(2, result.getContent().size()); + } + + + private void testMatch(RequestMethodsRequestCondition condition, RequestMethod method) throws Exception { + ServerWebExchange exchange = createExchange(method.name()); + RequestMethodsRequestCondition actual = condition.getMatchingCondition(exchange); + assertNotNull(actual); + assertEquals(Collections.singleton(method), actual.getContent()); + } + + private void testNoMatch(RequestMethodsRequestCondition condition, RequestMethod method) throws Exception { + ServerWebExchange exchange = createExchange(method.name()); + assertNull(condition.getMatchingCondition(exchange)); + } + + + private ServerWebExchange createExchange() throws URISyntaxException { + return createExchange(null); + } + + private ServerWebExchange createExchange(String method) throws URISyntaxException { + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.resolve(method), new URI("/")); + WebSessionManager sessionManager = mock(WebSessionManager.class); + return new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); + } + +} From 4431a76a054148cf5fe6d14b2d7a659ccbfb30eb Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Fri, 15 Apr 2016 21:25:56 +0100 Subject: [PATCH 296/498] Update addCookie signature and improve client cookie --- .../reactive/ReactorClientHttpRequest.java | 96 +------------------ .../reactive/ReactorServerHttpResponse.java | 2 +- 2 files changed, 3 insertions(+), 95 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java index 6ab0654123..16a192a5bc 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java @@ -17,11 +17,9 @@ package org.springframework.http.client.reactive; import java.net.URI; -import java.time.Duration; import java.util.Collection; -import java.util.Optional; -import io.netty.handler.codec.http.cookie.Cookie; +import io.netty.handler.codec.http.cookie.DefaultCookie; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -30,10 +28,8 @@ import reactor.io.netty.http.HttpClient; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferAllocator; -import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; -import org.springframework.http.ResponseCookie; /** * {@link ClientHttpRequest} implementation for the Reactor Net HTTP client @@ -110,7 +106,7 @@ public class ReactorClientHttpRequest extends AbstractClientHttpRequest { getHeaders().entrySet().stream().forEach(e -> channel.headers().set(e.getKey(), e.getValue())); getCookies().values().stream().flatMap(Collection::stream).forEach(cookie -> - channel.addCookie(cookie.getName(), new NettyCookie(cookie))); + channel.addCookie(new DefaultCookie(cookie.getName(), cookie.getValue()))); return Mono.empty(); }) .after(() -> { @@ -125,95 +121,7 @@ public class ReactorClientHttpRequest extends AbstractClientHttpRequest { .map(httpChannel -> new ReactorClientHttpResponse(httpChannel, allocator)); } - private final static class NettyCookie implements Cookie { - private final HttpCookie httpCookie; - - - public NettyCookie(HttpCookie httpCookie) { - this.httpCookie = httpCookie; - } - - @Override - public String name() { - return this.httpCookie.getName(); - } - - @Override - public String value() { - return this.httpCookie.getValue(); - } - - @Override - public boolean isHttpOnly() { - return true; - } - - @Override - public long maxAge() { - return -1; - } - - @Override - public String domain() { - return null; - } - - @Override - public String path() { - return null; - } - - @Override - public void setValue(String value) { - throw new UnsupportedOperationException("Read-Only Cookie"); - } - - @Override - public boolean wrap() { - return false; - } - - @Override - public void setWrap(boolean wrap) { - throw new UnsupportedOperationException("Read-Only Cookie"); - } - - @Override - public void setDomain(String domain) { - throw new UnsupportedOperationException("Read-Only Cookie"); - } - - @Override - public void setPath(String path) { - throw new UnsupportedOperationException("Read-Only Cookie"); - } - - @Override - public void setMaxAge(long maxAge) { - throw new UnsupportedOperationException("Read-Only Cookie"); - } - - @Override - public void setSecure(boolean secure) { - throw new UnsupportedOperationException("Read-Only Cookie"); - } - - @Override - public void setHttpOnly(boolean httpOnly) { - throw new UnsupportedOperationException("Read-Only Cookie"); - } - - @Override - public int compareTo(Cookie o) { - return httpCookie.getName().compareTo(o.name()); - } - - @Override - public boolean isSecure() { - return false; - } - } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java index 1578aa0490..ff44c2b1c2 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java @@ -85,7 +85,7 @@ public class ReactorServerHttpResponse extends AbstractServerHttpResponse { httpCookie.getPath().ifPresent(cookie::setPath); cookie.setSecure(httpCookie.isSecure()); cookie.setHttpOnly(httpCookie.isHttpOnly()); - this.channel.addResponseCookie(name, cookie); + this.channel.addResponseCookie(cookie); } } } From 9a4b98d2e990574a5a14c3cdc8b6577d4ab797a0 Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Fri, 15 Apr 2016 23:23:54 +0100 Subject: [PATCH 297/498] Update HttpClient with Inbound/Outbound contract split --- .../client/reactive/ReactorClientHttpRequest.java | 2 +- .../client/reactive/ReactorClientHttpResponse.java | 12 ++++++------ .../server/reactive/ReactorHttpHandlerAdapter.java | 4 ++-- .../server/reactive/ReactorServerHttpResponse.java | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java index 16a192a5bc..fc589a1463 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java @@ -111,7 +111,7 @@ public class ReactorClientHttpRequest extends AbstractClientHttpRequest { }) .after(() -> { if (body != null) { - return channel.send(body); + return channel.sendBody(body); } else { return channel.sendHeaders(); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpResponse.java index 019256ac77..6c7d7a8ac4 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpResponse.java @@ -19,7 +19,7 @@ package org.springframework.http.client.reactive; import java.util.Collection; import reactor.core.publisher.Flux; -import reactor.io.netty.http.HttpChannel; +import reactor.io.netty.http.HttpInbound; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferAllocator; @@ -40,29 +40,29 @@ public class ReactorClientHttpResponse implements ClientHttpResponse { private final DataBufferAllocator allocator; - private final HttpChannel channel; + private final HttpInbound channel; - public ReactorClientHttpResponse(HttpChannel channel, DataBufferAllocator allocator) { + public ReactorClientHttpResponse(HttpInbound channel, DataBufferAllocator allocator) { this.allocator = allocator; this.channel = channel; } @Override public Flux getBody() { - return channel.receive().map(b -> allocator.wrap(b.byteBuffer())); + return channel.receiveBody().map(b -> allocator.wrap(b.byteBuffer())); } @Override public HttpHeaders getHeaders() { HttpHeaders headers = new HttpHeaders(); - this.channel.responseHeaders().entries().stream().forEach(e -> headers.add(e.getKey(), e.getValue())); + this.channel.headers().entries().stream().forEach(e -> headers.add(e.getKey(), e.getValue())); return headers; } @Override public HttpStatus getStatusCode() { - return HttpStatus.valueOf(this.channel.responseStatus().code()); + return HttpStatus.valueOf(this.channel.status().code()); } @Override diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorHttpHandlerAdapter.java index ca3cddb280..f6abc3bb16 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorHttpHandlerAdapter.java @@ -18,7 +18,7 @@ package org.springframework.http.server.reactive; import reactor.core.publisher.Mono; import reactor.io.buffer.Buffer; -import reactor.io.ipc.ChannelFluxHandler; +import reactor.io.ipc.ChannelHandler; import reactor.io.netty.http.HttpChannel; import org.springframework.core.io.buffer.DataBufferAllocator; @@ -28,7 +28,7 @@ import org.springframework.util.Assert; * @author Stephane Maldini */ public class ReactorHttpHandlerAdapter - implements ChannelFluxHandler { + implements ChannelHandler { private final HttpHandler httpHandler; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java index ff44c2b1c2..fbd9f7661a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java @@ -55,7 +55,7 @@ public class ReactorServerHttpResponse extends AbstractServerHttpResponse { @Override public void setStatusCode(HttpStatus status) { - getReactorChannel().responseStatus(HttpResponseStatus.valueOf(status.value())); + getReactorChannel().status(HttpResponseStatus.valueOf(status.value())); } @Override From 0a3396c5b293089fc8f4b2b3a6c12a80ab2508c7 Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Sat, 16 Apr 2016 01:05:30 +0100 Subject: [PATCH 298/498] Polish contracts --- .../http/client/reactive/ReactorClientHttpResponse.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpResponse.java index 6c7d7a8ac4..c45181c699 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpResponse.java @@ -56,7 +56,7 @@ public class ReactorClientHttpResponse implements ClientHttpResponse { @Override public HttpHeaders getHeaders() { HttpHeaders headers = new HttpHeaders(); - this.channel.headers().entries().stream().forEach(e -> headers.add(e.getKey(), e.getValue())); + this.channel.responseHeaders().entries().stream().forEach(e -> headers.add(e.getKey(), e.getValue())); return headers; } From ad9d8c28fee58d9ae25de6689ab9d31238f18337 Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Sun, 17 Apr 2016 14:50:20 +0100 Subject: [PATCH 299/498] Polish reactor-ipc/netty contracts --- .../http/client/reactive/ReactorClientHttpRequest.java | 2 +- .../http/client/reactive/ReactorClientHttpResponse.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java index fc589a1463..16a192a5bc 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java @@ -111,7 +111,7 @@ public class ReactorClientHttpRequest extends AbstractClientHttpRequest { }) .after(() -> { if (body != null) { - return channel.sendBody(body); + return channel.send(body); } else { return channel.sendHeaders(); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpResponse.java index c45181c699..9b3757caf2 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpResponse.java @@ -50,7 +50,7 @@ public class ReactorClientHttpResponse implements ClientHttpResponse { @Override public Flux getBody() { - return channel.receiveBody().map(b -> allocator.wrap(b.byteBuffer())); + return channel.receive().map(b -> allocator.wrap(b.byteBuffer())); } @Override From 0e5a892bad25a3ebcf018763c46fc6c8cfa15de8 Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Mon, 18 Apr 2016 17:49:32 +0100 Subject: [PATCH 300/498] Use ByteBuf instead of Buffer in reactor-netty --- .../reactive/ReactorClientHttpRequest.java | 25 +++++++++++----- .../reactive/ReactorClientHttpResponse.java | 3 +- .../ReactorHttpClientRequestFactory.java | 14 +++------ .../reactive/ReactorHttpHandlerAdapter.java | 15 +++++----- .../reactive/ReactorServerHttpRequest.java | 10 +++---- .../reactive/ReactorServerHttpResponse.java | 15 ++++++++-- .../reactive/boot/ReactorHttpServer.java | 11 +------ .../reactive/WebClientIntegrationTests.java | 29 +++++++++---------- 8 files changed, 61 insertions(+), 61 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java index 16a192a5bc..a8331a50aa 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java @@ -19,15 +19,19 @@ package org.springframework.http.client.reactive; import java.net.URI; import java.util.Collection; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; import io.netty.handler.codec.http.cookie.DefaultCookie; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import reactor.io.buffer.Buffer; import reactor.io.netty.http.HttpClient; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferAllocator; +import org.springframework.core.io.buffer.DefaultDataBufferAllocator; +import org.springframework.core.io.buffer.NettyDataBuffer; +import org.springframework.core.io.buffer.NettyDataBufferAllocator; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -47,13 +51,13 @@ public class ReactorClientHttpRequest extends AbstractClientHttpRequest { private final HttpClient httpClient; - private Flux body; + private Flux body; - public ReactorClientHttpRequest(HttpMethod httpMethod, URI uri, HttpClient httpClient, HttpHeaders headers, - DataBufferAllocator allocator) { + public ReactorClientHttpRequest(HttpMethod httpMethod, URI uri, HttpClient httpClient, HttpHeaders headers) { super(headers); - this.allocator = allocator; + //FIXME use Netty allocator + this.allocator = new DefaultDataBufferAllocator(); this.httpMethod = httpMethod; this.uri = uri; this.httpClient = httpClient; @@ -88,7 +92,7 @@ public class ReactorClientHttpRequest extends AbstractClientHttpRequest { @Override public Mono setBody(Publisher body) { - this.body = Flux.from(body).map(b -> new Buffer(b.asByteBuffer())); + this.body = Flux.from(body).map(this::toByteBuf); return Mono.empty(); } @@ -121,7 +125,14 @@ public class ReactorClientHttpRequest extends AbstractClientHttpRequest { .map(httpChannel -> new ReactorClientHttpResponse(httpChannel, allocator)); } - + private ByteBuf toByteBuf(DataBuffer buffer) { + if (buffer instanceof NettyDataBuffer) { + return ((NettyDataBuffer) buffer).getNativeBuffer(); + } + else { + return Unpooled.wrappedBuffer(buffer.asByteBuffer()); + } + } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpResponse.java index 9b3757caf2..8e687132b7 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpResponse.java @@ -23,6 +23,7 @@ import reactor.io.netty.http.HttpInbound; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferAllocator; +import org.springframework.core.io.buffer.NettyDataBufferAllocator; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseCookie; @@ -50,7 +51,7 @@ public class ReactorClientHttpResponse implements ClientHttpResponse { @Override public Flux getBody() { - return channel.receive().map(b -> allocator.wrap(b.byteBuffer())); + return channel.receiveByteBuffer().map(allocator::wrap); } @Override diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorHttpClientRequestFactory.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorHttpClientRequestFactory.java index ce0cd81812..ad1a9aad42 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorHttpClientRequestFactory.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorHttpClientRequestFactory.java @@ -22,6 +22,7 @@ import reactor.io.netty.http.HttpClient; import org.springframework.core.io.buffer.DataBufferAllocator; import org.springframework.core.io.buffer.DefaultDataBufferAllocator; +import org.springframework.core.io.buffer.NettyDataBufferAllocator; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.util.Assert; @@ -33,20 +34,13 @@ import org.springframework.util.Assert; */ public class ReactorHttpClientRequestFactory implements ClientHttpRequestFactory { - private final DataBufferAllocator allocator; - private final HttpClient httpClient; public ReactorHttpClientRequestFactory() { - this(new DefaultDataBufferAllocator()); + this(reactor.io.netty.http.HttpClient.create()); } - public ReactorHttpClientRequestFactory(DataBufferAllocator allocator) { - this(allocator, reactor.io.netty.http.HttpClient.create()); - } - - protected ReactorHttpClientRequestFactory(DataBufferAllocator allocator, HttpClient httpClient) { - this.allocator = allocator; + protected ReactorHttpClientRequestFactory(HttpClient httpClient) { this.httpClient = httpClient; } @@ -56,7 +50,7 @@ public class ReactorHttpClientRequestFactory implements ClientHttpRequestFactory Assert.notNull(uri, "request URI is required"); Assert.notNull(headers, "request headers are required"); - return new ReactorClientHttpRequest(httpMethod, uri, this.httpClient, headers, this.allocator); + return new ReactorClientHttpRequest(httpMethod, uri, this.httpClient, headers); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorHttpHandlerAdapter.java index f6abc3bb16..24358893b4 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorHttpHandlerAdapter.java @@ -16,33 +16,32 @@ package org.springframework.http.server.reactive; +import io.netty.buffer.ByteBuf; import reactor.core.publisher.Mono; -import reactor.io.buffer.Buffer; import reactor.io.ipc.ChannelHandler; import reactor.io.netty.http.HttpChannel; -import org.springframework.core.io.buffer.DataBufferAllocator; +import org.springframework.core.io.buffer.NettyDataBufferAllocator; import org.springframework.util.Assert; /** * @author Stephane Maldini */ public class ReactorHttpHandlerAdapter - implements ChannelHandler { + implements ChannelHandler { private final HttpHandler httpHandler; - private final DataBufferAllocator allocator; - - public ReactorHttpHandlerAdapter(HttpHandler httpHandler, - DataBufferAllocator allocator) { + public ReactorHttpHandlerAdapter(HttpHandler httpHandler) { Assert.notNull(httpHandler, "'httpHandler' is required."); this.httpHandler = httpHandler; - this.allocator = allocator; } @Override public Mono apply(HttpChannel channel) { + NettyDataBufferAllocator allocator = + new NettyDataBufferAllocator(channel.delegate().alloc()); + ReactorServerHttpRequest adaptedRequest = new ReactorServerHttpRequest(channel, allocator); ReactorServerHttpResponse adaptedResponse = diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java index 5183fca307..ba52096395 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java @@ -25,6 +25,7 @@ import reactor.io.netty.http.HttpChannel; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferAllocator; +import org.springframework.core.io.buffer.NettyDataBufferAllocator; import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -41,10 +42,10 @@ public class ReactorServerHttpRequest extends AbstractServerHttpRequest { private final HttpChannel channel; - private final DataBufferAllocator allocator; + private final NettyDataBufferAllocator allocator; public ReactorServerHttpRequest(HttpChannel request, - DataBufferAllocator allocator) { + NettyDataBufferAllocator allocator) { Assert.notNull("'request' must not be null"); Assert.notNull(allocator, "'allocator' must not be null"); this.channel = request; @@ -89,10 +90,7 @@ public class ReactorServerHttpRequest extends AbstractServerHttpRequest { @Override public Flux getBody() { - return Flux.from(this.channel.receive()).map(bytes -> { - ByteBuffer byteBuffer = bytes.byteBuffer(); - return allocator.wrap(byteBuffer); - }); + return this.channel.receive().map(allocator::wrap); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java index fbd9f7661a..8eabb39314 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java @@ -16,17 +16,19 @@ package org.springframework.http.server.reactive; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.cookie.Cookie; import io.netty.handler.codec.http.cookie.DefaultCookie; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import reactor.io.buffer.Buffer; import reactor.io.netty.http.HttpChannel; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferAllocator; +import org.springframework.core.io.buffer.NettyDataBuffer; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseCookie; import org.springframework.util.Assert; @@ -60,8 +62,7 @@ public class ReactorServerHttpResponse extends AbstractServerHttpResponse { @Override protected Mono setBodyInternal(Publisher publisher) { - return Mono.from(this.channel.send( - Flux.from(publisher).map(buffer -> new Buffer(buffer.asByteBuffer())))); + return this.channel.send(Flux.from(publisher).map(this::toByteBuf)); } @Override @@ -90,4 +91,12 @@ public class ReactorServerHttpResponse extends AbstractServerHttpResponse { } } + private ByteBuf toByteBuf(DataBuffer buffer) { + if (buffer instanceof NettyDataBuffer) { + return ((NettyDataBuffer) buffer).getNativeBuffer(); + } + else { + return Unpooled.wrappedBuffer(buffer.asByteBuffer()); + } + } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java index 82b1e08dec..b83e2d0fa0 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java @@ -18,10 +18,7 @@ package org.springframework.http.server.reactive.boot; import reactor.core.flow.Loopback; import reactor.core.state.Completable; -import reactor.io.buffer.Buffer; -import org.springframework.core.io.buffer.DataBufferAllocator; -import org.springframework.core.io.buffer.DefaultDataBufferAllocator; import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter; import org.springframework.util.Assert; @@ -35,19 +32,13 @@ public class ReactorHttpServer extends HttpServerSupport private reactor.io.netty.http.HttpServer reactorServer; - private DataBufferAllocator allocator = new DefaultDataBufferAllocator(); - private boolean running; - public void setAllocator(DataBufferAllocator allocator) { - this.allocator = allocator; - } - @Override public void afterPropertiesSet() throws Exception { Assert.notNull(getHttpHandler()); - this.reactorHandler = new ReactorHttpHandlerAdapter(getHttpHandler(), allocator); + this.reactorHandler = new ReactorHttpHandlerAdapter(getHttpHandler()); this.reactorServer = (getPort() != -1 ? reactor.io.netty.http.HttpServer.create(getPort()) : reactor.io.netty.http.HttpServer.create()); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/client/reactive/WebClientIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/client/reactive/WebClientIntegrationTests.java index 5f37a963fb..7557e84082 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/client/reactive/WebClientIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/client/reactive/WebClientIntegrationTests.java @@ -65,7 +65,7 @@ public class WebClientIntegrationTests { .perform(get(baseUrl.toString())) .extract(headers()); - TestSubscriber ts = new TestSubscriber(); + TestSubscriber ts = new TestSubscriber<>(); result.subscribe(ts); ts.awaitAndAssertNextValuesWith( httpHeaders -> { @@ -92,7 +92,7 @@ public class WebClientIntegrationTests { .extract(body(String.class)); - TestSubscriber ts = new TestSubscriber(); + TestSubscriber ts = new TestSubscriber<>(); result.subscribe(ts); ts.awaitAndAssertNextValues("Hello Spring!").assertComplete(); @@ -114,15 +114,12 @@ public class WebClientIntegrationTests { .accept(MediaType.TEXT_PLAIN)) .extract(response(String.class)); - TestSubscriber> ts = new TestSubscriber(); + TestSubscriber> ts = new TestSubscriber<>(); result.subscribe(ts); - ts.awaitAndAssertNextValuesWith(new Consumer>() { - @Override - public void accept(ResponseEntity response) { - assertEquals(200, response.getStatusCode().value()); - assertEquals(MediaType.TEXT_PLAIN, response.getHeaders().getContentType()); - assertEquals("Hello Spring!", response.getBody()); - } + ts.awaitAndAssertNextValuesWith((Consumer>) response -> { + assertEquals(200, response.getStatusCode().value()); + assertEquals(MediaType.TEXT_PLAIN, response.getHeaders().getContentType()); + assertEquals("Hello Spring!", response.getBody()); }); RecordedRequest request = server.takeRequest(); assertEquals(1, server.getRequestCount()); @@ -143,7 +140,7 @@ public class WebClientIntegrationTests { .accept(MediaType.APPLICATION_JSON)) .extract(body(String.class)); - TestSubscriber ts = new TestSubscriber(); + TestSubscriber ts = new TestSubscriber<>(); result.subscribe(ts); ts.awaitAndAssertNextValues(content).assertComplete(); RecordedRequest request = server.takeRequest(); @@ -164,7 +161,7 @@ public class WebClientIntegrationTests { .accept(MediaType.APPLICATION_JSON)) .extract(body(Pojo.class)); - TestSubscriber ts = new TestSubscriber(); + TestSubscriber ts = new TestSubscriber<>(); result.subscribe(ts); ts.awaitAndAssertNextValuesWith(p -> assertEquals("barbar", p.getBar())).assertComplete(); RecordedRequest request = server.takeRequest(); @@ -185,7 +182,7 @@ public class WebClientIntegrationTests { .accept(MediaType.APPLICATION_JSON)) .extract(bodyStream(Pojo.class)); - TestSubscriber ts = new TestSubscriber(); + TestSubscriber ts = new TestSubscriber<>(); result.subscribe(ts); ts.awaitAndAssertNextValuesWith( p -> assertThat(p.getBar(), Matchers.is("bar1")), @@ -209,7 +206,7 @@ public class WebClientIntegrationTests { .accept(MediaType.APPLICATION_JSON)) .extract(responseStream(Pojo.class)); - TestSubscriber>> ts = new TestSubscriber(); + TestSubscriber>> ts = new TestSubscriber<>(); result.subscribe(ts); ts.awaitAndAssertNextValuesWith( response -> { @@ -237,7 +234,7 @@ public class WebClientIntegrationTests { .accept(MediaType.APPLICATION_JSON)) .extract(body(Pojo.class)); - TestSubscriber ts = new TestSubscriber(); + TestSubscriber ts = new TestSubscriber<>(); result.subscribe(ts); ts.awaitAndAssertNextValuesWith(p -> assertEquals("BARBAR", p.getBar())).assertComplete(); @@ -261,7 +258,7 @@ public class WebClientIntegrationTests { .extract(body(String.class)); - TestSubscriber ts = new TestSubscriber(); + TestSubscriber ts = new TestSubscriber<>(); result.subscribe(ts); // TODO: error message should be converted to a ClientException ts.await().assertError(); From 0f44fedd19f48670016502a185d9f296328a8584 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 18 Apr 2016 13:02:50 -0400 Subject: [PATCH 301/498] Complete RequestMappingHandlerMapping This commit adds RequestMappingInfoHandlerMapping and RequestMappingHandlerMapping with support equivalent to that in spring-webmvc. --- .../web/reactive/HandlerMapping.java | 39 ++ .../RequestMappingInfoHandlerMapping.java | 336 +++++++++++ .../RequestMappingHandlerMapping.java | 348 ++++++++---- .../web/util/HttpRequestPathHelper.java | 55 +- ...RequestMappingInfoHandlerMappingTests.java | 535 ++++++++++++++++++ .../RequestMappingHandlerMappingTests.java | 251 +++++--- 6 files changed, 1375 insertions(+), 189 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerMapping.java index 9dd0ccb072..6253cb8020 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerMapping.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerMapping.java @@ -29,6 +29,45 @@ import org.springframework.web.server.ServerWebExchange; */ public interface HandlerMapping { + /** + * Name of the {@link ServerWebExchange} attribute that contains the + * best matching pattern within the handler mapping. + *

    Note: This attribute is not required to be supported by all + * HandlerMapping implementations. URL-based HandlerMappings will + * typically support it, but handlers should not necessarily expect + * this request attribute to be present in all scenarios. + */ + String BEST_MATCHING_PATTERN_ATTRIBUTE = HandlerMapping.class.getName() + ".bestMatchingPattern"; + + /** + * Name of the {@link ServerWebExchange} attribute that contains the URI + * templates map, mapping variable names to values. + *

    Note: This attribute is not required to be supported by all + * HandlerMapping implementations. URL-based HandlerMappings will + * typically support it, but handlers should not necessarily expect + * this request attribute to be present in all scenarios. + */ + String URI_TEMPLATE_VARIABLES_ATTRIBUTE = HandlerMapping.class.getName() + ".uriTemplateVariables"; + + /** + * Name of the {@link ServerWebExchange} attribute that contains a map with + * URI matrix variables. + *

    Note: This attribute is not required to be supported by all + * HandlerMapping implementations and may also not be present depending on + * whether the HandlerMapping is configured to keep matrix variable content + * in the request URI. + */ + String MATRIX_VARIABLES_ATTRIBUTE = HandlerMapping.class.getName() + ".matrixVariables"; + + /** + * Name of the {@link ServerWebExchange} attribute that contains the set of + * producible MediaTypes applicable to the mapped handler. + *

    Note: This attribute is not required to be supported by all + * HandlerMapping implementations. Handlers should not necessarily expect + * this request attribute to be present in all scenarios. + */ + String PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE = HandlerMapping.class.getName() + ".producibleMediaTypes"; + /** * Return a handler for this request. * @param exchange current server exchange diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java new file mode 100644 index 0000000000..7e29549416 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java @@ -0,0 +1,336 @@ +/* + * 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.web.reactive.result.method; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.InvalidMediaTypeException; +import org.springframework.http.MediaType; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.util.CollectionUtils; +import org.springframework.util.MultiValueMap; +import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.HttpMediaTypeNotSupportedException; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.UnsatisfiedServletRequestParameterException; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.reactive.HandlerMapping; +import org.springframework.web.reactive.result.condition.NameValueExpression; +import org.springframework.web.reactive.result.condition.ParamsRequestCondition; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.util.WebUtils; + +/** + * Abstract base class for classes for which {@link RequestMappingInfo} defines + * the mapping between a request and a handler method. + * + * @author Rossen Stoyanchev + */ +public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMethodMapping { + + private static final Method HTTP_OPTIONS_HANDLE_METHOD; + + static { + try { + HTTP_OPTIONS_HANDLE_METHOD = HttpOptionsHandler.class.getMethod("handle"); + } + catch (NoSuchMethodException ex) { + // Should never happen + throw new IllegalStateException("No handler for HTTP OPTIONS", ex); + } + } + + + /** + * Get the URL path patterns associated with this {@link RequestMappingInfo}. + */ + @Override + protected Set getMappingPathPatterns(RequestMappingInfo info) { + return info.getPatternsCondition().getPatterns(); + } + + /** + * Check if the given RequestMappingInfo matches the current request and + * return a (potentially new) instance with conditions that match the + * current request -- for example with a subset of URL patterns. + * @return an info in case of a match; or {@code null} otherwise. + */ + @Override + protected RequestMappingInfo getMatchingMapping(RequestMappingInfo info, ServerWebExchange exchange) { + return info.getMatchingCondition(exchange); + } + + /** + * Provide a Comparator to sort RequestMappingInfos matched to a request. + */ + @Override + protected Comparator getMappingComparator(final ServerWebExchange exchange) { + return (info1, info2) -> info1.compareTo(info2, exchange); + } + + /** + * Expose URI template variables, matrix variables, and producible media types in the request. + * @see HandlerMapping#URI_TEMPLATE_VARIABLES_ATTRIBUTE + * @see HandlerMapping#MATRIX_VARIABLES_ATTRIBUTE + * @see HandlerMapping#PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE + */ + @Override + protected void handleMatch(RequestMappingInfo info, String lookupPath, ServerWebExchange exchange) { + super.handleMatch(info, lookupPath, exchange); + + String bestPattern; + Map uriVariables; + Map decodedUriVariables; + + Set patterns = info.getPatternsCondition().getPatterns(); + if (patterns.isEmpty()) { + bestPattern = lookupPath; + uriVariables = Collections.emptyMap(); + decodedUriVariables = Collections.emptyMap(); + } + else { + bestPattern = patterns.iterator().next(); + uriVariables = getPathMatcher().extractUriTemplateVariables(bestPattern, lookupPath); + decodedUriVariables = getPathHelper().decodePathVariables(exchange, uriVariables); + } + + exchange.getAttributes().put(BEST_MATCHING_PATTERN_ATTRIBUTE, bestPattern); + exchange.getAttributes().put(URI_TEMPLATE_VARIABLES_ATTRIBUTE, decodedUriVariables); + + Map> matrixVars = extractMatrixVariables(exchange, uriVariables); + exchange.getAttributes().put(MATRIX_VARIABLES_ATTRIBUTE, matrixVars); + + if (!info.getProducesCondition().getProducibleMediaTypes().isEmpty()) { + Set mediaTypes = info.getProducesCondition().getProducibleMediaTypes(); + exchange.getAttributes().put(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, mediaTypes); + } + } + + private Map> extractMatrixVariables( + ServerWebExchange exchange, Map uriVariables) { + + Map> result = new LinkedHashMap<>(); + for (Entry uriVar : uriVariables.entrySet()) { + String uriVarValue = uriVar.getValue(); + + int equalsIndex = uriVarValue.indexOf('='); + if (equalsIndex == -1) { + continue; + } + + String matrixVariables; + + int semicolonIndex = uriVarValue.indexOf(';'); + if ((semicolonIndex == -1) || (semicolonIndex == 0) || (equalsIndex < semicolonIndex)) { + matrixVariables = uriVarValue; + } + else { + matrixVariables = uriVarValue.substring(semicolonIndex + 1); + uriVariables.put(uriVar.getKey(), uriVarValue.substring(0, semicolonIndex)); + } + + MultiValueMap vars = WebUtils.parseMatrixVariables(matrixVariables); + result.put(uriVar.getKey(), getPathHelper().decodeMatrixVariables(exchange, vars)); + } + return result; + } + + /** + * Iterate all RequestMappingInfos once again, look if any match by URL at + * least and raise exceptions accordingly. + * @throws HttpRequestMethodNotSupportedException if there are matches by URL + * but not by HTTP method + * @throws HttpMediaTypeNotAcceptableException if there are matches by URL + * but not by consumable/producible media types + */ + @Override + protected HandlerMethod handleNoMatch(Set requestMappingInfos, + String lookupPath, ServerWebExchange exchange) throws Exception { + + Set allowedMethods = new LinkedHashSet<>(4); + + Set patternMatches = new HashSet<>(); + Set patternAndMethodMatches = new HashSet<>(); + + for (RequestMappingInfo info : requestMappingInfos) { + if (info.getPatternsCondition().getMatchingCondition(exchange) != null) { + patternMatches.add(info); + if (info.getMethodsCondition().getMatchingCondition(exchange) != null) { + patternAndMethodMatches.add(info); + } + else { + for (RequestMethod method : info.getMethodsCondition().getMethods()) { + allowedMethods.add(method.name()); + } + } + } + } + + ServerHttpRequest request = exchange.getRequest(); + if (patternMatches.isEmpty()) { + return null; + } + else if (patternAndMethodMatches.isEmpty()) { + HttpMethod httpMethod = request.getMethod(); + if (HttpMethod.OPTIONS.matches(httpMethod.name())) { + HttpOptionsHandler handler = new HttpOptionsHandler(allowedMethods); + return new HandlerMethod(handler, HTTP_OPTIONS_HANDLE_METHOD); + } + else if (!allowedMethods.isEmpty()) { + throw new HttpRequestMethodNotSupportedException(httpMethod.name(), allowedMethods); + } + } + + Set consumableMediaTypes; + Set producibleMediaTypes; + List paramConditions; + + if (patternAndMethodMatches.isEmpty()) { + consumableMediaTypes = getConsumableMediaTypes(exchange, patternMatches); + producibleMediaTypes = getProducibleMediaTypes(exchange, patternMatches); + paramConditions = getRequestParams(exchange, patternMatches); + } + else { + consumableMediaTypes = getConsumableMediaTypes(exchange, patternAndMethodMatches); + producibleMediaTypes = getProducibleMediaTypes(exchange, patternAndMethodMatches); + paramConditions = getRequestParams(exchange, patternAndMethodMatches); + } + + if (!consumableMediaTypes.isEmpty()) { + MediaType contentType; + try { + contentType = request.getHeaders().getContentType(); + } + catch (InvalidMediaTypeException ex) { + throw new HttpMediaTypeNotSupportedException(ex.getMessage()); + } + throw new HttpMediaTypeNotSupportedException(contentType, new ArrayList<>(consumableMediaTypes)); + } + else if (!producibleMediaTypes.isEmpty()) { + throw new HttpMediaTypeNotAcceptableException(new ArrayList<>(producibleMediaTypes)); + } + else { + if (!CollectionUtils.isEmpty(paramConditions)) { + Map params = request.getQueryParams().entrySet().stream() + .collect(Collectors.toMap(Entry::getKey, + entry -> entry.getValue().toArray(new String[entry.getValue().size()])) + ); + throw new UnsatisfiedServletRequestParameterException(paramConditions, params); + } + else { + return null; + } + } + } + + private Set getConsumableMediaTypes(ServerWebExchange exchange, + Set partialMatches) { + + Set result = new HashSet<>(); + for (RequestMappingInfo partialMatch : partialMatches) { + if (partialMatch.getConsumesCondition().getMatchingCondition(exchange) == null) { + result.addAll(partialMatch.getConsumesCondition().getConsumableMediaTypes()); + } + } + return result; + } + + private Set getProducibleMediaTypes(ServerWebExchange exchange, + Set partialMatches) { + + Set result = new HashSet<>(); + for (RequestMappingInfo partialMatch : partialMatches) { + if (partialMatch.getProducesCondition().getMatchingCondition(exchange) == null) { + result.addAll(partialMatch.getProducesCondition().getProducibleMediaTypes()); + } + } + return result; + } + + private List getRequestParams(ServerWebExchange exchange, + Set partialMatches) { + + List result = new ArrayList<>(); + for (RequestMappingInfo partialMatch : partialMatches) { + ParamsRequestCondition condition = partialMatch.getParamsCondition(); + Set> expressions = condition.getExpressions(); + if (!CollectionUtils.isEmpty(expressions) && condition.getMatchingCondition(exchange) == null) { + int i = 0; + String[] array = new String[expressions.size()]; + for (NameValueExpression expression : expressions) { + array[i++] = expression.toString(); + } + result.add(array); + } + } + return result; + } + + + /** + * Default handler for HTTP OPTIONS. + */ + private static class HttpOptionsHandler { + + private final HttpHeaders headers = new HttpHeaders(); + + + public HttpOptionsHandler(Set declaredMethods) { + this.headers.setAllow(initAllowedHttpMethods(declaredMethods)); + } + + private static Set initAllowedHttpMethods(Set declaredMethods) { + Set result = new LinkedHashSet(declaredMethods.size()); + if (declaredMethods.isEmpty()) { + for (HttpMethod method : HttpMethod.values()) { + if (!HttpMethod.TRACE.equals(method)) { + result.add(method); + } + } + } + else { + boolean hasHead = declaredMethods.contains("HEAD"); + for (String method : declaredMethods) { + result.add(HttpMethod.valueOf(method)); + if (!hasHead && "GET".equals(method)) { + result.add(HttpMethod.HEAD); + } + } + } + return result; + } + + public HttpHeaders handle() { + return this.headers; + } + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java index e6861bd8c2..96ec3b6c6e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -16,154 +16,260 @@ package org.springframework.web.reactive.result.method.annotation; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Method; import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.TreeMap; -import java.util.TreeSet; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -import org.springframework.beans.factory.InitializingBean; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; -import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.context.EmbeddedValueResolverAware; +import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.stereotype.Controller; +import org.springframework.util.Assert; +import org.springframework.util.StringValueResolver; +import org.springframework.web.accept.ContentNegotiationManager; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.method.HandlerMethod; -import org.springframework.web.method.HandlerMethodSelector; -import org.springframework.web.reactive.HandlerMapping; -import org.springframework.web.server.ServerWebExchange; - +import org.springframework.web.reactive.accept.ContentTypeResolver; +import org.springframework.web.reactive.accept.HeaderContentTypeResolver; +import org.springframework.web.reactive.result.condition.RequestCondition; +import org.springframework.web.reactive.result.method.RequestMappingInfo; +import org.springframework.web.reactive.result.method.RequestMappingInfoHandlerMapping; /** + * An extension of {@link RequestMappingInfoHandlerMapping} that creates + * {@link RequestMappingInfo} instances from class-level and method-level + * {@link RequestMapping @RequestMapping} annotations. + * * @author Rossen Stoyanchev */ -public class RequestMappingHandlerMapping implements HandlerMapping, - ApplicationContextAware, InitializingBean { +public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping + implements EmbeddedValueResolverAware { - private static final Log logger = LogFactory.getLog(RequestMappingHandlerMapping.class); + private boolean useSuffixPatternMatch = true; + + private boolean useRegisteredSuffixPatternMatch = true; + + private boolean useTrailingSlashMatch = true; + + private ContentTypeResolver contentTypeResolver = new HeaderContentTypeResolver(); + + private StringValueResolver embeddedValueResolver; + + private RequestMappingInfo.BuilderConfiguration config = new RequestMappingInfo.BuilderConfiguration(); - private final Map methodMap = new TreeMap<>(); - - private ApplicationContext applicationContext; - - - @Override - public void setApplicationContext(ApplicationContext applicationContext) { - this.applicationContext = applicationContext; + /** + * Whether to use suffix pattern matching. If enabled a method mapped to + * "/path" also matches to "/path.*". + *

    The default value is {@code true}. + *

    Note: when using suffix pattern matching it's usually + * preferable to be explicit about what is and isn't an extension so rather + * than setting this property consider using + * {@link #setUseRegisteredSuffixPatternMatch} instead. + */ + public void setUseSuffixPatternMatch(boolean useSuffixPatternMatch) { + this.useSuffixPatternMatch = useSuffixPatternMatch; } - - @Override - public void afterPropertiesSet() throws Exception { - this.applicationContext.getBeansOfType(Object.class).values().forEach(this::detectHandlerMethods); + /** + * Whether suffix pattern matching should work only against path extensions + * explicitly registered with the configured {@link ContentTypeResolver}. This + * is generally recommended to reduce ambiguity and to avoid issues such as + * when a "." appears in the path for other reasons. + *

    By default this is set to "true". + */ + public void setUseRegisteredSuffixPatternMatch(boolean useRegisteredSuffixPatternMatch) { + this.useRegisteredSuffixPatternMatch = useRegisteredSuffixPatternMatch; + this.useSuffixPatternMatch = (useRegisteredSuffixPatternMatch || this.useSuffixPatternMatch); } - protected void detectHandlerMethods(final Object bean) { - final Class beanType = bean.getClass(); - if (AnnotationUtils.findAnnotation(beanType, Controller.class) != null) { - HandlerMethodSelector.selectMethods(beanType, method -> { - RequestMapping annotation = AnnotationUtils.findAnnotation(method, RequestMapping.class); - if (annotation != null && annotation.value().length > 0) { - String path = annotation.value()[0]; - RequestMethod[] methods = annotation.method(); - HandlerMethod handlerMethod = new HandlerMethod(bean, method); - if (logger.isInfoEnabled()) { - logger.info("Mapped \"" + path + "\" onto " + handlerMethod); - } - RequestMappingInfo info = new RequestMappingInfo(path, methods); - if (this.methodMap.containsKey(info)) { - throw new IllegalStateException("Duplicate mapping found for " + info); - } - methodMap.put(info, handlerMethod); - } - return false; - }); - } + /** + * Whether to match to URLs irrespective of the presence of a trailing slash. + * If enabled a method mapped to "/users" also matches to "/users/". + *

    The default value is {@code true}. + */ + public void setUseTrailingSlashMatch(boolean useTrailingSlashMatch) { + this.useTrailingSlashMatch = useTrailingSlashMatch; + } + + /** + * Set the {@link ContentNegotiationManager} to use to determine requested media types. + * If not set, the default constructor is used. + */ + public void setContentTypeResolver(ContentTypeResolver contentTypeResolver) { + Assert.notNull(contentTypeResolver, "'ContentTypeResolver' must not be null"); + this.contentTypeResolver = contentTypeResolver; } @Override - public Mono getHandler(ServerWebExchange exchange) { - return Flux.create(subscriber -> { - for (Map.Entry entry : this.methodMap.entrySet()) { - RequestMappingInfo info = entry.getKey(); - if (info.matchesRequest(exchange.getRequest())) { - HandlerMethod handlerMethod = entry.getValue(); - if (logger.isDebugEnabled()) { - logger.debug("Mapped " + exchange.getRequest().getMethod() + " " + - exchange.getRequest().getURI().getPath() + " to [" + handlerMethod + "]"); - } - subscriber.onNext(handlerMethod); - break; - } - } - subscriber.onComplete(); - }).next(); + public void setEmbeddedValueResolver(StringValueResolver resolver) { + this.embeddedValueResolver = resolver; + } + + @Override + public void afterPropertiesSet() { + this.config = new RequestMappingInfo.BuilderConfiguration(); + this.config.setPathHelper(getPathHelper()); + this.config.setPathMatcher(getPathMatcher()); + this.config.setSuffixPatternMatch(this.useSuffixPatternMatch); + this.config.setTrailingSlashMatch(this.useTrailingSlashMatch); + this.config.setRegisteredSuffixPatternMatch(this.useRegisteredSuffixPatternMatch); + this.config.setContentTypeResolver(getContentTypeResolver()); + + super.afterPropertiesSet(); } - private static class RequestMappingInfo implements Comparable { + /** + * Whether to use suffix pattern matching. + */ + public boolean useSuffixPatternMatch() { + return this.useSuffixPatternMatch; + } - private String path; + /** + * Whether to use registered suffixes for pattern matching. + */ + public boolean useRegisteredSuffixPatternMatch() { + return this.useRegisteredSuffixPatternMatch; + } - private Set methods; + /** + * Whether to match to URLs irrespective of the presence of a trailing slash. + */ + public boolean useTrailingSlashMatch() { + return this.useTrailingSlashMatch; + } + + /** + * Return the configured {@link ContentTypeResolver}. + */ + public ContentTypeResolver getContentTypeResolver() { + return this.contentTypeResolver; + } + + /** + * Return the file extensions to use for suffix pattern matching. + */ + public List getFileExtensions() { + return this.config.getFileExtensions(); + } - public RequestMappingInfo(String path, RequestMethod... methods) { - this(path, asList(methods)); - } + /** + * {@inheritDoc} + * Expects a handler to have a type-level @{@link Controller} annotation. + */ + @Override + protected boolean isHandler(Class beanType) { + return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) || + AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class)); + } - private static List asList(RequestMethod... requestMethods) { - return (requestMethods != null ? - Arrays.asList(requestMethods) : Collections.emptyList()); - } - - public RequestMappingInfo(String path, Collection methods) { - this.path = path; - this.methods = new TreeSet<>(methods); - } - - - public String getPath() { - return this.path; - } - - public Set getMethods() { - return this.methods; - } - - public boolean matchesRequest(ServerHttpRequest request) { - String httpMethod = request.getMethod().name(); - return request.getURI().getPath().equals(getPath()) && - (getMethods().isEmpty() || getMethods().contains(RequestMethod.valueOf(httpMethod))); - } - - @Override - public int compareTo(Object o) { - RequestMappingInfo other = (RequestMappingInfo) o; - if (!this.path.equals(other.getPath())) { - return -1; + /** + * Uses method and type-level @{@link RequestMapping} annotations to create + * the RequestMappingInfo. + * @return the created RequestMappingInfo, or {@code null} if the method + * does not have a {@code @RequestMapping} annotation. + * @see #getCustomMethodCondition(Method) + * @see #getCustomTypeCondition(Class) + */ + @Override + protected RequestMappingInfo getMappingForMethod(Method method, Class handlerType) { + RequestMappingInfo info = createRequestMappingInfo(method); + if (info != null) { + RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType); + if (typeInfo != null) { + info = typeInfo.combine(info); } - if (this.methods.isEmpty() && !other.methods.isEmpty()) { - return 1; + } + return info; + } + + /** + * Delegates to {@link #createRequestMappingInfo(RequestMapping, RequestCondition)}, + * supplying the appropriate custom {@link RequestCondition} depending on whether + * the supplied {@code annotatedElement} is a class or method. + * @see #getCustomTypeCondition(Class) + * @see #getCustomMethodCondition(Method) + */ + private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) { + RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class); + RequestCondition condition = (element instanceof Class ? + getCustomTypeCondition((Class) element) : getCustomMethodCondition((Method) element)); + return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition) : null); + } + + /** + * Provide a custom type-level request condition. + * The custom {@link RequestCondition} can be of any type so long as the + * same condition type is returned from all calls to this method in order + * to ensure custom request conditions can be combined and compared. + *

    Consider extending + * {@link org.springframework.web.reactive.result.condition.AbstractRequestCondition + * AbstractRequestCondition} for custom condition types and using + * {@link org.springframework.web.reactive.result.condition.CompositeRequestCondition + * CompositeRequestCondition} to provide multiple custom conditions. + * @param handlerType the handler type for which to create the condition + * @return the condition, or {@code null} + */ + @SuppressWarnings("UnusedParameters") + protected RequestCondition getCustomTypeCondition(Class handlerType) { + return null; + } + + /** + * Provide a custom method-level request condition. + * The custom {@link RequestCondition} can be of any type so long as the + * same condition type is returned from all calls to this method in order + * to ensure custom request conditions can be combined and compared. + *

    Consider extending + * {@link org.springframework.web.reactive.result.condition.AbstractRequestCondition + * AbstractRequestCondition} for custom condition types and using + * {@link org.springframework.web.reactive.result.condition.CompositeRequestCondition + * CompositeRequestCondition} to provide multiple custom conditions. + * @param method the handler method for which to create the condition + * @return the condition, or {@code null} + */ + @SuppressWarnings("UnusedParameters") + protected RequestCondition getCustomMethodCondition(Method method) { + return null; + } + + /** + * Create a {@link RequestMappingInfo} from the supplied + * {@link RequestMapping @RequestMapping} annotation, which is either + * a directly declared annotation, a meta-annotation, or the synthesized + * result of merging annotation attributes within an annotation hierarchy. + */ + protected RequestMappingInfo createRequestMappingInfo( + RequestMapping requestMapping, RequestCondition customCondition) { + + return RequestMappingInfo + .paths(resolveEmbeddedValuesInPatterns(requestMapping.path())) + .methods(requestMapping.method()) + .params(requestMapping.params()) + .headers(requestMapping.headers()) + .consumes(requestMapping.consumes()) + .produces(requestMapping.produces()) + .mappingName(requestMapping.name()) + .customCondition(customCondition) + .options(this.config) + .build(); + } + + /** + * Resolve placeholder values in the given array of patterns. + * @return a new array with updated patterns + */ + protected String[] resolveEmbeddedValuesInPatterns(String[] patterns) { + if (this.embeddedValueResolver == null) { + return patterns; + } + else { + String[] resolvedPatterns = new String[patterns.length]; + for (int i = 0; i < patterns.length; i++) { + resolvedPatterns[i] = this.embeddedValueResolver.resolveStringValue(patterns[i]); } - if (!this.methods.isEmpty() && other.methods.isEmpty()) { - return -1; - } - if (this.methods.equals(other.methods)) { - return 0; - } - return -1; + return resolvedPatterns; } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/util/HttpRequestPathHelper.java b/spring-web-reactive/src/main/java/org/springframework/web/util/HttpRequestPathHelper.java index 45c893d3e8..259033df7b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/util/HttpRequestPathHelper.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/util/HttpRequestPathHelper.java @@ -16,7 +16,11 @@ package org.springframework.web.util; import java.io.UnsupportedEncodingException; +import java.util.LinkedHashMap; +import java.util.Map; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import org.springframework.web.server.ServerWebExchange; /** @@ -49,11 +53,12 @@ public class HttpRequestPathHelper { public String getLookupPathForRequest(ServerWebExchange exchange) { - String path = exchange.getRequest().getURI().getPath(); - return (this.shouldUrlDecode() ? decode(path) : path); + String path = exchange.getRequest().getURI().getRawPath(); + return (this.shouldUrlDecode() ? decode(exchange, path) : path); } - private String decode(String path) { + private String decode(ServerWebExchange exchange, String path) { + // TODO: look up request encoding? try { return UriUtils.decode(path, "UTF-8"); } @@ -63,4 +68,48 @@ public class HttpRequestPathHelper { } } + /** + * Decode the given URI path variables unless {@link #setUrlDecode(boolean)} + * is set to {@code true} in which case it is assumed the URL path from + * which the variables were extracted is already decoded through a call to + * {@link #getLookupPathForRequest(ServerWebExchange)}. + * @param exchange current exchange + * @param vars URI variables extracted from the URL path + * @return the same Map or a new Map instance + */ + public Map decodePathVariables(ServerWebExchange exchange, Map vars) { + if (this.urlDecode) { + return vars; + } + Map decodedVars = new LinkedHashMap<>(vars.size()); + for (Map.Entry entry : vars.entrySet()) { + decodedVars.put(entry.getKey(), decode(exchange, entry.getValue())); + } + return decodedVars; + } + + /** + * Decode the given matrix variables unless {@link #setUrlDecode(boolean)} + * is set to {@code true} in which case it is assumed the URL path from + * which the variables were extracted is already decoded through a call to + * {@link #getLookupPathForRequest(ServerWebExchange)}. + * @param exchange current exchange + * @param vars URI variables extracted from the URL path + * @return the same Map or a new Map instance + */ + public MultiValueMap decodeMatrixVariables(ServerWebExchange exchange, + MultiValueMap vars) { + + if (this.urlDecode) { + return vars; + } + MultiValueMap decodedVars = new LinkedMultiValueMap<>(vars.size()); + for (String key : vars.keySet()) { + for (String value : vars.get(key)) { + decodedVars.add(key, decode(exchange, value)); + } + } + return decodedVars; + } + } \ No newline at end of file diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java new file mode 100644 index 0000000000..50ca621bb4 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java @@ -0,0 +1,535 @@ +/* + * 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.web.reactive.result.method; + +import java.lang.reflect.Method; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Consumer; + +import org.junit.Before; +import org.junit.Test; +import reactor.core.publisher.Mono; +import reactor.core.test.TestSubscriber; + +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.stereotype.Controller; +import org.springframework.ui.ExtendedModelMap; +import org.springframework.ui.ModelMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.HttpMediaTypeNotSupportedException; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.UnsatisfiedServletRequestParameterException; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.reactive.HandlerMapping; +import org.springframework.web.reactive.HandlerResult; +import org.springframework.web.reactive.result.method.RequestMappingInfo.BuilderConfiguration; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.WebSessionManager; +import org.springframework.web.util.HttpRequestPathHelper; + +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + + +/** + * Unit tests for {@link RequestMappingInfoHandlerMapping}. + * + * @author Rossen Stoyanchev + */ +public class RequestMappingInfoHandlerMappingTests { + + private TestRequestMappingInfoHandlerMapping handlerMapping; + + private HandlerMethod fooMethod; + + private HandlerMethod fooParamMethod; + + private HandlerMethod barMethod; + + private HandlerMethod emptyMethod; + + + @Before + public void setUp() throws Exception { + TestController testController = new TestController(); + + this.fooMethod = new HandlerMethod(testController, "foo"); + this.fooParamMethod = new HandlerMethod(testController, "fooParam"); + this.barMethod = new HandlerMethod(testController, "bar"); + this.emptyMethod = new HandlerMethod(testController, "empty"); + + this.handlerMapping = new TestRequestMappingInfoHandlerMapping(); + this.handlerMapping.registerHandler(testController); + } + + + @Test + public void getMappingPathPatterns() throws Exception { + String[] patterns = {"/foo/*", "/foo", "/bar/*", "/bar"}; + RequestMappingInfo info = RequestMappingInfo.paths(patterns).build(); + Set actual = this.handlerMapping.getMappingPathPatterns(info); + + assertEquals(new HashSet<>(Arrays.asList(patterns)), actual); + } + + @Test + public void getHandlerDirectMatch() throws Exception { + ServerWebExchange exchange = createExchange(HttpMethod.GET, "/foo"); + HandlerMethod handlerMethod = getHandler(exchange); + assertEquals(this.fooMethod.getMethod(), handlerMethod.getMethod()); + } + + @Test + public void getHandlerGlobMatch() throws Exception { + ServerWebExchange exchange = createExchange(HttpMethod.GET, "/bar"); + HandlerMethod handlerMethod = getHandler(exchange); + assertEquals(this.barMethod.getMethod(), handlerMethod.getMethod()); + } + + @Test + public void getHandlerEmptyPathMatch() throws Exception { + ServerWebExchange exchange = createExchange(HttpMethod.GET, ""); + HandlerMethod handlerMethod = getHandler(exchange); + + assertEquals(this.emptyMethod.getMethod(), handlerMethod.getMethod()); + + exchange = createExchange(HttpMethod.GET, "/"); + handlerMethod = getHandler(exchange); + + assertEquals(this.emptyMethod.getMethod(), handlerMethod.getMethod()); + } + + @Test + public void getHandlerBestMatch() throws Exception { + ServerWebExchange exchange = createExchange(HttpMethod.GET, "/foo"); + exchange.getRequest().getQueryParams().add("p", "anything"); + HandlerMethod handlerMethod = getHandler(exchange); + + assertEquals(this.fooParamMethod.getMethod(), handlerMethod.getMethod()); + } + + @Test + public void getHandlerRequestMethodNotAllowed() throws Exception { + ServerWebExchange exchange = createExchange(HttpMethod.POST, "/bar"); + Mono mono = this.handlerMapping.getHandler(exchange); + assertError(mono, HttpRequestMethodNotSupportedException.class, + ex -> assertArrayEquals(new String[]{"GET", "HEAD"}, ex.getSupportedMethods())); + } + + // SPR-9603 + + @Test + public void getHandlerRequestMethodMatchFalsePositive() throws Exception { + ServerWebExchange exchange = createExchange(HttpMethod.GET, "/users"); + exchange.getRequest().getHeaders().setAccept(Collections.singletonList(MediaType.APPLICATION_XML)); + this.handlerMapping.registerHandler(new UserController()); + Mono mono = this.handlerMapping.getHandler(exchange); + + TestSubscriber subscriber = new TestSubscriber<>(); + mono.subscribeWith(subscriber); + subscriber.assertError(HttpMediaTypeNotAcceptableException.class); + } + + // SPR-8462 + + @Test + public void getHandlerMediaTypeNotSupported() throws Exception { + testHttpMediaTypeNotSupportedException("/person/1"); + testHttpMediaTypeNotSupportedException("/person/1/"); + testHttpMediaTypeNotSupportedException("/person/1.json"); + } + + @Test + public void getHandlerHttpOptions() throws Exception { + testHttpOptions("/foo", "GET,HEAD"); + testHttpOptions("/person/1", "PUT"); + testHttpOptions("/persons", "GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS"); + testHttpOptions("/something", "PUT,POST"); + } + + @Test + public void getHandlerTestInvalidContentType() throws Exception { + ServerWebExchange exchange = createExchange(HttpMethod.PUT, "/person/1"); + exchange.getRequest().getHeaders().add("Content-Type", "bogus"); + Mono mono = this.handlerMapping.getHandler(exchange); + assertError(mono, HttpMediaTypeNotSupportedException.class, + ex -> assertEquals("Invalid mime type \"bogus\": does not contain '/'", ex.getMessage())); + } + + // SPR-8462 + + @Test + public void getHandlerMediaTypeNotAccepted() throws Exception { + testHttpMediaTypeNotAcceptableException("/persons"); + testHttpMediaTypeNotAcceptableException("/persons/"); + testHttpMediaTypeNotAcceptableException("/persons.json"); + } + + // SPR-12854 + + @Test + public void getHandlerUnsatisfiedServletRequestParameterException() throws Exception { + ServerWebExchange exchange = createExchange(HttpMethod.GET, "/params"); + Mono mono = this.handlerMapping.getHandler(exchange); + assertError(mono, UnsatisfiedServletRequestParameterException.class, ex -> { + List groups = ex.getParamConditionGroups(); + assertEquals(2, groups.size()); + assertThat(Arrays.asList("foo=bar", "bar=baz"), + containsInAnyOrder(groups.get(0)[0], groups.get(1)[0])); + }); + } + + @Test + public void getHandlerProducibleMediaTypesAttribute() throws Exception { + ServerWebExchange exchange = createExchange(HttpMethod.GET, "/content"); + exchange.getRequest().getHeaders().setAccept(Collections.singletonList(MediaType.APPLICATION_XML)); + getHandler(exchange); + + String name = HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE; + assertEquals(Collections.singleton(MediaType.APPLICATION_XML), exchange.getAttributes().get(name)); + + exchange = createExchange(HttpMethod.GET, "/content"); + exchange.getRequest().getHeaders().setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); + getHandler(exchange); + + assertNull("Negated expression shouldn't be listed as producible type", + exchange.getAttributes().get(name)); + } + + @SuppressWarnings("unchecked") + @Test + public void handleMatchUriTemplateVariables() throws Exception { + RequestMappingInfo key = RequestMappingInfo.paths("/{path1}/{path2}").build(); + ServerWebExchange exchange = createExchange(HttpMethod.GET, "/1/2"); + String lookupPath = exchange.getRequest().getURI().getPath(); + this.handlerMapping.handleMatch(key, lookupPath, exchange); + + String name = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE; + Map uriVariables = (Map) exchange.getAttributes().get(name); + + assertNotNull(uriVariables); + assertEquals("1", uriVariables.get("path1")); + assertEquals("2", uriVariables.get("path2")); + } + + // SPR-9098 + + @Test + public void handleMatchUriTemplateVariablesDecode() throws Exception { + RequestMappingInfo key = RequestMappingInfo.paths("/{group}/{identifier}").build(); + ServerWebExchange exchange = createExchange(HttpMethod.GET, "/group/a%2Fb"); + + HttpRequestPathHelper pathHelper = new HttpRequestPathHelper(); + pathHelper.setUrlDecode(false); + String lookupPath = pathHelper.getLookupPathForRequest(exchange); + + this.handlerMapping.setPathHelper(pathHelper); + this.handlerMapping.handleMatch(key, lookupPath, exchange); + + String name = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE; + @SuppressWarnings("unchecked") + Map uriVariables = (Map) exchange.getAttributes().get(name); + + assertNotNull(uriVariables); + assertEquals("group", uriVariables.get("group")); + assertEquals("a/b", uriVariables.get("identifier")); + } + + @Test + public void handleMatchBestMatchingPatternAttribute() throws Exception { + RequestMappingInfo key = RequestMappingInfo.paths("/{path1}/2", "/**").build(); + ServerWebExchange exchange = createExchange(HttpMethod.GET, "/1/2"); + this.handlerMapping.handleMatch(key, "/1/2", exchange); + + assertEquals("/{path1}/2", exchange.getAttributes().get(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE)); + } + + @Test + public void handleMatchBestMatchingPatternAttributeNoPatternsDefined() throws Exception { + RequestMappingInfo key = RequestMappingInfo.paths().build(); + ServerWebExchange exchange = createExchange(HttpMethod.GET, "/1/2"); + + this.handlerMapping.handleMatch(key, "/1/2", exchange); + + assertEquals("/1/2", exchange.getAttributes().get(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE)); + } + + @Test + public void handleMatchMatrixVariables() throws Exception { + ServerWebExchange exchange; + MultiValueMap matrixVariables; + Map uriVariables; + + exchange = createExchange(HttpMethod.GET, "/"); + handleMatch(exchange, "/{cars}", "/cars;colors=red,blue,green;year=2012"); + + matrixVariables = getMatrixVariables(exchange, "cars"); + uriVariables = getUriTemplateVariables(exchange); + + assertNotNull(matrixVariables); + assertEquals(Arrays.asList("red", "blue", "green"), matrixVariables.get("colors")); + assertEquals("2012", matrixVariables.getFirst("year")); + assertEquals("cars", uriVariables.get("cars")); + + exchange = createExchange(HttpMethod.GET, "/"); + handleMatch(exchange, "/{cars:[^;]+}{params}", "/cars;colors=red,blue,green;year=2012"); + + matrixVariables = getMatrixVariables(exchange, "params"); + uriVariables = getUriTemplateVariables(exchange); + + assertNotNull(matrixVariables); + assertEquals(Arrays.asList("red", "blue", "green"), matrixVariables.get("colors")); + assertEquals("2012", matrixVariables.getFirst("year")); + assertEquals("cars", uriVariables.get("cars")); + assertEquals(";colors=red,blue,green;year=2012", uriVariables.get("params")); + + exchange = createExchange(HttpMethod.GET, "/"); + handleMatch(exchange, "/{cars:[^;]+}{params}", "/cars"); + + matrixVariables = getMatrixVariables(exchange, "params"); + uriVariables = getUriTemplateVariables(exchange); + + assertNull(matrixVariables); + assertEquals("cars", uriVariables.get("cars")); + assertEquals("", uriVariables.get("params")); + } + + @Test + public void handleMatchMatrixVariablesDecoding() throws Exception { + HttpRequestPathHelper urlPathHelper = new HttpRequestPathHelper(); + urlPathHelper.setUrlDecode(false); + + this.handlerMapping.setPathHelper(urlPathHelper ); + + ServerWebExchange exchange = createExchange(HttpMethod.GET, "/"); + handleMatch(exchange, "/path{filter}", "/path;mvar=a%2fb"); + + MultiValueMap matrixVariables = getMatrixVariables(exchange, "filter"); + Map uriVariables = getUriTemplateVariables(exchange); + + assertNotNull(matrixVariables); + assertEquals(Collections.singletonList("a/b"), matrixVariables.get("mvar")); + assertEquals(";mvar=a/b", uriVariables.get("filter")); + } + + + private ServerWebExchange createExchange(HttpMethod method, String url) throws URISyntaxException { + ServerHttpRequest request = new MockServerHttpRequest(method, new URI(url)); + WebSessionManager sessionManager = mock(WebSessionManager.class); + return new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); + + } + + @SuppressWarnings("unchecked") + private void assertError(Mono mono, final Class exceptionClass, final Consumer consumer) { + TestSubscriber subscriber = new TestSubscriber<>(); + mono.subscribeWith(subscriber); + subscriber.assertErrorWith(ex -> { + assertEquals(exceptionClass, ex.getClass()); + consumer.accept((T) ex); + }); + } + + @SuppressWarnings("ConstantConditions") + private HandlerMethod getHandler(ServerWebExchange exchange) throws Exception { + Mono handler = this.handlerMapping.getHandler(exchange); + return (HandlerMethod) handler.get(); + } + + private void testHttpMediaTypeNotSupportedException(String url) throws Exception { + ServerWebExchange exchange = createExchange(HttpMethod.PUT, url); + exchange.getRequest().getHeaders().setContentType(MediaType.APPLICATION_JSON); + Mono mono = this.handlerMapping.getHandler(exchange); + + assertError(mono, HttpMediaTypeNotSupportedException.class, ex -> + assertEquals("Invalid supported consumable media types", + Collections.singletonList(new MediaType("application", "xml")), + ex.getSupportedMediaTypes())); + } + + private void testHttpOptions(String requestURI, String allowHeader) throws Exception { + ServerWebExchange exchange = createExchange(HttpMethod.OPTIONS, requestURI); + HandlerMethod handlerMethod = getHandler(exchange); + + ModelMap model = new ExtendedModelMap(); + Mono mono = new InvocableHandlerMethod(handlerMethod).invokeForRequest(exchange, model); + + HandlerResult result = mono.get(); + assertNotNull(result); + + Optional value = result.getReturnValue(); + assertTrue(value.isPresent()); + assertEquals(HttpHeaders.class, value.get().getClass()); + assertEquals(allowHeader, ((HttpHeaders) value.get()).getFirst("Allow")); + } + + private void testHttpMediaTypeNotAcceptableException(String url) throws Exception { + ServerWebExchange exchange = createExchange(HttpMethod.GET, url); + exchange.getRequest().getHeaders().setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); + Mono mono = this.handlerMapping.getHandler(exchange); + + assertError(mono, HttpMediaTypeNotAcceptableException.class, ex -> + assertEquals("Invalid supported producible media types", + Collections.singletonList(new MediaType("application", "xml")), + ex.getSupportedMediaTypes())); + } + + private void handleMatch(ServerWebExchange exchange, String pattern, String lookupPath) { + RequestMappingInfo info = RequestMappingInfo.paths(pattern).build(); + this.handlerMapping.handleMatch(info, lookupPath, exchange); + } + + @SuppressWarnings("unchecked") + private MultiValueMap getMatrixVariables(ServerWebExchange exchange, String uriVarName) { + String attrName = HandlerMapping.MATRIX_VARIABLES_ATTRIBUTE; + return ((Map>) exchange.getAttributes().get(attrName)).get(uriVarName); + } + + @SuppressWarnings("unchecked") + private Map getUriTemplateVariables(ServerWebExchange exchange) { + String attrName = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE; + return (Map) exchange.getAttributes().get(attrName); + } + + + @SuppressWarnings("unused") + @Controller + private static class TestController { + + @RequestMapping(value = "/foo", method = RequestMethod.GET) + public void foo() { + } + + @RequestMapping(value = "/foo", method = RequestMethod.GET, params="p") + public void fooParam() { + } + + @RequestMapping(value = "/ba*", method = { RequestMethod.GET, RequestMethod.HEAD }) + public void bar() { + } + + @RequestMapping(value = "") + public void empty() { + } + + @RequestMapping(value = "/person/{id}", method = RequestMethod.PUT, consumes="application/xml") + public void consumes(@RequestBody String text) { + } + + @RequestMapping(value = "/persons", produces="application/xml") + public String produces() { + return ""; + } + + @RequestMapping(value = "/params", params="foo=bar") + public String param() { + return ""; + } + + @RequestMapping(value = "/params", params="bar=baz") + public String param2() { + return ""; + } + + @RequestMapping(value = "/content", produces="application/xml") + public String xmlContent() { + return ""; + } + + @RequestMapping(value = "/content", produces="!application/xml") + public String nonXmlContent() { + return ""; + } + + @RequestMapping(value = "/something", method = RequestMethod.OPTIONS) + public HttpHeaders fooOptions() { + HttpHeaders headers = new HttpHeaders(); + headers.add("Allow", "PUT,POST"); + return headers; + } + } + + @SuppressWarnings("unused") + @Controller + private static class UserController { + + @RequestMapping(value = "/users", method = RequestMethod.GET, produces = "application/json") + public void getUser() { + } + + @RequestMapping(value = "/users", method = RequestMethod.PUT) + public void saveUser() { + } + } + + private static class TestRequestMappingInfoHandlerMapping extends RequestMappingInfoHandlerMapping { + + public void registerHandler(Object handler) { + super.detectHandlerMethods(handler); + } + + @Override + protected boolean isHandler(Class beanType) { + return AnnotationUtils.findAnnotation(beanType, RequestMapping.class) != null; + } + + @Override + protected RequestMappingInfo getMappingForMethod(Method method, Class handlerType) { + RequestMapping annotation = AnnotationUtils.findAnnotation(method, RequestMapping.class); + if (annotation != null) { + BuilderConfiguration options = new BuilderConfiguration(); + options.setPathHelper(getPathHelper()); + options.setPathMatcher(getPathMatcher()); + options.setSuffixPatternMatch(true); + options.setTrailingSlashMatch(true); + return RequestMappingInfo.paths(annotation.value()).methods(annotation.method()) + .params(annotation.params()).headers(annotation.headers()) + .consumes(annotation.consumes()).produces(annotation.produces()) + .options(options).build(); + } + else { + return null; + } + } + } + +} \ No newline at end of file diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMappingTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMappingTests.java index d9bba5aefb..f8feef526d 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMappingTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMappingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -16,111 +16,232 @@ package org.springframework.web.reactive.result.method.annotation; -import java.net.URI; -import java.util.List; -import java.util.stream.StreamSupport; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; import org.junit.Before; import org.junit.Test; -import org.reactivestreams.Publisher; -import reactor.core.publisher.Flux; -import org.springframework.context.support.StaticApplicationContext; -import org.springframework.http.HttpMethod; -import org.springframework.http.server.reactive.MockServerHttpRequest; -import org.springframework.http.server.reactive.MockServerHttpResponse; -import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.core.annotation.AliasFor; +import org.springframework.http.MediaType; import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.method.HandlerMethod; -import org.springframework.web.server.adapter.DefaultServerWebExchange; -import org.springframework.web.server.ServerWebExchange; -import org.springframework.web.server.session.WebSessionManager; +import org.springframework.web.context.support.StaticWebApplicationContext; +import org.springframework.web.reactive.accept.FileExtensionContentTypeResolver; +import org.springframework.web.reactive.result.method.RequestMappingInfo; -import static java.util.stream.Collectors.toList; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; /** - * @author Sebastien Deleuze + * Unit tests for {@link RequestMappingHandlerMapping}. + * + * @author Rossen Stoyanchev */ public class RequestMappingHandlerMappingTests { - private RequestMappingHandlerMapping mapping; + private final StaticWebApplicationContext wac = new StaticWebApplicationContext(); + + private final RequestMappingHandlerMapping handlerMapping = new RequestMappingHandlerMapping(); @Before - public void setup() { - StaticApplicationContext wac = new StaticApplicationContext(); - wac.registerSingleton("handlerMapping", RequestMappingHandlerMapping.class); - wac.registerSingleton("controller", TestController.class); - wac.refresh(); - this.mapping = (RequestMappingHandlerMapping)wac.getBean("handlerMapping"); + public void setUp() throws Exception { + this.handlerMapping.setApplicationContext(wac); } @Test - public void path() throws Exception { - ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("boo")); - MockServerHttpResponse response = new MockServerHttpResponse(); - WebSessionManager sessionManager = mock(WebSessionManager.class); - ServerWebExchange exchange = new DefaultServerWebExchange(request, response, sessionManager); - Publisher handlerPublisher = this.mapping.getHandler(exchange); - HandlerMethod handlerMethod = toHandlerMethod(handlerPublisher); - assertEquals(TestController.class.getMethod("boo"), handlerMethod.getMethod()); + public void useRegisteredSuffixPatternMatch() { + assertTrue(this.handlerMapping.useSuffixPatternMatch()); + assertTrue(this.handlerMapping.useRegisteredSuffixPatternMatch()); + + FileExtensionContentTypeResolver contentTypeResolver = mock(FileExtensionContentTypeResolver.class); + when(contentTypeResolver.getAllFileExtensions()).thenReturn(Collections.singletonList("json")); + + this.handlerMapping.setContentTypeResolver(contentTypeResolver); + this.handlerMapping.afterPropertiesSet(); + + assertTrue(this.handlerMapping.useSuffixPatternMatch()); + assertTrue(this.handlerMapping.useRegisteredSuffixPatternMatch()); + assertEquals(Collections.singletonList("json"), this.handlerMapping.getFileExtensions()); } @Test - public void method() throws Exception { - ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.POST, new URI("foo")); - MockServerHttpResponse response = new MockServerHttpResponse(); - WebSessionManager sessionManager = mock(WebSessionManager.class); - ServerWebExchange exchange = new DefaultServerWebExchange(request, response, sessionManager); - Publisher handlerPublisher = this.mapping.getHandler(exchange); - HandlerMethod handlerMethod = toHandlerMethod(handlerPublisher); - assertEquals(TestController.class.getMethod("postFoo"), handlerMethod.getMethod()); + public void useRegisteredSuffixPatternMatchInitialization() { + FileExtensionContentTypeResolver contentTypeResolver = mock(FileExtensionContentTypeResolver.class); + when(contentTypeResolver.getAllFileExtensions()).thenReturn(Collections.singletonList("json")); - request = new MockServerHttpRequest(HttpMethod.GET, new URI("foo")); - exchange = new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); - handlerPublisher = this.mapping.getHandler(exchange); - handlerMethod = toHandlerMethod(handlerPublisher); - assertEquals(TestController.class.getMethod("getFoo"), handlerMethod.getMethod()); + final Set actualExtensions = new HashSet<>(); + RequestMappingHandlerMapping localHandlerMapping = new RequestMappingHandlerMapping() { + @Override + protected RequestMappingInfo getMappingForMethod(Method method, Class handlerType) { + actualExtensions.addAll(getFileExtensions()); + return super.getMappingForMethod(method, handlerType); + } + }; + this.wac.registerSingleton("testController", ComposedAnnotationController.class); + this.wac.refresh(); + + localHandlerMapping.setContentTypeResolver(contentTypeResolver); + localHandlerMapping.setUseRegisteredSuffixPatternMatch(true); + localHandlerMapping.setApplicationContext(this.wac); + localHandlerMapping.afterPropertiesSet(); + + assertEquals(Collections.singleton("json"), actualExtensions); } - private HandlerMethod toHandlerMethod(Publisher handlerPublisher) throws InterruptedException { - assertNotNull(handlerPublisher); - List handlerList = StreamSupport.stream(Flux.from(handlerPublisher).toIterable().spliterator(), false).collect(toList()); - assertEquals(1, handlerList.size()); - return (HandlerMethod) handlerList.get(0); + @Test + public void useSuffixPatternMatch() { + assertTrue(this.handlerMapping.useSuffixPatternMatch()); + assertTrue(this.handlerMapping.useRegisteredSuffixPatternMatch()); + + this.handlerMapping.setUseSuffixPatternMatch(false); + assertFalse(this.handlerMapping.useSuffixPatternMatch()); + + this.handlerMapping.setUseRegisteredSuffixPatternMatch(false); + assertFalse("'false' registeredSuffixPatternMatch shouldn't impact suffixPatternMatch", + this.handlerMapping.useSuffixPatternMatch()); + + this.handlerMapping.setUseRegisteredSuffixPatternMatch(true); + assertTrue("'true' registeredSuffixPatternMatch should enable suffixPatternMatch", + this.handlerMapping.useSuffixPatternMatch()); + } + + @Test + public void resolveEmbeddedValuesInPatterns() { + this.handlerMapping.setEmbeddedValueResolver( + value -> "/${pattern}/bar".equals(value) ? "/foo/bar" : value + ); + + String[] patterns = new String[] { "/foo", "/${pattern}/bar" }; + String[] result = this.handlerMapping.resolveEmbeddedValuesInPatterns(patterns); + + assertArrayEquals(new String[] { "/foo", "/foo/bar" }, result); + } + + @Test + public void resolveRequestMappingViaComposedAnnotation() throws Exception { + RequestMappingInfo info = assertComposedAnnotationMapping("postJson", "/postJson", RequestMethod.POST); + + assertEquals(MediaType.APPLICATION_JSON_VALUE, + info.getConsumesCondition().getConsumableMediaTypes().iterator().next().toString()); + assertEquals(MediaType.APPLICATION_JSON_VALUE, + info.getProducesCondition().getProducibleMediaTypes().iterator().next().toString()); + } + + @Test + public void getMapping() throws Exception { + assertComposedAnnotationMapping(RequestMethod.GET); + } + + @Test + public void postMapping() throws Exception { + assertComposedAnnotationMapping(RequestMethod.POST); + } + + @Test + public void putMapping() throws Exception { + assertComposedAnnotationMapping(RequestMethod.PUT); + } + + @Test + public void deleteMapping() throws Exception { + assertComposedAnnotationMapping(RequestMethod.DELETE); + } + + @Test + public void patchMapping() throws Exception { + assertComposedAnnotationMapping(RequestMethod.PATCH); + } + + private RequestMappingInfo assertComposedAnnotationMapping(RequestMethod requestMethod) throws Exception { + String methodName = requestMethod.name().toLowerCase(); + String path = "/" + methodName; + + return assertComposedAnnotationMapping(methodName, path, requestMethod); + } + + private RequestMappingInfo assertComposedAnnotationMapping(String methodName, String path, + RequestMethod requestMethod) throws Exception { + + Class clazz = ComposedAnnotationController.class; + Method method = clazz.getMethod(methodName); + RequestMappingInfo info = this.handlerMapping.getMappingForMethod(method, clazz); + + assertNotNull(info); + + Set paths = info.getPatternsCondition().getPatterns(); + assertEquals(1, paths.size()); + assertEquals(path, paths.iterator().next()); + + Set methods = info.getMethodsCondition().getMethods(); + assertEquals(1, methods.size()); + assertEquals(requestMethod, methods.iterator().next()); + + return info; } - @Controller - @SuppressWarnings("unused") - private static class TestController { + @Controller @SuppressWarnings("unused") + static class ComposedAnnotationController { - @RequestMapping(path = "foo", method = RequestMethod.POST) - public String postFoo() { - return "postFoo"; + @RequestMapping + public void handle() { } - @RequestMapping(path = "foo", method = RequestMethod.GET) - public String getFoo() { - return "getFoo"; + @PostJson("/postJson") + public void postJson() { } - @RequestMapping("bar") - public String bar() { - return "bar"; + @GetMapping("/get") + public void get() { } - @RequestMapping("boo") - public String boo() { - return "boo"; + @PostMapping("/post") + public void post() { } + @PutMapping("/put") + public void put() { + } + + @DeleteMapping("/delete") + public void delete() { + } + + @PatchMapping("/patch") + public void patch() { + } + + } + + @RequestMapping(method = RequestMethod.POST, + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + @interface PostJson { + + @AliasFor(annotation = RequestMapping.class, attribute = "path") @SuppressWarnings("unused") + String[] value() default {}; } } From 7d60c2767d9fe47dac2cd4130d20dea2be9b3792 Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Mon, 18 Apr 2016 21:24:59 +0100 Subject: [PATCH 302/498] Normalize returns with Scheduler factories in SchedulerGroup --- .../http/server/reactive/AsyncIntegrationTests.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java index 6e99d57c92..90b5dcfdf1 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java @@ -24,6 +24,7 @@ import org.junit.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.SchedulerGroup; +import reactor.core.scheduler.Scheduler; import reactor.core.scheduler.Timer; import org.springframework.core.io.buffer.DataBufferAllocator; @@ -41,7 +42,7 @@ import static org.junit.Assert.assertThat; */ public class AsyncIntegrationTests extends AbstractHttpHandlerIntegrationTests { - private final SchedulerGroup asyncGroup = SchedulerGroup.async(); + private final Scheduler asyncGroup = SchedulerGroup.async(); private final DataBufferAllocator allocator = new DefaultDataBufferAllocator(); From 9ffc0b5e65f7b178247fe040edb19f2c532ff3fb Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 18 Apr 2016 15:44:04 -0400 Subject: [PATCH 303/498] Update FileExtensionContentTypeResolver Rename to MappingContentTypeResolver and replace "fileExtension" with "key" in methods to be more general (e.g. file extension or query param). Also switch from List to Set for the return values. --- ...olver.java => MappingContentTypeResolver.java} | 15 ++++++++------- .../condition/PatternsRequestCondition.java | 7 ++++--- .../result/method/RequestMappingInfo.java | 10 +++++----- .../annotation/RequestMappingHandlerMapping.java | 4 ++-- .../condition/PatternsRequestConditionTests.java | 6 +++--- .../RequestMappingHandlerMappingTests.java | 10 +++++----- 6 files changed, 27 insertions(+), 25 deletions(-) rename spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/{FileExtensionContentTypeResolver.java => MappingContentTypeResolver.java} (69%) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/FileExtensionContentTypeResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/MappingContentTypeResolver.java similarity index 69% rename from spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/FileExtensionContentTypeResolver.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/MappingContentTypeResolver.java index 8343c8da7c..645beed077 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/FileExtensionContentTypeResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/MappingContentTypeResolver.java @@ -17,16 +17,17 @@ package org.springframework.web.reactive.accept; import java.util.List; +import java.util.Set; import org.springframework.http.MediaType; /** - * An extension of {@link ContentTypeResolver} for a resolver that uses file - * extensions and can expose file extension mappings. + * An extension of {@link ContentTypeResolver} that maintains a mapping between + * keys (e.g. file extension, query parameter) and media types. * * @author Rossen Stoyanchev */ -public interface FileExtensionContentTypeResolver extends ContentTypeResolver { +public interface MappingContentTypeResolver extends ContentTypeResolver { /** * Resolve the given media type to a list of path extensions. @@ -34,12 +35,12 @@ public interface FileExtensionContentTypeResolver extends ContentTypeResolver { * @param mediaType the media type to resolve * @return a list of extensions or an empty list, never {@code null} */ - List getFileExtensions(MediaType mediaType); + Set getKeysFor(MediaType mediaType); /** - * Return all registered file extensions. - * @return a list of extensions or an empty list, never {@code null} + * Return all registered keys (e.g. "json", "xml"). + * @return a list of keys or an empty list, never {@code null} */ - List getAllFileExtensions(); + Set getKeys(); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/PatternsRequestCondition.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/PatternsRequestCondition.java index cba2fe75ef..5c30eafe7c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/PatternsRequestCondition.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/PatternsRequestCondition.java @@ -21,6 +21,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; +import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; @@ -50,7 +51,7 @@ public final class PatternsRequestCondition extends AbstractRequestCondition fileExtensions = new ArrayList(); + private final Set fileExtensions = new HashSet<>(); /** @@ -74,7 +75,7 @@ public final class PatternsRequestCondition extends AbstractRequestCondition extensions) { + Set extensions) { this(asList(patterns), pathHelper, pathMatcher, useSuffixPatternMatch, useTrailingSlashMatch, extensions); } @@ -84,7 +85,7 @@ public final class PatternsRequestCondition extends AbstractRequestCondition patterns, HttpRequestPathHelper pathHelper, PathMatcher pathMatcher, boolean useSuffixPatternMatch, boolean useTrailingSlashMatch, - List fileExtensions) { + Set fileExtensions) { this.patterns = Collections.unmodifiableSet(prependLeadingSlash(patterns)); this.pathHelper = (pathHelper != null ? pathHelper : new HttpRequestPathHelper()); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfo.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfo.java index 15f91ea04d..bebceeb3e2 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfo.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfo.java @@ -16,13 +16,13 @@ package org.springframework.web.reactive.result.method; -import java.util.List; +import java.util.Set; import org.springframework.util.PathMatcher; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.reactive.accept.ContentTypeResolver; -import org.springframework.web.reactive.accept.FileExtensionContentTypeResolver; +import org.springframework.web.reactive.accept.MappingContentTypeResolver; import org.springframework.web.reactive.result.condition.ConsumesRequestCondition; import org.springframework.web.reactive.result.condition.HeadersRequestCondition; import org.springframework.web.reactive.result.condition.ParamsRequestCondition; @@ -576,11 +576,11 @@ public final class RequestMappingInfo implements RequestCondition getFileExtensions() { + public Set getFileExtensions() { ContentTypeResolver resolver = getContentTypeResolver(); if (useRegisteredSuffixPatternMatch() && resolver != null) { - if (resolver instanceof FileExtensionContentTypeResolver) { - return ((FileExtensionContentTypeResolver) resolver).getAllFileExtensions(); + if (resolver instanceof MappingContentTypeResolver) { + return ((MappingContentTypeResolver) resolver).getKeys(); } } return null; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java index 96ec3b6c6e..26ae11b43a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java @@ -18,7 +18,7 @@ package org.springframework.web.reactive.result.method.annotation; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; -import java.util.List; +import java.util.Set; import org.springframework.context.EmbeddedValueResolverAware; import org.springframework.core.annotation.AnnotatedElementUtils; @@ -149,7 +149,7 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi /** * Return the file extensions to use for suffix pattern matching. */ - public List getFileExtensions() { + public Set getFileExtensions() { return this.config.getFileExtensions(); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/PatternsRequestConditionTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/PatternsRequestConditionTests.java index 715b247425..4720f94b6a 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/PatternsRequestConditionTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/PatternsRequestConditionTests.java @@ -19,7 +19,7 @@ package org.springframework.web.reactive.result.condition; import java.net.URI; import java.net.URISyntaxException; import java.util.Collections; -import java.util.List; +import java.util.Set; import org.junit.Test; @@ -131,7 +131,7 @@ public class PatternsRequestConditionTests { @Test public void matchSuffixPatternUsingFileExtensions() throws Exception { String[] patterns = new String[] {"/jobs/{jobName}"}; - List extensions = Collections.singletonList("json"); + Set extensions = Collections.singleton("json"); PatternsRequestCondition condition = new PatternsRequestCondition(patterns, null, null, true, false, extensions); ServerWebExchange exchange = createExchange("/jobs/my.job"); @@ -150,7 +150,7 @@ public class PatternsRequestConditionTests { @Test public void matchSuffixPatternUsingFileExtensions2() throws Exception { PatternsRequestCondition condition1 = new PatternsRequestCondition( - new String[] {"/prefix"}, null, null, true, false, Collections.singletonList("json")); + new String[] {"/prefix"}, null, null, true, false, Collections.singleton("json")); PatternsRequestCondition condition2 = new PatternsRequestCondition( new String[] {"/suffix"}, null, null, true, false, null); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMappingTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMappingTests.java index f8feef526d..3afe014a61 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMappingTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMappingTests.java @@ -39,7 +39,7 @@ import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.context.support.StaticWebApplicationContext; -import org.springframework.web.reactive.accept.FileExtensionContentTypeResolver; +import org.springframework.web.reactive.accept.MappingContentTypeResolver; import org.springframework.web.reactive.result.method.RequestMappingInfo; import static org.junit.Assert.assertArrayEquals; @@ -73,8 +73,8 @@ public class RequestMappingHandlerMappingTests { assertTrue(this.handlerMapping.useSuffixPatternMatch()); assertTrue(this.handlerMapping.useRegisteredSuffixPatternMatch()); - FileExtensionContentTypeResolver contentTypeResolver = mock(FileExtensionContentTypeResolver.class); - when(contentTypeResolver.getAllFileExtensions()).thenReturn(Collections.singletonList("json")); + MappingContentTypeResolver contentTypeResolver = mock(MappingContentTypeResolver.class); + when(contentTypeResolver.getKeys()).thenReturn(Collections.singleton("json")); this.handlerMapping.setContentTypeResolver(contentTypeResolver); this.handlerMapping.afterPropertiesSet(); @@ -86,8 +86,8 @@ public class RequestMappingHandlerMappingTests { @Test public void useRegisteredSuffixPatternMatchInitialization() { - FileExtensionContentTypeResolver contentTypeResolver = mock(FileExtensionContentTypeResolver.class); - when(contentTypeResolver.getAllFileExtensions()).thenReturn(Collections.singletonList("json")); + MappingContentTypeResolver contentTypeResolver = mock(MappingContentTypeResolver.class); + when(contentTypeResolver.getKeys()).thenReturn(Collections.singleton("json")); final Set actualExtensions = new HashSet<>(); RequestMappingHandlerMapping localHandlerMapping = new RequestMappingHandlerMapping() { From 1f283acb9820f19baded69fe9cb797dce071197d Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 18 Apr 2016 16:38:33 -0400 Subject: [PATCH 304/498] Add path extension and parameter ContentTypeResolver's --- .../AbstractMappingContentTypeResolver.java | 159 ++++++++++++++ .../accept/ParameterContentTypeResolver.java | 82 ++++++++ .../PathExtensionContentTypeResolver.java | 196 ++++++++++++++++++ .../MappingContentTypeResolverTests.java | 122 +++++++++++ ...ensionContentNegotiationStrategyTests.java | 119 +++++++++++ 5 files changed, 678 insertions(+) create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/AbstractMappingContentTypeResolver.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/ParameterContentTypeResolver.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/PathExtensionContentTypeResolver.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/accept/MappingContentTypeResolverTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/accept/PathExtensionContentNegotiationStrategyTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/AbstractMappingContentTypeResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/AbstractMappingContentTypeResolver.java new file mode 100644 index 0000000000..3e89fe8a73 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/AbstractMappingContentTypeResolver.java @@ -0,0 +1,159 @@ +/* + * 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.web.reactive.accept; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.springframework.http.MediaType; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; +import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.server.ServerWebExchange; + +/** + * Abstract base class for {@link MappingContentTypeResolver} implementations. + * Maintains the actual mappings and pre-implements the overall algorithm with + * sub-classes left to provide a way to extract the lookup key (e.g. file + * extension, query parameter, etc) for a given exchange. + * + * @author Rossen Stoyanchev + */ +public abstract class AbstractMappingContentTypeResolver implements MappingContentTypeResolver { + + /** Primary lookup for media types by key (e.g. "json" -> "application/json") */ + private final ConcurrentMap mediaTypeLookup = new ConcurrentHashMap<>(64); + + /** Reverse lookup for keys associated with a media type */ + private final MultiValueMap keyLookup = new LinkedMultiValueMap<>(64); + + + /** + * Create an instance with the given map of file extensions and media types. + */ + public AbstractMappingContentTypeResolver(Map mediaTypes) { + if (mediaTypes != null) { + for (Map.Entry entry : mediaTypes.entrySet()) { + String extension = entry.getKey().toLowerCase(Locale.ENGLISH); + MediaType mediaType = entry.getValue(); + this.mediaTypeLookup.put(extension, mediaType); + this.keyLookup.add(mediaType, extension); + } + } + } + + + /** + * Sub-classes can use this method to look up a MediaType by key. + * @param key the key converted to lower case + * @return a MediaType or {@code null} + */ + protected MediaType getMediaType(String key) { + return this.mediaTypeLookup.get(key.toLowerCase(Locale.ENGLISH)); + } + + /** + * Sub-classes can use this method get all mapped media types. + */ + protected List getMediaTypes() { + return new ArrayList<>(this.mediaTypeLookup.values()); + } + + + // ContentTypeResolver implementation + + @Override + public List resolveMediaTypes(ServerWebExchange exchange) + throws HttpMediaTypeNotAcceptableException { + + String key = extractKey(exchange); + return resolveMediaTypes(key); + } + + /** + * An overloaded resolve method with a pre-resolved lookup key. + * @param key the key for looking up media types + * @return a list of resolved media types or an empty list + * @throws HttpMediaTypeNotAcceptableException + */ + public List resolveMediaTypes(String key) + throws HttpMediaTypeNotAcceptableException { + + if (StringUtils.hasText(key)) { + MediaType mediaType = getMediaType(key); + if (mediaType != null) { + handleMatch(key, mediaType); + return Collections.singletonList(mediaType); + } + mediaType = handleNoMatch(key); + if (mediaType != null) { + MediaType previous = this.mediaTypeLookup.putIfAbsent(key, mediaType); + if (previous == null) { + this.keyLookup.add(mediaType, key); + } + return Collections.singletonList(mediaType); + } + } + return Collections.emptyList(); + } + + /** + * Extract the key to use to look up a media type from the given exchange, + * e.g. file extension, query parameter, etc. + * @return the key or {@code null} + */ + protected abstract String extractKey(ServerWebExchange exchange); + + /** + * Override to provide handling when a key is successfully resolved via + * {@link #getMediaType(String)}. + */ + @SuppressWarnings("UnusedParameters") + protected void handleMatch(String key, MediaType mediaType) { + } + + /** + * Override to provide handling when a key is not resolved via. + * {@link #getMediaType(String)}. If a MediaType is returned from + * this method it will be added to the mappings. + */ + @SuppressWarnings("UnusedParameters") + protected MediaType handleNoMatch(String key) throws HttpMediaTypeNotAcceptableException { + return null; + } + + // MappingContentTypeResolver implementation + + @Override + public Set getKeysFor(MediaType mediaType) { + List keys = this.keyLookup.get(mediaType); + return (keys != null ? new HashSet<>(keys) : Collections.emptySet()); + } + + @Override + public Set getKeys() { + return new HashSet<>(this.mediaTypeLookup.keySet()); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/ParameterContentTypeResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/ParameterContentTypeResolver.java new file mode 100644 index 0000000000..4b6ed63678 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/ParameterContentTypeResolver.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.web.reactive.accept; + +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.http.MediaType; +import org.springframework.util.Assert; +import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.server.ServerWebExchange; + +/** + * A {@link ContentTypeResolver} that extracts the media type lookup key from a + * known query parameter named "format" by default. + *s + * @author Rossen Stoyanchev + */ +public class ParameterContentTypeResolver extends AbstractMappingContentTypeResolver { + + private static final Log logger = LogFactory.getLog(ParameterContentTypeResolver.class); + + private String parameterName = "format"; + + + /** + * Create an instance with the given map of file extensions and media types. + */ + public ParameterContentTypeResolver(Map mediaTypes) { + super(mediaTypes); + } + + + /** + * Set the name of the parameter to use to determine requested media types. + *

    By default this is set to {@code "format"}. + */ + public void setParameterName(String parameterName) { + Assert.notNull(parameterName, "parameterName is required"); + this.parameterName = parameterName; + } + + public String getParameterName() { + return this.parameterName; + } + + + @Override + protected String extractKey(ServerWebExchange exchange) { + return exchange.getRequest().getQueryParams().getFirst(getParameterName()); + } + + @Override + protected void handleMatch(String mediaTypeKey, MediaType mediaType) { + if (logger.isDebugEnabled()) { + logger.debug("Requested media type is '" + mediaType + + "' based on '" + getParameterName() + "'='" + mediaTypeKey + "'."); + } + } + + @Override + protected MediaType handleNoMatch(String key) throws HttpMediaTypeNotAcceptableException { + throw new HttpMediaTypeNotAcceptableException(getMediaTypes()); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/PathExtensionContentTypeResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/PathExtensionContentTypeResolver.java new file mode 100644 index 0000000000..f8ea0747c8 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/PathExtensionContentTypeResolver.java @@ -0,0 +1,196 @@ +/* + * 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.web.reactive.accept; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Locale; +import java.util.Map; +import javax.activation.FileTypeMap; +import javax.activation.MimetypesFileTypeMap; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.http.MediaType; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.accept.PathExtensionContentNegotiationStrategy; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.util.WebUtils; + +/** + * A {@link ContentTypeResolver} that extracts the file extension from the + * request path and uses that as the media type lookup key. + * + *

    If the file extension is not found in the explicit registrations provided + * to the constructor, the Java Activation Framework (JAF) is used as a fallback + * mechanism. The presence of the JAF is detected and enabled automatically but + * the {@link #setUseJaf(boolean)} property may be set to false. + * + * @author Rossen Stoyanchev + */ +public class PathExtensionContentTypeResolver extends AbstractMappingContentTypeResolver { + + private static final Log logger = LogFactory.getLog(PathExtensionContentNegotiationStrategy.class); + + private static final boolean JAF_PRESENT = ClassUtils.isPresent( + "javax.activation.FileTypeMap", + PathExtensionContentNegotiationStrategy.class.getClassLoader()); + + + private boolean useJaf = true; + + private boolean ignoreUnknownExtensions = true; + + + /** + * Create an instance with the given map of file extensions and media types. + */ + public PathExtensionContentTypeResolver(Map mediaTypes) { + super(mediaTypes); + } + + /** + * Create an instance without any mappings to start with. Mappings may be added + * later on if any extensions are resolved through the Java Activation framework. + */ + public PathExtensionContentTypeResolver() { + super(null); + } + + + /** + * Whether to use the Java Activation Framework to look up file extensions. + *

    By default this is set to "true" but depends on JAF being present. + */ + public void setUseJaf(boolean useJaf) { + this.useJaf = useJaf; + } + + /** + * Whether to ignore requests with unknown file extension. Setting this to + * {@code false} results in {@code HttpMediaTypeNotAcceptableException}. + *

    By default this is set to {@code true}. + */ + public void setIgnoreUnknownExtensions(boolean ignoreUnknownExtensions) { + this.ignoreUnknownExtensions = ignoreUnknownExtensions; + } + + + @Override + protected String extractKey(ServerWebExchange exchange) { + String path = exchange.getRequest().getURI().getRawPath(); + String filename = WebUtils.extractFullFilenameFromUrlPath(path); + String extension = StringUtils.getFilenameExtension(filename); + return (StringUtils.hasText(extension)) ? extension.toLowerCase(Locale.ENGLISH) : null; + } + + @Override + protected MediaType handleNoMatch(String key) throws HttpMediaTypeNotAcceptableException { + if (this.useJaf && JAF_PRESENT) { + MediaType mediaType = JafMediaTypeFactory.getMediaType("file." + key); + if (mediaType != null && !MediaType.APPLICATION_OCTET_STREAM.equals(mediaType)) { + return mediaType; + } + } + if (!this.ignoreUnknownExtensions) { + throw new HttpMediaTypeNotAcceptableException(getMediaTypes()); + } + return null; + } + + /** + * A public method exposing the knowledge of the path extension resolver to + * determine the media type for a given {@link Resource}. First it checks + * the explicitly registered mappings and then falls back on JAF. + * @param resource the resource + * @return the MediaType for the extension or {@code null}. + */ + public MediaType resolveMediaTypeForResource(Resource resource) { + Assert.notNull(resource); + MediaType mediaType = null; + String filename = resource.getFilename(); + String extension = StringUtils.getFilenameExtension(filename); + if (extension != null) { + mediaType = getMediaType(extension); + } + if (mediaType == null && JAF_PRESENT) { + mediaType = JafMediaTypeFactory.getMediaType(filename); + } + if (MediaType.APPLICATION_OCTET_STREAM.equals(mediaType)) { + mediaType = null; + } + return mediaType; + } + + + /** + * Inner class to avoid hard-coded dependency on JAF. + */ + private static class JafMediaTypeFactory { + + private static final FileTypeMap fileTypeMap; + + static { + fileTypeMap = initFileTypeMap(); + } + + /** + * Find extended mime.types from the spring-context-support module. + */ + private static FileTypeMap initFileTypeMap() { + Resource resource = new ClassPathResource("org/springframework/mail/javamail/mime.types"); + if (resource.exists()) { + if (logger.isTraceEnabled()) { + logger.trace("Loading JAF FileTypeMap from " + resource); + } + InputStream inputStream = null; + try { + inputStream = resource.getInputStream(); + return new MimetypesFileTypeMap(inputStream); + } + catch (IOException ex) { + // ignore + } + finally { + if (inputStream != null) { + try { + inputStream.close(); + } + catch (IOException ex) { + // ignore + } + } + } + } + if (logger.isTraceEnabled()) { + logger.trace("Loading default Java Activation Framework FileTypeMap"); + } + return FileTypeMap.getDefaultFileTypeMap(); + } + + public static MediaType getMediaType(String filename) { + String mediaType = fileTypeMap.getContentType(filename); + return (StringUtils.hasText(mediaType) ? MediaType.parseMediaType(mediaType) : null); + } + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/accept/MappingContentTypeResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/accept/MappingContentTypeResolverTests.java new file mode 100644 index 0000000000..6c2cd5a30a --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/accept/MappingContentTypeResolverTests.java @@ -0,0 +1,122 @@ +/* + * 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.web.reactive.accept; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.junit.Test; + +import org.springframework.http.MediaType; +import org.springframework.web.server.ServerWebExchange; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * Unit tests for {@link AbstractMappingContentTypeResolver}. + * @author Rossen Stoyanchev + */ +public class MappingContentTypeResolverTests { + + @Test + public void resolveExtensions() { + Map mapping = Collections.singletonMap("json", MediaType.APPLICATION_JSON); + TestMappingContentTypeResolver resolver = new TestMappingContentTypeResolver("", mapping); + Set keys = resolver.getKeysFor(MediaType.APPLICATION_JSON); + + assertEquals(1, keys.size()); + assertEquals("json", keys.iterator().next()); + } + + @Test + public void resolveExtensionsNoMatch() { + Map mapping = Collections.singletonMap("json", MediaType.APPLICATION_JSON); + TestMappingContentTypeResolver resolver = new TestMappingContentTypeResolver("", mapping); + Set keys = resolver.getKeysFor(MediaType.TEXT_HTML); + + assertTrue(keys.isEmpty()); + } + + @Test // SPR-13747 + public void lookupMediaTypeCaseInsensitive() { + Map mapping = Collections.singletonMap("json", MediaType.APPLICATION_JSON); + TestMappingContentTypeResolver resolver = new TestMappingContentTypeResolver("", mapping); + MediaType mediaType = resolver.getMediaType("JSoN"); + + assertEquals(mediaType, MediaType.APPLICATION_JSON); + } + + @Test + public void resolveMediaTypes() throws Exception { + Map mapping = Collections.singletonMap("json", MediaType.APPLICATION_JSON); + TestMappingContentTypeResolver resolver = new TestMappingContentTypeResolver("json", mapping); + List mediaTypes = resolver.resolveMediaTypes((ServerWebExchange) null); + + assertEquals(1, mediaTypes.size()); + assertEquals("application/json", mediaTypes.get(0).toString()); + } + + @Test + public void resolveMediaTypesNoMatch() throws Exception { + TestMappingContentTypeResolver resolver = new TestMappingContentTypeResolver("blah", null); + List mediaTypes = resolver.resolveMediaTypes((ServerWebExchange) null); + + assertEquals(0, mediaTypes.size()); + } + + @Test + public void resolveMediaTypesNoKey() throws Exception { + Map mapping = Collections.singletonMap("json", MediaType.APPLICATION_JSON); + TestMappingContentTypeResolver resolver = new TestMappingContentTypeResolver(null, mapping); + List mediaTypes = resolver.resolveMediaTypes((ServerWebExchange) null); + + assertEquals(0, mediaTypes.size()); + } + + @Test + public void resolveMediaTypesHandleNoMatch() throws Exception { + TestMappingContentTypeResolver resolver = new TestMappingContentTypeResolver("xml", null); + List mediaTypes = resolver.resolveMediaTypes((ServerWebExchange) null); + + assertEquals(1, mediaTypes.size()); + assertEquals("application/xml", mediaTypes.get(0).toString()); + } + + + private static class TestMappingContentTypeResolver extends AbstractMappingContentTypeResolver { + + private final String key; + + public TestMappingContentTypeResolver(String key, Map mapping) { + super(mapping); + this.key = key; + } + + @Override + protected String extractKey(ServerWebExchange exchange) { + return this.key; + } + + @Override + protected MediaType handleNoMatch(String mappingKey) { + return "xml".equals(mappingKey) ? MediaType.APPLICATION_XML : null; + } + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/accept/PathExtensionContentNegotiationStrategyTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/accept/PathExtensionContentNegotiationStrategyTests.java new file mode 100644 index 0000000000..4399f7c56e --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/accept/PathExtensionContentNegotiationStrategyTests.java @@ -0,0 +1,119 @@ +/* + * 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.web.reactive.accept; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.junit.Test; + +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.WebSessionManager; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; + +/** + * Unit tests for {@link PathExtensionContentTypeResolver}. + * + * @author Rossen Stoyanchev + */ +public class PathExtensionContentNegotiationStrategyTests { + + @Test + public void resolveMediaTypesFromMapping() throws Exception { + ServerWebExchange exchange = createExchange("/test.html"); + PathExtensionContentTypeResolver resolver = new PathExtensionContentTypeResolver(); + List mediaTypes = resolver.resolveMediaTypes(exchange); + + assertEquals(Collections.singletonList(new MediaType("text", "html")), mediaTypes); + + Map mapping = Collections.singletonMap("HTML", MediaType.APPLICATION_XHTML_XML); + resolver = new PathExtensionContentTypeResolver(mapping); + mediaTypes = resolver.resolveMediaTypes(exchange); + + assertEquals(Collections.singletonList(new MediaType("application", "xhtml+xml")), mediaTypes); + } + + @Test + public void resolveMediaTypesFromJaf() throws Exception { + ServerWebExchange exchange = createExchange("test.xls"); + PathExtensionContentTypeResolver resolver = new PathExtensionContentTypeResolver(); + List mediaTypes = resolver.resolveMediaTypes(exchange); + + assertEquals(Collections.singletonList(new MediaType("application", "vnd.ms-excel")), mediaTypes); + } + + // SPR-10334 + + @Test + public void getMediaTypeFromFilenameNoJaf() throws Exception { + ServerWebExchange exchange = createExchange("test.json"); + PathExtensionContentTypeResolver resolver = new PathExtensionContentTypeResolver(); + resolver.setUseJaf(false); + List mediaTypes = resolver.resolveMediaTypes(exchange); + + assertEquals(Collections.emptyList(), mediaTypes); + } + + // SPR-9390 + + @Test + public void getMediaTypeFilenameWithEncodedURI() throws Exception { + ServerWebExchange exchange = createExchange("/quo%20vadis%3f.html"); + PathExtensionContentTypeResolver resolver = new PathExtensionContentTypeResolver(); + List result = resolver.resolveMediaTypes(exchange); + + assertEquals("Invalid content type", Collections.singletonList(new MediaType("text", "html")), result); + } + + // SPR-10170 + + @Test + public void resolveMediaTypesIgnoreUnknownExtension() throws Exception { + ServerWebExchange exchange = createExchange("test.xyz"); + PathExtensionContentTypeResolver resolver = new PathExtensionContentTypeResolver(); + List mediaTypes = resolver.resolveMediaTypes(exchange); + + assertEquals(Collections.emptyList(), mediaTypes); + } + + @Test(expected = HttpMediaTypeNotAcceptableException.class) + public void resolveMediaTypesDoNotIgnoreUnknownExtension() throws Exception { + ServerWebExchange exchange = createExchange("test.xyz"); + PathExtensionContentTypeResolver resolver = new PathExtensionContentTypeResolver(); + resolver.setIgnoreUnknownExtensions(false); + resolver.resolveMediaTypes(exchange); + } + + + private ServerWebExchange createExchange(String path) throws URISyntaxException { + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI(path)); + WebSessionManager sessionManager = mock(WebSessionManager.class); + return new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); + } + +} From 4af99473ffa36a75fb4d6d521f736a403a4f904f Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 18 Apr 2016 17:40:33 -0400 Subject: [PATCH 305/498] Add CompositeContenTypeResolver and a builder This is the equivalent of the existing ContentNegotiationManager + ContentNegotiationManagerFactoryBean --- .../accept/CompositeContentTypeResolver.java | 105 +++++++++ .../CompositeContentTypeResolverBuilder.java | 200 ++++++++++++++++++ .../accept/FixedContentTypeResolver.java | 48 +++++ ...positeContentTypeResolverBuilderTests.java | 196 +++++++++++++++++ 4 files changed, 549 insertions(+) create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/CompositeContentTypeResolver.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/CompositeContentTypeResolverBuilder.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/FixedContentTypeResolver.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/accept/CompositeContentTypeResolverBuilderTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/CompositeContentTypeResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/CompositeContentTypeResolver.java new file mode 100644 index 0000000000..71377ab377 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/CompositeContentTypeResolver.java @@ -0,0 +1,105 @@ +/* + * 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.web.reactive.accept; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import org.springframework.http.MediaType; +import org.springframework.util.Assert; +import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.server.ServerWebExchange; + +/** + * A {@link ContentTypeResolver} that contains and delegates to a list of other + * resolvers. + * + *

    Also an implementation of {@link MappingContentTypeResolver} that delegates + * to those resolvers from the list that are also of type + * {@code MappingContentTypeResolver}. + * + * @author Rossen Stoyanchev + */ +public class CompositeContentTypeResolver implements MappingContentTypeResolver { + + private final List resolvers = new ArrayList<>(); + + + public CompositeContentTypeResolver(List resolvers) { + Assert.notEmpty(resolvers, "At least one resolver is expected."); + this.resolvers.addAll(resolvers); + } + + + /** + * Return a read-only list of the configured resolvers. + */ + public List getResolvers() { + return Collections.unmodifiableList(this.resolvers); + } + + /** + * Return the first {@link ContentTypeResolver} of the given type. + * @param resolverType the resolver type + * @return the first matching resolver or {@code null}. + */ + @SuppressWarnings("unchecked") + public T findResolver(Class resolverType) { + for (ContentTypeResolver resolver : this.resolvers) { + if (resolverType.isInstance(resolver)) { + return (T) resolver; + } + } + return null; + } + + + @Override + public List resolveMediaTypes(ServerWebExchange exchange) throws HttpMediaTypeNotAcceptableException { + for (ContentTypeResolver resolver : this.resolvers) { + List mediaTypes = resolver.resolveMediaTypes(exchange); + if (mediaTypes.isEmpty() || (mediaTypes.size() == 1 && mediaTypes.contains(MediaType.ALL))) { + continue; + } + return mediaTypes; + } + return Collections.emptyList(); + } + + @Override + public Set getKeysFor(MediaType mediaType) { + Set result = new LinkedHashSet<>(); + for (ContentTypeResolver resolver : this.resolvers) { + if (resolver instanceof MappingContentTypeResolver) + result.addAll(((MappingContentTypeResolver) resolver).getKeysFor(mediaType)); + } + return result; + } + + @Override + public Set getKeys() { + Set result = new LinkedHashSet<>(); + for (ContentTypeResolver resolver : this.resolvers) { + if (resolver instanceof MappingContentTypeResolver) + result.addAll(((MappingContentTypeResolver) resolver).getKeys()); + } + return result; + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/CompositeContentTypeResolverBuilder.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/CompositeContentTypeResolverBuilder.java new file mode 100644 index 0000000000..d136e323cc --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/CompositeContentTypeResolverBuilder.java @@ -0,0 +1,200 @@ +/* + * 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.web.reactive.accept; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import org.springframework.http.MediaType; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + + +/** + * Builder for {@link CompositeContentTypeResolver}. + * + * @author Rossen Stoyanchev + */ +public class CompositeContentTypeResolverBuilder { + + private boolean favorPathExtension = true; + + private boolean favorParameter = false; + + private boolean ignoreAcceptHeader = false; + + private Map mediaTypes = new HashMap<>(); + + private boolean ignoreUnknownPathExtensions = true; + + private Boolean useJaf; + + private String parameterName = "format"; + + private ContentTypeResolver contentTypeResolver; + + + /** + * Whether the path extension in the URL path should be used to determine + * the requested media type. + *

    By default this is set to {@code true} in which case a request + * for {@code /hotels.pdf} will be interpreted as a request for + * {@code "application/pdf"} regardless of the 'Accept' header. + */ + public CompositeContentTypeResolverBuilder favorPathExtension(boolean favorPathExtension) { + this.favorPathExtension = favorPathExtension; + return this; + } + + /** + * Add a mapping from a key, extracted from a path extension or a query + * parameter, to a MediaType. This is required in order for the parameter + * strategy to work. Any extensions explicitly registered here are also + * whitelisted for the purpose of Reflected File Download attack detection + * (see Spring Framework reference documentation for more details on RFD + * attack protection). + *

    The path extension strategy will also try to use JAF (if present) to + * resolve path extensions. To change this behavior see {@link #useJaf}. + * @param mediaTypes media type mappings + */ + public CompositeContentTypeResolverBuilder mediaTypes(Map mediaTypes) { + if (!CollectionUtils.isEmpty(mediaTypes)) { + for (Map.Entry entry : mediaTypes.entrySet()) { + String extension = entry.getKey().toLowerCase(Locale.ENGLISH); + this.mediaTypes.put(extension, entry.getValue()); + } + } + return this; + } + + /** + * Alternative to {@link #mediaTypes} to add a single mapping. + */ + public CompositeContentTypeResolverBuilder mediaType(String key, MediaType mediaType) { + this.mediaTypes.put(key, mediaType); + return this; + } + + /** + * Whether to ignore requests with path extension that cannot be resolved + * to any media type. Setting this to {@code false} will result in an + * {@link org.springframework.web.HttpMediaTypeNotAcceptableException} if + * there is no match. + *

    By default this is set to {@code true}. + */ + public CompositeContentTypeResolverBuilder ignoreUnknownPathExtensions(boolean ignore) { + this.ignoreUnknownPathExtensions = ignore; + return this; + } + + /** + * When {@link #favorPathExtension favorPathExtension} is set, this + * property determines whether to allow use of JAF (Java Activation Framework) + * to resolve a path extension to a specific MediaType. + *

    By default this is not set in which case + * {@code PathExtensionContentNegotiationStrategy} will use JAF if available. + */ + public CompositeContentTypeResolverBuilder useJaf(boolean useJaf) { + this.useJaf = useJaf; + return this; + } + + /** + * Whether a request parameter ("format" by default) should be used to + * determine the requested media type. For this option to work you must + * register {@link #mediaTypes media type mappings}. + *

    By default this is set to {@code false}. + * @see #parameterName + */ + public CompositeContentTypeResolverBuilder favorParameter(boolean favorParameter) { + this.favorParameter = favorParameter; + return this; + } + + /** + * Set the query parameter name to use when {@link #favorParameter} is on. + *

    The default parameter name is {@code "format"}. + */ + public CompositeContentTypeResolverBuilder parameterName(String parameterName) { + Assert.notNull(parameterName, "parameterName is required"); + this.parameterName = parameterName; + return this; + } + + /** + * Whether to disable checking the 'Accept' request header. + *

    By default this value is set to {@code false}. + */ + public CompositeContentTypeResolverBuilder ignoreAcceptHeader(boolean ignoreAcceptHeader) { + this.ignoreAcceptHeader = ignoreAcceptHeader; + return this; + } + + /** + * Set the default content type to use when no content type is requested. + *

    By default this is not set. + * @see #defaultContentTypeResolver + */ + public CompositeContentTypeResolverBuilder defaultContentType(MediaType contentType) { + this.contentTypeResolver = new FixedContentTypeResolver(contentType); + return this; + } + + /** + * Set a custom {@link ContentTypeResolver} to use to determine + * the content type to use when no content type is requested. + *

    By default this is not set. + * @see #defaultContentType + */ + public CompositeContentTypeResolverBuilder defaultContentTypeResolver(ContentTypeResolver resolver) { + this.contentTypeResolver = resolver; + return this; + } + + + public CompositeContentTypeResolver build() { + List resolvers = new ArrayList<>(); + + if (this.favorPathExtension) { + PathExtensionContentTypeResolver resolver = new PathExtensionContentTypeResolver(this.mediaTypes); + resolver.setIgnoreUnknownExtensions(this.ignoreUnknownPathExtensions); + if (this.useJaf != null) { + resolver.setUseJaf(this.useJaf); + } + resolvers.add(resolver); + } + + if (this.favorParameter) { + ParameterContentTypeResolver resolver = new ParameterContentTypeResolver(this.mediaTypes); + resolver.setParameterName(this.parameterName); + resolvers.add(resolver); + } + + if (!this.ignoreAcceptHeader) { + resolvers.add(new HeaderContentTypeResolver()); + } + + if (this.contentTypeResolver != null) { + resolvers.add(this.contentTypeResolver); + } + + return new CompositeContentTypeResolver(resolvers); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/FixedContentTypeResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/FixedContentTypeResolver.java new file mode 100644 index 0000000000..a6c73bc0d1 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/FixedContentTypeResolver.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2015 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.web.reactive.accept; + +import java.util.Collections; +import java.util.List; + +import org.springframework.http.MediaType; +import org.springframework.web.server.ServerWebExchange; + +/** + * A {@link ContentTypeResolver} that resolves to a fixed list of media types. + * + * @author Rossen Stoyanchev + */ +public class FixedContentTypeResolver implements ContentTypeResolver { + + private final List mediaTypes; + + + /** + * Create an instance with the given content type. + */ + public FixedContentTypeResolver(MediaType mediaTypes) { + this.mediaTypes = Collections.singletonList(mediaTypes); + } + + + @Override + public List resolveMediaTypes(ServerWebExchange exchange) { + return this.mediaTypes; + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/accept/CompositeContentTypeResolverBuilderTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/accept/CompositeContentTypeResolverBuilderTests.java new file mode 100644 index 0000000000..074dc914a7 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/accept/CompositeContentTypeResolverBuilderTests.java @@ -0,0 +1,196 @@ +/* + * 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.web.reactive.accept; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Collections; + +import org.junit.Test; + +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.WebSessionManager; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; + +/** + * Unit tests for {@link CompositeContentTypeResolverBuilder}. + * + * @author Rossen Stoyanchev + */ +public class CompositeContentTypeResolverBuilderTests { + + @Test + public void defaultSettings() throws Exception { + CompositeContentTypeResolver resolver = new CompositeContentTypeResolverBuilder().build(); + + ServerWebExchange exchange = createExchange("/flower.gif"); + + assertEquals("Should be able to resolve file extensions by default", + Collections.singletonList(MediaType.IMAGE_GIF), resolver.resolveMediaTypes(exchange)); + + exchange = createExchange("/flower.xyz"); + + assertEquals("Should ignore unknown extensions by default", + Collections.emptyList(), resolver.resolveMediaTypes(exchange)); + + exchange = createExchange("/flower"); + exchange.getRequest().getQueryParams().add("format", "gif"); + + assertEquals("Should not resolve request parameters by default", + Collections.emptyList(), resolver.resolveMediaTypes(exchange)); + + exchange = createExchange("/flower"); + exchange.getRequest().getHeaders().setAccept(Collections.singletonList(MediaType.IMAGE_GIF)); + + assertEquals("Should resolve Accept header by default", + Collections.singletonList(MediaType.IMAGE_GIF), resolver.resolveMediaTypes(exchange)); + } + + @Test + public void favorPath() throws Exception { + CompositeContentTypeResolver resolver = new CompositeContentTypeResolverBuilder() + .favorPathExtension(true) + .mediaType("foo", new MediaType("application", "foo")) + .mediaType("bar", new MediaType("application", "bar")) + .build(); + + ServerWebExchange exchange = createExchange("/flower.foo"); + assertEquals(Collections.singletonList(new MediaType("application", "foo")), + resolver.resolveMediaTypes(exchange)); + + exchange = createExchange("/flower.bar"); + assertEquals(Collections.singletonList(new MediaType("application", "bar")), + resolver.resolveMediaTypes(exchange)); + + exchange = createExchange("/flower.gif"); + assertEquals(Collections.singletonList(MediaType.IMAGE_GIF), resolver.resolveMediaTypes(exchange)); + } + + @Test + public void favorPathWithJafTurnedOff() throws Exception { + CompositeContentTypeResolver resolver = new CompositeContentTypeResolverBuilder() + .favorPathExtension(true) + .useJaf(false) + .build(); + + ServerWebExchange exchange = createExchange("/flower.foo"); + assertEquals(Collections.emptyList(), resolver.resolveMediaTypes(exchange)); + + exchange = createExchange("/flower.gif"); + assertEquals(Collections.emptyList(), resolver.resolveMediaTypes(exchange)); + } + + @Test(expected = HttpMediaTypeNotAcceptableException.class) // SPR-10170 + public void favorPathWithIgnoreUnknownPathExtensionTurnedOff() throws Exception { + CompositeContentTypeResolver resolver = new CompositeContentTypeResolverBuilder() + .favorPathExtension(true) + .ignoreUnknownPathExtensions(false) + .build(); + + ServerWebExchange exchange = createExchange("/flower.xyz"); + exchange.getRequest().getQueryParams().add("format", "json"); + + resolver.resolveMediaTypes(exchange); + } + + @Test + public void favorParameter() throws Exception { + CompositeContentTypeResolver resolver = new CompositeContentTypeResolverBuilder() + .favorParameter(true) + .mediaType("json", MediaType.APPLICATION_JSON) + .build(); + + ServerWebExchange exchange = createExchange("/flower"); + exchange.getRequest().getQueryParams().add("format", "json"); + + assertEquals(Collections.singletonList(MediaType.APPLICATION_JSON), + resolver.resolveMediaTypes(exchange)); + } + + @Test(expected = HttpMediaTypeNotAcceptableException.class) // SPR-10170 + public void favorParameterWithUnknownMediaType() throws Exception { + CompositeContentTypeResolver resolver = new CompositeContentTypeResolverBuilder() + .favorParameter(true) + .build(); + + ServerWebExchange exchange = createExchange("/flower"); + exchange.getRequest().getQueryParams().add("format", "xyz"); + + resolver.resolveMediaTypes(exchange); + } + + @Test + public void ignoreAcceptHeader() throws Exception { + CompositeContentTypeResolver resolver = new CompositeContentTypeResolverBuilder() + .ignoreAcceptHeader(true) + .build(); + + ServerWebExchange exchange = createExchange("/flower"); + exchange.getRequest().getHeaders().setAccept(Collections.singletonList(MediaType.IMAGE_GIF)); + + assertEquals(Collections.emptyList(), resolver.resolveMediaTypes(exchange)); + } + + @Test // SPR-10513 + public void setDefaultContentType() throws Exception { + CompositeContentTypeResolver resolver = new CompositeContentTypeResolverBuilder() + .defaultContentType(MediaType.APPLICATION_JSON) + .build(); + + ServerWebExchange exchange = createExchange("/"); + + assertEquals(Collections.singletonList(MediaType.APPLICATION_JSON), + resolver.resolveMediaTypes(exchange)); + + exchange.getRequest().getHeaders().setAccept(Collections.singletonList(MediaType.ALL)); + + assertEquals(Collections.singletonList(MediaType.APPLICATION_JSON), + resolver.resolveMediaTypes(exchange)); + } + + @Test // SPR-12286 + public void setDefaultContentTypeWithStrategy() throws Exception { + CompositeContentTypeResolver resolver = new CompositeContentTypeResolverBuilder() + .defaultContentTypeResolver(new FixedContentTypeResolver(MediaType.APPLICATION_JSON)) + .build(); + + ServerWebExchange exchange = createExchange("/"); + + assertEquals(Collections.singletonList(MediaType.APPLICATION_JSON), + resolver.resolveMediaTypes(exchange)); + + exchange.getRequest().getHeaders().setAccept(Collections.singletonList(MediaType.ALL)); + assertEquals(Collections.singletonList(MediaType.APPLICATION_JSON), + resolver.resolveMediaTypes(exchange)); + } + + + private ServerWebExchange createExchange(String path) throws URISyntaxException { + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI(path)); + WebSessionManager sessionManager = mock(WebSessionManager.class); + return new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); + } + +} From 92c72b93a61b84a8d94119e8126e964f933f09b5 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 18 Apr 2016 17:50:57 -0400 Subject: [PATCH 306/498] Switch defaults for ContentTypeResolver Now that we have a CompositeContentTypeResolverBuilder with built-in defaults, we switch to those defaults in places where a ContentTypeResolver is used. --- .../CompositeContentTypeResolverBuilder.java | 50 ++++++++++++++++++- .../condition/ProducesRequestCondition.java | 4 +- .../RequestMappingHandlerMapping.java | 4 +- .../RequestMappingHandlerMappingTests.java | 2 +- 4 files changed, 56 insertions(+), 4 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/CompositeContentTypeResolverBuilder.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/CompositeContentTypeResolverBuilder.java index d136e323cc..f545c08911 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/CompositeContentTypeResolverBuilder.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/CompositeContentTypeResolverBuilder.java @@ -27,7 +27,55 @@ import org.springframework.util.CollectionUtils; /** - * Builder for {@link CompositeContentTypeResolver}. + * Factory to create a {@link CompositeContentTypeResolver} and configure it with + * one or more {@link ContentTypeResolver} instances with build style methods. + * The following table shows methods, resulting strategy instances, and if in + * use by default: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    Property SetterUnderlying StrategyDefault Setting
    {@link #favorPathExtension}{@link PathExtensionContentTypeResolver Path Extension resolver}On
    {@link #favorParameter}{@link ParameterContentTypeResolver Parameter resolver}Off
    {@link #ignoreAcceptHeader}{@link HeaderContentTypeResolver Header resolver}On
    {@link #defaultContentType}{@link FixedContentTypeResolver Fixed content resolver}Not set
    {@link #defaultContentTypeResolver}{@link ContentTypeResolver}Not set
    + * + *

    The order in which resolvers are configured is fixed. Config methods may only + * turn individual resolvers on or off. If you need a custom order for any + * reason simply instantiate {@code {@link CompositeContentTypeResolver}} directly. + * + *

    For the path extension and parameter resolvers you may explicitly add + * {@link #mediaTypes(Map)}. This will be used to resolve path extensions or a + * parameter value such as "json" to a media type such as "application/json". + * + *

    The path extension strategy will also use the Java Activation framework + * (JAF), if available, to resolve a path extension to a MediaType. You may + * {@link #useJaf suppress} the use of JAF. * * @author Rossen Stoyanchev */ diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/ProducesRequestCondition.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/ProducesRequestCondition.java index d9943b009a..156d420440 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/ProducesRequestCondition.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/ProducesRequestCondition.java @@ -28,6 +28,7 @@ import org.springframework.http.MediaType; import org.springframework.web.HttpMediaTypeNotAcceptableException; import org.springframework.web.accept.ContentNegotiationManager; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.reactive.accept.CompositeContentTypeResolverBuilder; import org.springframework.web.reactive.accept.ContentTypeResolver; import org.springframework.web.reactive.accept.HeaderContentTypeResolver; import org.springframework.web.server.ServerWebExchange; @@ -96,7 +97,8 @@ public final class ProducesRequestCondition extends AbstractRequestCondition(expressions); Collections.sort(this.expressions); - this.contentTypeResolver = (resolver != null ? resolver : new HeaderContentTypeResolver()); + this.contentTypeResolver = (resolver != null ? + resolver : new CompositeContentTypeResolverBuilder().build()); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java index 26ae11b43a..a5f23e986b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java @@ -27,6 +27,8 @@ import org.springframework.util.Assert; import org.springframework.util.StringValueResolver; import org.springframework.web.accept.ContentNegotiationManager; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.reactive.accept.CompositeContentTypeResolver; +import org.springframework.web.reactive.accept.CompositeContentTypeResolverBuilder; import org.springframework.web.reactive.accept.ContentTypeResolver; import org.springframework.web.reactive.accept.HeaderContentTypeResolver; import org.springframework.web.reactive.result.condition.RequestCondition; @@ -49,7 +51,7 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi private boolean useTrailingSlashMatch = true; - private ContentTypeResolver contentTypeResolver = new HeaderContentTypeResolver(); + private ContentTypeResolver contentTypeResolver = new CompositeContentTypeResolverBuilder().build(); private StringValueResolver embeddedValueResolver; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMappingTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMappingTests.java index 3afe014a61..8635b6f9eb 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMappingTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMappingTests.java @@ -81,7 +81,7 @@ public class RequestMappingHandlerMappingTests { assertTrue(this.handlerMapping.useSuffixPatternMatch()); assertTrue(this.handlerMapping.useRegisteredSuffixPatternMatch()); - assertEquals(Collections.singletonList("json"), this.handlerMapping.getFileExtensions()); + assertEquals(Collections.singleton("json"), this.handlerMapping.getFileExtensions()); } @Test From 00eff4b611ad245520bd21ae914a7150a582c8e3 Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Tue, 19 Apr 2016 09:07:23 +0100 Subject: [PATCH 307/498] Rename SchedulerGroup to Computations and its methods to avoid confusion --- .../http/server/reactive/AsyncIntegrationTests.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java index 90b5dcfdf1..69658622bc 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java @@ -23,7 +23,7 @@ import org.hamcrest.Matchers; import org.junit.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import reactor.core.publisher.SchedulerGroup; +import reactor.core.publisher.Computations; import reactor.core.scheduler.Scheduler; import reactor.core.scheduler.Timer; @@ -42,7 +42,7 @@ import static org.junit.Assert.assertThat; */ public class AsyncIntegrationTests extends AbstractHttpHandlerIntegrationTests { - private final Scheduler asyncGroup = SchedulerGroup.async(); + private final Scheduler asyncGroup = Computations.parallel(); private final DataBufferAllocator allocator = new DefaultDataBufferAllocator(); From 66707835fee8a748bfe6312697cbbecc7d4fc3bd Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 19 Apr 2016 11:18:57 -0400 Subject: [PATCH 308/498] Move ResponseStatusException into server sub-package --- .../web/ResponseStatusException.java | 45 ------------ .../DispatcherHandlerExceptionMapper.java | 2 +- .../ResponseStatusExceptionHandler.java | 4 +- .../web/server/ResponseStatusException.java | 68 +++++++++++++++++++ .../reactive/DispatcherHandlerErrorTests.java | 2 +- ...DispatcherHandlerExceptionMapperTests.java | 10 +-- .../ResponseStatusExceptionHandlerTests.java | 2 +- 7 files changed, 78 insertions(+), 55 deletions(-) delete mode 100644 spring-web-reactive/src/main/java/org/springframework/web/ResponseStatusException.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/server/ResponseStatusException.java diff --git a/spring-web-reactive/src/main/java/org/springframework/web/ResponseStatusException.java b/spring-web-reactive/src/main/java/org/springframework/web/ResponseStatusException.java deleted file mode 100644 index 3aa9eac84f..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/web/ResponseStatusException.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2002-2015 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.web; - -import org.springframework.core.NestedRuntimeException; -import org.springframework.http.HttpStatus; - -/** - * Exception wrapper to associate an exception with a status code at runtime. - * - * @author Rossen Stoyanchev - */ -public class ResponseStatusException extends NestedRuntimeException { - - private final HttpStatus httpStatus; - - - public ResponseStatusException(HttpStatus status) { - this(status, null); - } - - public ResponseStatusException(HttpStatus status, Throwable cause) { - super("Request processing failure with status code: " + status, cause); - this.httpStatus = status; - } - - - public HttpStatus getHttpStatus() { - return this.httpStatus; - } - -} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandlerExceptionMapper.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandlerExceptionMapper.java index 07a39d735f..c1b27fa791 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandlerExceptionMapper.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandlerExceptionMapper.java @@ -20,7 +20,7 @@ import java.util.function.Function; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.http.HttpStatus; import org.springframework.web.HttpMediaTypeNotAcceptableException; -import org.springframework.web.ResponseStatusException; +import org.springframework.web.server.ResponseStatusException; import org.springframework.web.bind.annotation.ResponseStatus; /** diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/ResponseStatusExceptionHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/ResponseStatusExceptionHandler.java index add1da9fdc..6a931a01a1 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/ResponseStatusExceptionHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/ResponseStatusExceptionHandler.java @@ -17,7 +17,7 @@ package org.springframework.web.reactive; import reactor.core.publisher.Mono; -import org.springframework.web.ResponseStatusException; +import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.WebExceptionHandler; import org.springframework.web.server.ServerWebExchange; @@ -32,7 +32,7 @@ public class ResponseStatusExceptionHandler implements WebExceptionHandler { @Override public Mono handle(ServerWebExchange exchange, Throwable ex) { if (ex instanceof ResponseStatusException) { - exchange.getResponse().setStatusCode(((ResponseStatusException) ex).getHttpStatus()); + exchange.getResponse().setStatusCode(((ResponseStatusException) ex).getStatus()); return Mono.empty(); } return Mono.error(ex); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/ResponseStatusException.java b/spring-web-reactive/src/main/java/org/springframework/web/server/ResponseStatusException.java new file mode 100644 index 0000000000..86cdd15d87 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/ResponseStatusException.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2015 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.web.server; + +import org.springframework.core.NestedRuntimeException; +import org.springframework.http.HttpStatus; +import org.springframework.util.Assert; + +/** + * Base class for exceptions associated with specific HTTP response status codes. + * + * @author Rossen Stoyanchev + */ +public class ResponseStatusException extends NestedRuntimeException { + + private final HttpStatus status; + + private final String reason; + + + /** + * Constructor with a response code and a reason to add to the exception + * message as explanation. + */ + public ResponseStatusException(HttpStatus status, String reason) { + this(status, reason, null); + } + + /** + * Constructor with a nested exception. + */ + public ResponseStatusException(HttpStatus status, String reason, Throwable cause) { + super("Request failure [status: " + status + ", reason: \"" + reason + "\"]", cause); + Assert.notNull(status, "'status' is required"); + Assert.notNull(reason, "'reason' is required"); + this.status = status; + this.reason = reason; + } + + + /** + * The HTTP status that fits the exception. + */ + public HttpStatus getStatus() { + return this.status; + } + + /** + * The reason explaining the exception. + */ + public String getReason() { + return this.reason; + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java index 99b99b004b..cd5dce7650 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java @@ -42,7 +42,7 @@ import org.springframework.http.server.reactive.MockServerHttpRequest; import org.springframework.http.server.reactive.MockServerHttpResponse; import org.springframework.stereotype.Controller; import org.springframework.web.HttpMediaTypeNotAcceptableException; -import org.springframework.web.ResponseStatusException; +import org.springframework.web.server.ResponseStatusException; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerExceptionMapperTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerExceptionMapperTests.java index 4fd4b152b4..8c4ede4ab6 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerExceptionMapperTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerExceptionMapperTests.java @@ -22,7 +22,7 @@ import org.junit.Test; import org.springframework.http.HttpStatus; import org.springframework.web.HttpMediaTypeNotAcceptableException; -import org.springframework.web.ResponseStatusException; +import org.springframework.web.server.ResponseStatusException; import org.springframework.web.bind.annotation.ResponseStatus; import static org.junit.Assert.assertEquals; @@ -47,7 +47,7 @@ public class DispatcherHandlerExceptionMapperTests { ex = this.mapper.apply(ex); assertEquals(ResponseStatusException.class, ex.getClass()); - assertEquals(HttpStatus.NOT_FOUND, ((ResponseStatusException) ex).getHttpStatus()); + assertEquals(HttpStatus.NOT_FOUND, ((ResponseStatusException) ex).getStatus()); } @@ -57,7 +57,7 @@ public class DispatcherHandlerExceptionMapperTests { ex = this.mapper.apply(ex); assertEquals(ResponseStatusException.class, ex.getClass()); - assertEquals(HttpStatus.NOT_ACCEPTABLE, ((ResponseStatusException) ex).getHttpStatus()); + assertEquals(HttpStatus.NOT_ACCEPTABLE, ((ResponseStatusException) ex).getStatus()); } @Test @@ -66,7 +66,7 @@ public class DispatcherHandlerExceptionMapperTests { ex = this.mapper.apply(ex); assertEquals(ResponseStatusException.class, ex.getClass()); - assertEquals(HttpStatus.BAD_REQUEST, ((ResponseStatusException) ex).getHttpStatus()); + assertEquals(HttpStatus.BAD_REQUEST, ((ResponseStatusException) ex).getStatus()); } @Test @@ -75,7 +75,7 @@ public class DispatcherHandlerExceptionMapperTests { ex = this.mapper.apply(ex); assertEquals(ResponseStatusException.class, ex.getClass()); - assertEquals(HttpStatus.BAD_REQUEST, ((ResponseStatusException) ex).getHttpStatus()); + assertEquals(HttpStatus.BAD_REQUEST, ((ResponseStatusException) ex).getStatus()); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java index 37866c5311..c5b85760a7 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java @@ -26,7 +26,7 @@ import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.MockServerHttpRequest; import org.springframework.http.server.reactive.MockServerHttpResponse; -import org.springframework.web.ResponseStatusException; +import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange; import org.springframework.web.server.session.WebSessionManager; From 8f0fe713f534efb74d63753f1a2d9477fbc7223e Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 19 Apr 2016 15:30:13 -0400 Subject: [PATCH 309/498] Add concrete ResponseStatusException sub-classes --- .../web/server/BadRequestStatusException.java | 63 ++++++++++++++++ .../web/server/MethodNotAllowedException.java | 59 +++++++++++++++ .../server/NotAcceptableStatusException.java | 59 +++++++++++++++ .../UnsupportedMediaTypeStatusException.java | 71 +++++++++++++++++++ 4 files changed, 252 insertions(+) create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/server/BadRequestStatusException.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/server/MethodNotAllowedException.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/server/NotAcceptableStatusException.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/server/UnsupportedMediaTypeStatusException.java diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/BadRequestStatusException.java b/spring-web-reactive/src/main/java/org/springframework/web/server/BadRequestStatusException.java new file mode 100644 index 0000000000..5f469b0dd7 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/BadRequestStatusException.java @@ -0,0 +1,63 @@ +/* + * 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.web.server; + +import java.util.Optional; + +import org.springframework.core.MethodParameter; +import org.springframework.http.HttpStatus; + +/** + * Exception for errors that fit response status 400 (bad request). + * + * @author Rossen Stoyanchev + */ +public class BadRequestStatusException extends ResponseStatusException { + + private final MethodParameter parameter; + + + /** + * Constructor with an explanation only. + */ + public BadRequestStatusException(String reason) { + this(reason, null); + } + + /** + * Constructor for a 400 error linked to a specific {@code MethodParameter}. + */ + public BadRequestStatusException(String reason, MethodParameter parameter) { + this(reason, parameter, null); + } + + /** + * Constructor for a 400 error with a root cause. + */ + public BadRequestStatusException(String reason, MethodParameter parameter, Throwable cause) { + super(HttpStatus.BAD_REQUEST, reason, cause); + this.parameter = parameter; + } + + + /** + * Return the {@code MethodParameter} associated with this error, if any. + */ + public Optional getMethodParameter() { + return Optional.ofNullable(this.parameter); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/MethodNotAllowedException.java b/spring-web-reactive/src/main/java/org/springframework/web/server/MethodNotAllowedException.java new file mode 100644 index 0000000000..3759483a64 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/MethodNotAllowedException.java @@ -0,0 +1,59 @@ +/* + * 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.web.server; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.springframework.http.HttpStatus; +import org.springframework.util.Assert; + +/** + * Exception for errors that fit response status 405 (method not allowed). + * + * @author Rossen Stoyanchev + */ +public class MethodNotAllowedException extends ResponseStatusException { + + private String method; + + private Set supportedMethods; + + + public MethodNotAllowedException(String method, Collection supportedMethods) { + super(HttpStatus.METHOD_NOT_ALLOWED, "Request method '" + method + "' not supported"); + Assert.notNull(method, "'method' is required"); + this.method = method; + this.supportedMethods = Collections.unmodifiableSet(new HashSet<>(supportedMethods)); + } + + + /** + * Return the HTTP method for the failed request. + */ + public String getHttpMethod() { + return this.method; + } + + /** + * Return the list of supported HTTP methods. + */ + public Set getSupportedMethods() { + return supportedMethods; + } +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/NotAcceptableStatusException.java b/spring-web-reactive/src/main/java/org/springframework/web/server/NotAcceptableStatusException.java new file mode 100644 index 0000000000..fde57105b4 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/NotAcceptableStatusException.java @@ -0,0 +1,59 @@ +/* + * 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.web.server; + +import java.util.Collections; +import java.util.List; + +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; + +/** + * Exception for errors that fit response status 405 (not acceptable). + * + * @author Rossen Stoyanchev + */ +public class NotAcceptableStatusException extends ResponseStatusException { + + private final List supportedMediaTypes; + + + /** + * Constructor for when the requested Content-Type is invalid. + */ + public NotAcceptableStatusException(String reason) { + super(HttpStatus.NOT_ACCEPTABLE, reason); + this.supportedMediaTypes = Collections.emptyList(); + } + + /** + * Constructor for when requested Content-Type is not supported. + */ + public NotAcceptableStatusException(List supportedMediaTypes) { + super(HttpStatus.NOT_ACCEPTABLE, "Could not find acceptable representation", null); + this.supportedMediaTypes = Collections.unmodifiableList(supportedMediaTypes); + } + + + /** + * Return the list of supported content types in cases when the Accept + * header is parsed but not supported, or an empty list otherwise. + */ + public List getSupportedMediaTypes() { + return this.supportedMediaTypes; + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/UnsupportedMediaTypeStatusException.java b/spring-web-reactive/src/main/java/org/springframework/web/server/UnsupportedMediaTypeStatusException.java new file mode 100644 index 0000000000..0ac7be9def --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/UnsupportedMediaTypeStatusException.java @@ -0,0 +1,71 @@ +/* + * 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.web.server; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; + +/** + * Exception for errors that fit response status 416 (unsupported media type). + * + * @author Rossen Stoyanchev + */ +public class UnsupportedMediaTypeStatusException extends ResponseStatusException { + + private final MediaType contentType; + + private final List supportedContentTypes; + + + /** + * Constructor for when the specified Content-Type is invalid. + */ + public UnsupportedMediaTypeStatusException(String reason) { + super(HttpStatus.UNSUPPORTED_MEDIA_TYPE, reason); + this.contentType = null; + this.supportedContentTypes = Collections.emptyList(); + } + + /** + * Constructor for when the Content-Type can be parsed but is not supported. + */ + public UnsupportedMediaTypeStatusException(MediaType contentType, List supportedContentTypes) { + super(HttpStatus.UNSUPPORTED_MEDIA_TYPE, "Content type '" + contentType + "' not supported"); + this.contentType = contentType; + this.supportedContentTypes = Collections.unmodifiableList(supportedContentTypes); + } + + + /** + * Return the request Content-Type header if it was parsed successfully. + */ + public Optional getContentType() { + return Optional.ofNullable(this.contentType); + } + + /** + * Return the list of supported content types in cases when the Content-Type + * header is parsed but not supported, or an empty list otherwise. + */ + public List getSupportedContentTypes() { + return this.supportedContentTypes; + } + +} From 3849fd6b5b4dac8cd30c3061243b156d1613c150 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 19 Apr 2016 15:55:34 -0400 Subject: [PATCH 310/498] Remove DispatcherHandlerExceptionMapper No need to translate to response status codes any more and no need for an exception handling mechanism at this level. --- .../web/reactive/DispatcherHandler.java | 28 ++---- .../DispatcherHandlerExceptionMapper.java | 65 -------------- ...DispatcherHandlerExceptionMapperTests.java | 86 ------------------- 3 files changed, 6 insertions(+), 173 deletions(-) delete mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandlerExceptionMapper.java delete mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerExceptionMapperTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java index 318f52a991..bf21007c8a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java @@ -19,7 +19,6 @@ package org.springframework.web.reactive; import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.function.Function; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -31,9 +30,11 @@ import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.ServerHttpRequest; -import org.springframework.web.server.WebHandler; +import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebHandler; /** * Central dispatcher for HTTP request handlers/controllers. Dispatches to registered @@ -64,29 +65,12 @@ public class DispatcherHandler implements WebHandler, ApplicationContextAware { private List resultHandlers; - private Function errorMapper = new DispatcherHandlerExceptionMapper(); - @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { initStrategies(applicationContext); } - /** - * Configure a function to map error signals from the {@code DispatcherHandler}. - *

    By default this is set to {@link DispatcherHandlerExceptionMapper}. - * @param errorMapper the function - */ - public void setErrorMapper(Function errorMapper) { - this.errorMapper = errorMapper; - } - - /** - * Return the configured function for mapping exceptions. - */ - public Function getErrorMapper() { - return this.errorMapper; - } protected void initStrategies(ApplicationContext context) { @@ -121,8 +105,7 @@ public class DispatcherHandler implements WebHandler, ApplicationContextAware { .concatMap(mapping -> mapping.getHandler(exchange)) .next() .then(handler -> invokeHandler(exchange, handler)) - .then(result -> handleResult(exchange, result)) - .otherwise(ex -> Mono.error(this.errorMapper.apply(ex))); + .then(result -> handleResult(exchange, result)); } private Mono invokeHandler(ServerWebExchange exchange, Object handler) { @@ -153,7 +136,8 @@ public class DispatcherHandler implements WebHandler, ApplicationContextAware { private static class NotFoundHandlerMapping implements HandlerMapping { @SuppressWarnings("ThrowableInstanceNeverThrown") - private static final Exception HANDLER_NOT_FOUND_EXCEPTION = new HandlerNotFoundException(); + private static final Exception HANDLER_NOT_FOUND_EXCEPTION = + new ResponseStatusException(HttpStatus.NOT_FOUND, "No matching handler"); @Override diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandlerExceptionMapper.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandlerExceptionMapper.java deleted file mode 100644 index c1b27fa791..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandlerExceptionMapper.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2002-2015 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.web.reactive; - -import java.util.function.Function; - -import org.springframework.core.annotation.AnnotatedElementUtils; -import org.springframework.http.HttpStatus; -import org.springframework.web.HttpMediaTypeNotAcceptableException; -import org.springframework.web.server.ResponseStatusException; -import org.springframework.web.bind.annotation.ResponseStatus; - -/** - * Map "standard" framework exceptions and - * {@link ResponseStatus @ResponseStatus}-annotated exceptions to a - * {@link ResponseStatusException}. - * - * @author Rossen Stoyanchev - */ -public class DispatcherHandlerExceptionMapper implements Function { - - - @Override - public Throwable apply(Throwable ex) { - if (ex instanceof HandlerNotFoundException) { - ex = new ResponseStatusException(HttpStatus.NOT_FOUND, ex); - } - else if (ex instanceof HttpMediaTypeNotAcceptableException) { - ex = new ResponseStatusException(HttpStatus.NOT_ACCEPTABLE, ex); - } - else { - ResponseStatus status = findStatus(ex); - if (status != null) { - ex = new ResponseStatusException(status.code(), ex); - } - } - return ex; - } - - private ResponseStatus findStatus(Throwable ex) { - Class type = ex.getClass(); - ResponseStatus status = AnnotatedElementUtils.findMergedAnnotation(type, ResponseStatus.class); - if (status != null) { - return status; - } - else if (ex.getCause() != null) { - return findStatus(ex.getCause()); - } - return null; - } - -} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerExceptionMapperTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerExceptionMapperTests.java deleted file mode 100644 index 8c4ede4ab6..0000000000 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerExceptionMapperTests.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright 2002-2015 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.web.reactive; - -import java.util.Collections; - -import org.junit.Before; -import org.junit.Test; - -import org.springframework.http.HttpStatus; -import org.springframework.web.HttpMediaTypeNotAcceptableException; -import org.springframework.web.server.ResponseStatusException; -import org.springframework.web.bind.annotation.ResponseStatus; - -import static org.junit.Assert.assertEquals; - -/** - * @author Rossen Stoyanchev - */ -public class DispatcherHandlerExceptionMapperTests { - - private DispatcherHandlerExceptionMapper mapper; - - - @Before - public void setUp() throws Exception { - this.mapper = new DispatcherHandlerExceptionMapper(); - } - - - @Test - public void handlerNotFound() throws Exception { - Throwable ex = new HandlerNotFoundException(); - ex = this.mapper.apply(ex); - - assertEquals(ResponseStatusException.class, ex.getClass()); - assertEquals(HttpStatus.NOT_FOUND, ((ResponseStatusException) ex).getStatus()); - } - - - @Test - public void httpMediaTypeNotAcceptable() throws Exception { - Throwable ex = new HttpMediaTypeNotAcceptableException(Collections.emptyList()); - ex = this.mapper.apply(ex); - - assertEquals(ResponseStatusException.class, ex.getClass()); - assertEquals(HttpStatus.NOT_ACCEPTABLE, ((ResponseStatusException) ex).getStatus()); - } - - @Test - public void responseStatusAnnotation() throws Exception { - Throwable ex = new ResponseStatusAnnotatedException(); - ex = this.mapper.apply(ex); - - assertEquals(ResponseStatusException.class, ex.getClass()); - assertEquals(HttpStatus.BAD_REQUEST, ((ResponseStatusException) ex).getStatus()); - } - - @Test - public void responseStatusAnnotationOnRootCause() throws Exception { - Throwable ex = new Exception(new ResponseStatusAnnotatedException()); - ex = this.mapper.apply(ex); - - assertEquals(ResponseStatusException.class, ex.getClass()); - assertEquals(HttpStatus.BAD_REQUEST, ((ResponseStatusException) ex).getStatus()); - } - - - @ResponseStatus(code = HttpStatus.BAD_REQUEST) - private static class ResponseStatusAnnotatedException extends Exception { - } - -} From 6b7360fed1741bd9f7208f993f1df0036e2137d6 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 19 Apr 2016 15:58:24 -0400 Subject: [PATCH 311/498] Apply new ResponseStatusException hierarchy This commit replaces use of the existing ServletException-based exceptions with the new ones from ~.web.server. --- .../reactive/HandlerNotFoundException.java | 30 -------------- .../AbstractMappingContentTypeResolver.java | 12 +++--- .../accept/CompositeContentTypeResolver.java | 4 +- .../reactive/accept/ContentTypeResolver.java | 7 ++-- .../accept/HeaderContentTypeResolver.java | 9 ++--- .../accept/ParameterContentTypeResolver.java | 6 +-- .../PathExtensionContentTypeResolver.java | 14 +++---- .../AbstractMediaTypeExpression.java | 11 ++++-- .../condition/ConsumesRequestCondition.java | 6 +-- .../condition/ProducesRequestCondition.java | 8 ++-- .../RequestMappingInfoHandlerMapping.java | 32 ++++++++------- .../annotation/ResponseBodyResultHandler.java | 6 +-- .../reactive/DispatcherHandlerErrorTests.java | 16 ++++---- .../ResponseStatusExceptionHandlerTests.java | 4 +- ...positeContentTypeResolverBuilderTests.java | 6 +-- .../HeaderContentTypeResolverTests.java | 4 +- ...athExtensionContentTypeResolverTests.java} | 6 +-- ...RequestMappingInfoHandlerMappingTests.java | 39 +++++++++---------- 18 files changed, 95 insertions(+), 125 deletions(-) delete mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerNotFoundException.java rename spring-web-reactive/src/test/java/org/springframework/web/reactive/accept/{PathExtensionContentNegotiationStrategyTests.java => PathExtensionContentTypeResolverTests.java} (96%) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerNotFoundException.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerNotFoundException.java deleted file mode 100644 index f9d8f074af..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerNotFoundException.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2002-2015 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.web.reactive; - -import org.springframework.core.NestedRuntimeException; - -/** - * @author Rossen Stoyanchev - */ -public class HandlerNotFoundException extends NestedRuntimeException { - - - public HandlerNotFoundException() { - super("No handler found."); - } - -} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/AbstractMappingContentTypeResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/AbstractMappingContentTypeResolver.java index 3e89fe8a73..37f9f77d4a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/AbstractMappingContentTypeResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/AbstractMappingContentTypeResolver.java @@ -29,7 +29,7 @@ import org.springframework.http.MediaType; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; -import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.server.NotAcceptableStatusException; import org.springframework.web.server.ServerWebExchange; /** @@ -85,7 +85,7 @@ public abstract class AbstractMappingContentTypeResolver implements MappingConte @Override public List resolveMediaTypes(ServerWebExchange exchange) - throws HttpMediaTypeNotAcceptableException { + throws NotAcceptableStatusException { String key = extractKey(exchange); return resolveMediaTypes(key); @@ -95,11 +95,9 @@ public abstract class AbstractMappingContentTypeResolver implements MappingConte * An overloaded resolve method with a pre-resolved lookup key. * @param key the key for looking up media types * @return a list of resolved media types or an empty list - * @throws HttpMediaTypeNotAcceptableException + * @throws NotAcceptableStatusException */ - public List resolveMediaTypes(String key) - throws HttpMediaTypeNotAcceptableException { - + public List resolveMediaTypes(String key) throws NotAcceptableStatusException { if (StringUtils.hasText(key)) { MediaType mediaType = getMediaType(key); if (mediaType != null) { @@ -139,7 +137,7 @@ public abstract class AbstractMappingContentTypeResolver implements MappingConte * this method it will be added to the mappings. */ @SuppressWarnings("UnusedParameters") - protected MediaType handleNoMatch(String key) throws HttpMediaTypeNotAcceptableException { + protected MediaType handleNoMatch(String key) throws NotAcceptableStatusException { return null; } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/CompositeContentTypeResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/CompositeContentTypeResolver.java index 71377ab377..68df906508 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/CompositeContentTypeResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/CompositeContentTypeResolver.java @@ -23,7 +23,7 @@ import java.util.Set; import org.springframework.http.MediaType; import org.springframework.util.Assert; -import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.server.NotAcceptableStatusException; import org.springframework.web.server.ServerWebExchange; /** @@ -71,7 +71,7 @@ public class CompositeContentTypeResolver implements MappingContentTypeResolver @Override - public List resolveMediaTypes(ServerWebExchange exchange) throws HttpMediaTypeNotAcceptableException { + public List resolveMediaTypes(ServerWebExchange exchange) throws NotAcceptableStatusException { for (ContentTypeResolver resolver : this.resolvers) { List mediaTypes = resolver.resolveMediaTypes(exchange); if (mediaTypes.isEmpty() || (mediaTypes.size() == 1 && mediaTypes.contains(MediaType.ALL))) { diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/ContentTypeResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/ContentTypeResolver.java index c91c494a62..e359cdeed5 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/ContentTypeResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/ContentTypeResolver.java @@ -18,7 +18,7 @@ package org.springframework.web.reactive.accept; import java.util.List; import org.springframework.http.MediaType; -import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.server.NotAcceptableStatusException; import org.springframework.web.server.ServerWebExchange; /** @@ -34,9 +34,8 @@ public interface ContentTypeResolver { * @param exchange the current exchange * @return the requested media types or an empty list * - * @throws HttpMediaTypeNotAcceptableException if the requested media - * types cannot be parsed + * @throws NotAcceptableStatusException if the requested media types is invalid */ - List resolveMediaTypes(ServerWebExchange exchange) throws HttpMediaTypeNotAcceptableException; + List resolveMediaTypes(ServerWebExchange exchange) throws NotAcceptableStatusException; } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/HeaderContentTypeResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/HeaderContentTypeResolver.java index de6caac150..4ef573a93c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/HeaderContentTypeResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/HeaderContentTypeResolver.java @@ -19,7 +19,7 @@ import java.util.List; import org.springframework.http.InvalidMediaTypeException; import org.springframework.http.MediaType; -import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.server.NotAcceptableStatusException; import org.springframework.web.server.ServerWebExchange; /** @@ -30,9 +30,7 @@ import org.springframework.web.server.ServerWebExchange; public class HeaderContentTypeResolver implements ContentTypeResolver { @Override - public List resolveMediaTypes(ServerWebExchange exchange) - throws HttpMediaTypeNotAcceptableException { - + public List resolveMediaTypes(ServerWebExchange exchange) throws NotAcceptableStatusException { try { List mediaTypes = exchange.getRequest().getHeaders().getAccept(); MediaType.sortBySpecificityAndQuality(mediaTypes); @@ -40,8 +38,9 @@ public class HeaderContentTypeResolver implements ContentTypeResolver { } catch (InvalidMediaTypeException ex) { String value = exchange.getRequest().getHeaders().getFirst("Accept"); - throw new HttpMediaTypeNotAcceptableException( + throw new NotAcceptableStatusException( "Could not parse 'Accept' header [" + value + "]: " + ex.getMessage()); } } + } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/ParameterContentTypeResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/ParameterContentTypeResolver.java index 4b6ed63678..70641c3b45 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/ParameterContentTypeResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/ParameterContentTypeResolver.java @@ -23,7 +23,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.http.MediaType; import org.springframework.util.Assert; -import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.server.NotAcceptableStatusException; import org.springframework.web.server.ServerWebExchange; /** @@ -75,8 +75,8 @@ public class ParameterContentTypeResolver extends AbstractMappingContentTypeReso } @Override - protected MediaType handleNoMatch(String key) throws HttpMediaTypeNotAcceptableException { - throw new HttpMediaTypeNotAcceptableException(getMediaTypes()); + protected MediaType handleNoMatch(String key) throws NotAcceptableStatusException { + throw new NotAcceptableStatusException(getMediaTypes()); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/PathExtensionContentTypeResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/PathExtensionContentTypeResolver.java index f8ea0747c8..57b98d666f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/PathExtensionContentTypeResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/PathExtensionContentTypeResolver.java @@ -31,8 +31,7 @@ import org.springframework.http.MediaType; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; -import org.springframework.web.HttpMediaTypeNotAcceptableException; -import org.springframework.web.accept.PathExtensionContentNegotiationStrategy; +import org.springframework.web.server.NotAcceptableStatusException; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.util.WebUtils; @@ -49,11 +48,10 @@ import org.springframework.web.util.WebUtils; */ public class PathExtensionContentTypeResolver extends AbstractMappingContentTypeResolver { - private static final Log logger = LogFactory.getLog(PathExtensionContentNegotiationStrategy.class); + private static final Log logger = LogFactory.getLog(PathExtensionContentTypeResolver.class); - private static final boolean JAF_PRESENT = ClassUtils.isPresent( - "javax.activation.FileTypeMap", - PathExtensionContentNegotiationStrategy.class.getClassLoader()); + private static final boolean JAF_PRESENT = ClassUtils.isPresent("javax.activation.FileTypeMap", + PathExtensionContentTypeResolver.class.getClassLoader()); private boolean useJaf = true; @@ -104,7 +102,7 @@ public class PathExtensionContentTypeResolver extends AbstractMappingContentType } @Override - protected MediaType handleNoMatch(String key) throws HttpMediaTypeNotAcceptableException { + protected MediaType handleNoMatch(String key) throws NotAcceptableStatusException { if (this.useJaf && JAF_PRESENT) { MediaType mediaType = JafMediaTypeFactory.getMediaType("file." + key); if (mediaType != null && !MediaType.APPLICATION_OCTET_STREAM.equals(mediaType)) { @@ -112,7 +110,7 @@ public class PathExtensionContentTypeResolver extends AbstractMappingContentType } } if (!this.ignoreUnknownExtensions) { - throw new HttpMediaTypeNotAcceptableException(getMediaTypes()); + throw new NotAcceptableStatusException(getMediaTypes()); } return null; } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/AbstractMediaTypeExpression.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/AbstractMediaTypeExpression.java index 8ae3974257..6f9b773574 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/AbstractMediaTypeExpression.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/AbstractMediaTypeExpression.java @@ -20,9 +20,10 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.http.MediaType; -import org.springframework.web.HttpMediaTypeException; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.server.NotAcceptableStatusException; import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.UnsupportedMediaTypeStatusException; /** * Supports media type expressions as described in: @@ -72,12 +73,16 @@ abstract class AbstractMediaTypeExpression implements Comparable getAcceptedMediaTypes(ServerWebExchange exchange) - throws HttpMediaTypeNotAcceptableException { + throws NotAcceptableStatusException { List mediaTypes = this.contentTypeResolver.resolveMediaTypes(exchange); return mediaTypes.isEmpty() ? Collections.singletonList(MediaType.ALL) : mediaTypes; @@ -306,7 +306,7 @@ public final class ProducesRequestCondition extends AbstractRequestCondition acceptedMediaTypes = getAcceptedMediaTypes(exchange); for (MediaType acceptedMediaType : acceptedMediaTypes) { if (getMediaType().isCompatibleWith(acceptedMediaType)) { diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java index 7e29549416..fb99f6dec1 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java @@ -36,16 +36,16 @@ import org.springframework.http.MediaType; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.util.CollectionUtils; import org.springframework.util.MultiValueMap; -import org.springframework.web.HttpMediaTypeNotAcceptableException; -import org.springframework.web.HttpMediaTypeNotSupportedException; -import org.springframework.web.HttpRequestMethodNotSupportedException; -import org.springframework.web.bind.UnsatisfiedServletRequestParameterException; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerMapping; import org.springframework.web.reactive.result.condition.NameValueExpression; import org.springframework.web.reactive.result.condition.ParamsRequestCondition; +import org.springframework.web.server.BadRequestStatusException; +import org.springframework.web.server.MethodNotAllowedException; +import org.springframework.web.server.NotAcceptableStatusException; import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.UnsupportedMediaTypeStatusException; import org.springframework.web.util.WebUtils; /** @@ -166,10 +166,13 @@ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMe /** * Iterate all RequestMappingInfos once again, look if any match by URL at * least and raise exceptions accordingly. - * @throws HttpRequestMethodNotSupportedException if there are matches by URL - * but not by HTTP method - * @throws HttpMediaTypeNotAcceptableException if there are matches by URL - * but not by consumable/producible media types + * @throws MethodNotAllowedException for matches by URL but not by HTTP method + * @throws UnsupportedMediaTypeStatusException if there are matches by URL + * and HTTP method but not by consumable media types + * @throws NotAcceptableStatusException if there are matches by URL and HTTP + * method but not by producible media types + * @throws BadRequestStatusException if there are matches by URL and HTTP + * method but not by query parameter conditions */ @Override protected HandlerMethod handleNoMatch(Set requestMappingInfos, @@ -205,7 +208,7 @@ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMe return new HandlerMethod(handler, HTTP_OPTIONS_HANDLE_METHOD); } else if (!allowedMethods.isEmpty()) { - throw new HttpRequestMethodNotSupportedException(httpMethod.name(), allowedMethods); + throw new MethodNotAllowedException(httpMethod.name(), allowedMethods); } } @@ -230,12 +233,12 @@ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMe contentType = request.getHeaders().getContentType(); } catch (InvalidMediaTypeException ex) { - throw new HttpMediaTypeNotSupportedException(ex.getMessage()); + throw new UnsupportedMediaTypeStatusException(ex.getMessage()); } - throw new HttpMediaTypeNotSupportedException(contentType, new ArrayList<>(consumableMediaTypes)); + throw new UnsupportedMediaTypeStatusException(contentType, new ArrayList<>(consumableMediaTypes)); } else if (!producibleMediaTypes.isEmpty()) { - throw new HttpMediaTypeNotAcceptableException(new ArrayList<>(producibleMediaTypes)); + throw new NotAcceptableStatusException(new ArrayList<>(producibleMediaTypes)); } else { if (!CollectionUtils.isEmpty(paramConditions)) { @@ -243,7 +246,8 @@ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMe .collect(Collectors.toMap(Entry::getKey, entry -> entry.getValue().toArray(new String[entry.getValue().size()])) ); - throw new UnsatisfiedServletRequestParameterException(paramConditions, params); + throw new BadRequestStatusException("Unsatisfied query parameter conditions: " + + paramConditions + ", actual: " + params); } else { return null; @@ -308,7 +312,7 @@ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMe } private static Set initAllowedHttpMethods(Set declaredMethods) { - Set result = new LinkedHashSet(declaredMethods.size()); + Set result = new LinkedHashSet<>(declaredMethods.size()); if (declaredMethods.isEmpty()) { for (HttpMethod method : HttpMethod.values()) { if (!HttpMethod.TRACE.equals(method)) { diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java index e0b5c2fdb8..c69a315401 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java @@ -42,11 +42,11 @@ import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.util.Assert; import org.springframework.util.MimeType; -import org.springframework.web.HttpMediaTypeNotAcceptableException; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.HandlerResultHandler; +import org.springframework.web.server.NotAcceptableStatusException; import org.springframework.web.server.ServerWebExchange; @@ -170,7 +170,7 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered } } if (compatibleMediaTypes.isEmpty()) { - return Mono.error(new HttpMediaTypeNotAcceptableException(producibleMediaTypes)); + return Mono.error(new NotAcceptableStatusException(producibleMediaTypes)); } List mediaTypes = new ArrayList<>(compatibleMediaTypes); @@ -200,7 +200,7 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered } } - return Mono.error(new HttpMediaTypeNotAcceptableException(this.allMediaTypes)); + return Mono.error(new NotAcceptableStatusException(this.allMediaTypes)); } private List getAcceptableMediaTypes(ServerHttpRequest request) { diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java index cd5dce7650..91c91bc9aa 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java @@ -41,14 +41,14 @@ import org.springframework.http.MediaType; import org.springframework.http.server.reactive.MockServerHttpRequest; import org.springframework.http.server.reactive.MockServerHttpResponse; import org.springframework.stereotype.Controller; -import org.springframework.web.HttpMediaTypeNotAcceptableException; -import org.springframework.web.server.ResponseStatusException; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter; import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.reactive.result.method.annotation.ResponseBodyResultHandler; +import org.springframework.web.server.NotAcceptableStatusException; +import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebExceptionHandler; import org.springframework.web.server.WebFilter; @@ -60,7 +60,9 @@ import org.springframework.web.server.handler.FilteringWebHandler; import org.springframework.web.server.session.WebSessionManager; import static org.hamcrest.CoreMatchers.startsWith; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThat; import static org.mockito.Mockito.mock; /** @@ -109,8 +111,7 @@ public class DispatcherHandlerErrorTests { Throwable ex = awaitErrorSignal(publisher); assertEquals(ResponseStatusException.class, ex.getClass()); - assertNotNull(ex.getCause()); - assertEquals(HandlerNotFoundException.class, ex.getCause().getClass()); + assertEquals(HttpStatus.NOT_FOUND, ((ResponseStatusException) ex).getStatus()); } @Test @@ -166,9 +167,7 @@ public class DispatcherHandlerErrorTests { Mono publisher = this.dispatcherHandler.handle(this.exchange); Throwable ex = awaitErrorSignal(publisher); - assertEquals(ResponseStatusException.class, ex.getClass()); - assertNotNull(ex.getCause()); - assertEquals(HttpMediaTypeNotAcceptableException.class, ex.getCause().getClass()); + assertEquals(NotAcceptableStatusException.class, ex.getClass()); } @Test @@ -181,7 +180,6 @@ public class DispatcherHandlerErrorTests { ex.printStackTrace(); assertSame(EXCEPTION, ex); - } @Test diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java index c5b85760a7..f36b67f2bb 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java @@ -35,6 +35,8 @@ import static org.junit.Assert.*; import static org.mockito.Mockito.mock; /** + * Unit tests for {@link ResponseStatusExceptionHandler}. + * * @author Rossen Stoyanchev */ public class ResponseStatusExceptionHandlerTests { @@ -58,7 +60,7 @@ public class ResponseStatusExceptionHandlerTests { @Test public void handleException() throws Exception { - Throwable ex = new ResponseStatusException(HttpStatus.BAD_REQUEST); + Throwable ex = new ResponseStatusException(HttpStatus.BAD_REQUEST, ""); Mono publisher = this.handler.handle(this.exchange, ex); publisher.get(); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/accept/CompositeContentTypeResolverBuilderTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/accept/CompositeContentTypeResolverBuilderTests.java index 074dc914a7..6295b997a2 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/accept/CompositeContentTypeResolverBuilderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/accept/CompositeContentTypeResolverBuilderTests.java @@ -26,7 +26,7 @@ import org.springframework.http.MediaType; import org.springframework.http.server.reactive.MockServerHttpRequest; import org.springframework.http.server.reactive.MockServerHttpResponse; import org.springframework.http.server.reactive.ServerHttpRequest; -import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.server.NotAcceptableStatusException; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange; import org.springframework.web.server.session.WebSessionManager; @@ -102,7 +102,7 @@ public class CompositeContentTypeResolverBuilderTests { assertEquals(Collections.emptyList(), resolver.resolveMediaTypes(exchange)); } - @Test(expected = HttpMediaTypeNotAcceptableException.class) // SPR-10170 + @Test(expected = NotAcceptableStatusException.class) // SPR-10170 public void favorPathWithIgnoreUnknownPathExtensionTurnedOff() throws Exception { CompositeContentTypeResolver resolver = new CompositeContentTypeResolverBuilder() .favorPathExtension(true) @@ -129,7 +129,7 @@ public class CompositeContentTypeResolverBuilderTests { resolver.resolveMediaTypes(exchange)); } - @Test(expected = HttpMediaTypeNotAcceptableException.class) // SPR-10170 + @Test(expected = NotAcceptableStatusException.class) // SPR-10170 public void favorParameterWithUnknownMediaType() throws Exception { CompositeContentTypeResolver resolver = new CompositeContentTypeResolverBuilder() .favorParameter(true) diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/accept/HeaderContentTypeResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/accept/HeaderContentTypeResolverTests.java index 9b2288b2c4..5280ed0393 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/accept/HeaderContentTypeResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/accept/HeaderContentTypeResolverTests.java @@ -27,7 +27,7 @@ import org.springframework.http.MediaType; import org.springframework.http.server.reactive.MockServerHttpRequest; import org.springframework.http.server.reactive.MockServerHttpResponse; import org.springframework.http.server.reactive.ServerHttpRequest; -import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.server.NotAcceptableStatusException; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange; import org.springframework.web.server.session.WebSessionManager; @@ -63,7 +63,7 @@ public class HeaderContentTypeResolverTests { assertEquals("text/plain;q=0.5", mediaTypes.get(3).toString()); } - @Test(expected=HttpMediaTypeNotAcceptableException.class) + @Test(expected=NotAcceptableStatusException.class) public void resolveMediaTypesParseError() throws Exception { ServerWebExchange exchange = createExchange("textplain; q=0.5"); this.resolver.resolveMediaTypes(exchange); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/accept/PathExtensionContentNegotiationStrategyTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/accept/PathExtensionContentTypeResolverTests.java similarity index 96% rename from spring-web-reactive/src/test/java/org/springframework/web/reactive/accept/PathExtensionContentNegotiationStrategyTests.java rename to spring-web-reactive/src/test/java/org/springframework/web/reactive/accept/PathExtensionContentTypeResolverTests.java index 4399f7c56e..db71fcf1c4 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/accept/PathExtensionContentNegotiationStrategyTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/accept/PathExtensionContentTypeResolverTests.java @@ -28,7 +28,7 @@ import org.springframework.http.MediaType; import org.springframework.http.server.reactive.MockServerHttpRequest; import org.springframework.http.server.reactive.MockServerHttpResponse; import org.springframework.http.server.reactive.ServerHttpRequest; -import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.server.NotAcceptableStatusException; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange; import org.springframework.web.server.session.WebSessionManager; @@ -41,7 +41,7 @@ import static org.mockito.Mockito.mock; * * @author Rossen Stoyanchev */ -public class PathExtensionContentNegotiationStrategyTests { +public class PathExtensionContentTypeResolverTests { @Test public void resolveMediaTypesFromMapping() throws Exception { @@ -101,7 +101,7 @@ public class PathExtensionContentNegotiationStrategyTests { assertEquals(Collections.emptyList(), mediaTypes); } - @Test(expected = HttpMediaTypeNotAcceptableException.class) + @Test(expected = NotAcceptableStatusException.class) public void resolveMediaTypesDoNotIgnoreUnknownExtension() throws Exception { ServerWebExchange exchange = createExchange("test.xyz"); PathExtensionContentTypeResolver resolver = new PathExtensionContentTypeResolver(); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java index 50ca621bb4..7239f8ed8b 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java @@ -22,12 +22,12 @@ import java.net.URISyntaxException; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; -import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.Consumer; +import org.hamcrest.Matchers; import org.junit.Before; import org.junit.Test; import reactor.core.publisher.Mono; @@ -44,10 +44,6 @@ import org.springframework.stereotype.Controller; import org.springframework.ui.ExtendedModelMap; import org.springframework.ui.ModelMap; import org.springframework.util.MultiValueMap; -import org.springframework.web.HttpMediaTypeNotAcceptableException; -import org.springframework.web.HttpMediaTypeNotSupportedException; -import org.springframework.web.HttpRequestMethodNotSupportedException; -import org.springframework.web.bind.UnsatisfiedServletRequestParameterException; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @@ -55,17 +51,19 @@ import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerMapping; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.result.method.RequestMappingInfo.BuilderConfiguration; +import org.springframework.web.server.BadRequestStatusException; +import org.springframework.web.server.MethodNotAllowedException; +import org.springframework.web.server.NotAcceptableStatusException; import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.UnsupportedMediaTypeStatusException; import org.springframework.web.server.adapter.DefaultServerWebExchange; import org.springframework.web.server.session.WebSessionManager; import org.springframework.web.util.HttpRequestPathHelper; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.junit.Assert.assertArrayEquals; +import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; @@ -151,8 +149,8 @@ public class RequestMappingInfoHandlerMappingTests { public void getHandlerRequestMethodNotAllowed() throws Exception { ServerWebExchange exchange = createExchange(HttpMethod.POST, "/bar"); Mono mono = this.handlerMapping.getHandler(exchange); - assertError(mono, HttpRequestMethodNotSupportedException.class, - ex -> assertArrayEquals(new String[]{"GET", "HEAD"}, ex.getSupportedMethods())); + assertError(mono, MethodNotAllowedException.class, + ex -> assertEquals(new HashSet<>(Arrays.asList("GET", "HEAD")), ex.getSupportedMethods())); } // SPR-9603 @@ -166,7 +164,7 @@ public class RequestMappingInfoHandlerMappingTests { TestSubscriber subscriber = new TestSubscriber<>(); mono.subscribeWith(subscriber); - subscriber.assertError(HttpMediaTypeNotAcceptableException.class); + subscriber.assertError(NotAcceptableStatusException.class); } // SPR-8462 @@ -191,8 +189,10 @@ public class RequestMappingInfoHandlerMappingTests { ServerWebExchange exchange = createExchange(HttpMethod.PUT, "/person/1"); exchange.getRequest().getHeaders().add("Content-Type", "bogus"); Mono mono = this.handlerMapping.getHandler(exchange); - assertError(mono, HttpMediaTypeNotSupportedException.class, - ex -> assertEquals("Invalid mime type \"bogus\": does not contain '/'", ex.getMessage())); + assertError(mono, UnsupportedMediaTypeStatusException.class, + ex -> assertEquals("Request failure [status: 415, " + + "reason: \"Invalid mime type \"bogus\": does not contain '/'\"]", + ex.getMessage())); } // SPR-8462 @@ -210,11 +210,8 @@ public class RequestMappingInfoHandlerMappingTests { public void getHandlerUnsatisfiedServletRequestParameterException() throws Exception { ServerWebExchange exchange = createExchange(HttpMethod.GET, "/params"); Mono mono = this.handlerMapping.getHandler(exchange); - assertError(mono, UnsatisfiedServletRequestParameterException.class, ex -> { - List groups = ex.getParamConditionGroups(); - assertEquals(2, groups.size()); - assertThat(Arrays.asList("foo=bar", "bar=baz"), - containsInAnyOrder(groups.get(0)[0], groups.get(1)[0])); + assertError(mono, BadRequestStatusException.class, ex -> { + assertThat(ex.getReason(), Matchers.startsWith("Unsatisfied query parameter conditions:")); }); } @@ -380,10 +377,10 @@ public class RequestMappingInfoHandlerMappingTests { exchange.getRequest().getHeaders().setContentType(MediaType.APPLICATION_JSON); Mono mono = this.handlerMapping.getHandler(exchange); - assertError(mono, HttpMediaTypeNotSupportedException.class, ex -> + assertError(mono, UnsupportedMediaTypeStatusException.class, ex -> assertEquals("Invalid supported consumable media types", Collections.singletonList(new MediaType("application", "xml")), - ex.getSupportedMediaTypes())); + ex.getSupportedContentTypes())); } private void testHttpOptions(String requestURI, String allowHeader) throws Exception { @@ -407,7 +404,7 @@ public class RequestMappingInfoHandlerMappingTests { exchange.getRequest().getHeaders().setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); Mono mono = this.handlerMapping.getHandler(exchange); - assertError(mono, HttpMediaTypeNotAcceptableException.class, ex -> + assertError(mono, NotAcceptableStatusException.class, ex -> assertEquals("Invalid supported producible media types", Collections.singletonList(new MediaType("application", "xml")), ex.getSupportedMediaTypes())); From 9c40b65b7085abb2551d9739c0c7a442856aeed4 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 19 Apr 2016 16:28:32 -0400 Subject: [PATCH 312/498] Improve error message for unsatisfied param conditions --- .../RequestMappingInfoHandlerMapping.java | 27 +++++++------------ ...RequestMappingInfoHandlerMappingTests.java | 5 ++-- 2 files changed, 11 insertions(+), 21 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java index fb99f6dec1..14f9232ca1 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java @@ -39,7 +39,6 @@ import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerMapping; -import org.springframework.web.reactive.result.condition.NameValueExpression; import org.springframework.web.reactive.result.condition.ParamsRequestCondition; import org.springframework.web.server.BadRequestStatusException; import org.springframework.web.server.MethodNotAllowedException; @@ -214,7 +213,7 @@ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMe Set consumableMediaTypes; Set producibleMediaTypes; - List paramConditions; + List> paramConditions; if (patternAndMethodMatches.isEmpty()) { consumableMediaTypes = getConsumableMediaTypes(exchange, patternMatches); @@ -247,7 +246,7 @@ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMe entry -> entry.getValue().toArray(new String[entry.getValue().size()])) ); throw new BadRequestStatusException("Unsatisfied query parameter conditions: " + - paramConditions + ", actual: " + params); + paramConditions + ", actual parameters: " + params); } else { return null; @@ -279,23 +278,15 @@ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMe return result; } - private List getRequestParams(ServerWebExchange exchange, + private List> getRequestParams(ServerWebExchange exchange, Set partialMatches) { - List result = new ArrayList<>(); - for (RequestMappingInfo partialMatch : partialMatches) { - ParamsRequestCondition condition = partialMatch.getParamsCondition(); - Set> expressions = condition.getExpressions(); - if (!CollectionUtils.isEmpty(expressions) && condition.getMatchingCondition(exchange) == null) { - int i = 0; - String[] array = new String[expressions.size()]; - for (NameValueExpression expression : expressions) { - array[i++] = expression.toString(); - } - result.add(array); - } - } - return result; + return partialMatches.stream() + .map(RequestMappingInfo::getParamsCondition) + .filter(condition -> condition.getMatchingCondition(exchange) == null) + .map(ParamsRequestCondition::getExpressions) + .map(expressions -> expressions.stream().map(Object::toString).collect(Collectors.toList())) + .collect(Collectors.toList()); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java index 7239f8ed8b..174e99458c 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java @@ -27,7 +27,6 @@ import java.util.Optional; import java.util.Set; import java.util.function.Consumer; -import org.hamcrest.Matchers; import org.junit.Before; import org.junit.Test; import reactor.core.publisher.Mono; @@ -60,7 +59,6 @@ import org.springframework.web.server.adapter.DefaultServerWebExchange; import org.springframework.web.server.session.WebSessionManager; import org.springframework.web.util.HttpRequestPathHelper; -import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; @@ -211,7 +209,8 @@ public class RequestMappingInfoHandlerMappingTests { ServerWebExchange exchange = createExchange(HttpMethod.GET, "/params"); Mono mono = this.handlerMapping.getHandler(exchange); assertError(mono, BadRequestStatusException.class, ex -> { - assertThat(ex.getReason(), Matchers.startsWith("Unsatisfied query parameter conditions:")); + assertEquals(ex.getReason(), "Unsatisfied query parameter conditions: " + + "[[bar=baz], [foo=bar]], actual parameters: {}"); }); } From 8ca5b830b1699608e8afefef16b1668359fdd2fe Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 19 Apr 2016 16:41:32 -0400 Subject: [PATCH 313/498] Remove dependency on WebUtils --- .../RequestMappingInfoHandlerMapping.java | 37 ++++++++++++++++++- .../RequestMappingHandlerMapping.java | 5 +-- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java index 14f9232ca1..640fed7eda 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java @@ -27,6 +27,7 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import java.util.StringTokenizer; import java.util.stream.Collectors; import org.springframework.http.HttpHeaders; @@ -35,7 +36,9 @@ import org.springframework.http.InvalidMediaTypeException; import org.springframework.http.MediaType; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.util.CollectionUtils; +import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerMapping; @@ -45,7 +48,6 @@ import org.springframework.web.server.MethodNotAllowedException; import org.springframework.web.server.NotAcceptableStatusException; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.UnsupportedMediaTypeStatusException; -import org.springframework.web.util.WebUtils; /** * Abstract base class for classes for which {@link RequestMappingInfo} defines @@ -156,12 +158,43 @@ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMe uriVariables.put(uriVar.getKey(), uriVarValue.substring(0, semicolonIndex)); } - MultiValueMap vars = WebUtils.parseMatrixVariables(matrixVariables); + MultiValueMap vars = parseMatrixVariables(matrixVariables); result.put(uriVar.getKey(), getPathHelper().decodeMatrixVariables(exchange, vars)); } return result; } + /** + * Parse the given string with matrix variables. An example string would look + * like this {@code "q1=a;q1=b;q2=a,b,c"}. The resulting map would contain + * keys {@code "q1"} and {@code "q2"} with values {@code ["a","b"]} and + * {@code ["a","b","c"]} respectively. + * @param matrixVariables the unparsed matrix variables string + * @return a map with matrix variable names and values (never {@code null}) + */ + private static MultiValueMap parseMatrixVariables(String matrixVariables) { + MultiValueMap result = new LinkedMultiValueMap<>(); + if (!StringUtils.hasText(matrixVariables)) { + return result; + } + StringTokenizer pairs = new StringTokenizer(matrixVariables, ";"); + while (pairs.hasMoreTokens()) { + String pair = pairs.nextToken(); + int index = pair.indexOf('='); + if (index != -1) { + String name = pair.substring(0, index); + String rawValue = pair.substring(index + 1); + for (String value : StringUtils.commaDelimitedListToStringArray(rawValue)) { + result.add(name, value); + } + } + else { + result.add(pair, ""); + } + } + return result; + } + /** * Iterate all RequestMappingInfos once again, look if any match by URL at * least and raise exceptions accordingly. diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java index a5f23e986b..c27350e707 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java @@ -25,12 +25,9 @@ import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.stereotype.Controller; import org.springframework.util.Assert; import org.springframework.util.StringValueResolver; -import org.springframework.web.accept.ContentNegotiationManager; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.reactive.accept.CompositeContentTypeResolver; import org.springframework.web.reactive.accept.CompositeContentTypeResolverBuilder; import org.springframework.web.reactive.accept.ContentTypeResolver; -import org.springframework.web.reactive.accept.HeaderContentTypeResolver; import org.springframework.web.reactive.result.condition.RequestCondition; import org.springframework.web.reactive.result.method.RequestMappingInfo; import org.springframework.web.reactive.result.method.RequestMappingInfoHandlerMapping; @@ -93,7 +90,7 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi } /** - * Set the {@link ContentNegotiationManager} to use to determine requested media types. + * Set the {@link ContentTypeResolver} to use to determine requested media types. * If not set, the default constructor is used. */ public void setContentTypeResolver(ContentTypeResolver contentTypeResolver) { From d041b73c106913ad9ff012f4d672bc2eafdc3a91 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 19 Apr 2016 16:43:50 -0400 Subject: [PATCH 314/498] Polish --- .../server/UnsupportedMediaTypeStatusException.java | 12 ++++++------ .../RequestMappingInfoHandlerMappingTests.java | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/UnsupportedMediaTypeStatusException.java b/spring-web-reactive/src/main/java/org/springframework/web/server/UnsupportedMediaTypeStatusException.java index 0ac7be9def..456913655c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/UnsupportedMediaTypeStatusException.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/UnsupportedMediaTypeStatusException.java @@ -31,7 +31,7 @@ public class UnsupportedMediaTypeStatusException extends ResponseStatusException private final MediaType contentType; - private final List supportedContentTypes; + private final List supportedMediaTypes; /** @@ -40,16 +40,16 @@ public class UnsupportedMediaTypeStatusException extends ResponseStatusException public UnsupportedMediaTypeStatusException(String reason) { super(HttpStatus.UNSUPPORTED_MEDIA_TYPE, reason); this.contentType = null; - this.supportedContentTypes = Collections.emptyList(); + this.supportedMediaTypes = Collections.emptyList(); } /** * Constructor for when the Content-Type can be parsed but is not supported. */ - public UnsupportedMediaTypeStatusException(MediaType contentType, List supportedContentTypes) { + public UnsupportedMediaTypeStatusException(MediaType contentType, List supportedMediaTypes) { super(HttpStatus.UNSUPPORTED_MEDIA_TYPE, "Content type '" + contentType + "' not supported"); this.contentType = contentType; - this.supportedContentTypes = Collections.unmodifiableList(supportedContentTypes); + this.supportedMediaTypes = Collections.unmodifiableList(supportedMediaTypes); } @@ -64,8 +64,8 @@ public class UnsupportedMediaTypeStatusException extends ResponseStatusException * Return the list of supported content types in cases when the Content-Type * header is parsed but not supported, or an empty list otherwise. */ - public List getSupportedContentTypes() { - return this.supportedContentTypes; + public List getSupportedMediaTypes() { + return this.supportedMediaTypes; } } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java index 174e99458c..2c26457328 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java @@ -379,7 +379,7 @@ public class RequestMappingInfoHandlerMappingTests { assertError(mono, UnsupportedMediaTypeStatusException.class, ex -> assertEquals("Invalid supported consumable media types", Collections.singletonList(new MediaType("application", "xml")), - ex.getSupportedContentTypes())); + ex.getSupportedMediaTypes())); } private void testHttpOptions(String requestURI, String allowHeader) throws Exception { From a173c78d989d1c70fd63625fb678e16a18312b88 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 19 Apr 2016 17:34:08 -0400 Subject: [PATCH 315/498] Add getAttribute to WebSession and ServerWebExchange --- .../springframework/web/server/ServerWebExchange.java | 9 +++++++++ .../java/org/springframework/web/server/WebSession.java | 9 +++++++++ .../web/server/adapter/DefaultServerWebExchange.java | 6 ++++++ .../web/server/session/DefaultWebSession.java | 6 ++++++ 4 files changed, 30 insertions(+) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/ServerWebExchange.java b/spring-web-reactive/src/main/java/org/springframework/web/server/ServerWebExchange.java index 144cc81953..a2b3857c1d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/ServerWebExchange.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/ServerWebExchange.java @@ -16,6 +16,7 @@ package org.springframework.web.server; import java.util.Map; +import java.util.Optional; import reactor.core.publisher.Mono; @@ -46,6 +47,14 @@ public interface ServerWebExchange { */ Map getAttributes(); + /** + * Return the request attribute value if present. + * @param name the attribute name + * @param the attribute type + * @return the attribute value + */ + Optional getAttribute(String name); + /** * Return the web session for the current request. Always guaranteed to * return an instance either matching to the session id requested by the diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/WebSession.java b/spring-web-reactive/src/main/java/org/springframework/web/server/WebSession.java index ca6554cbcf..136daccc15 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/WebSession.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/WebSession.java @@ -18,6 +18,7 @@ package org.springframework.web.server; import java.time.Duration; import java.time.Instant; import java.util.Map; +import java.util.Optional; import reactor.core.publisher.Mono; @@ -44,6 +45,14 @@ public interface WebSession { */ Map getAttributes(); + /** + * Return the attribute value if present. + * @param name the attribute name + * @param the attribute type + * @return the attribute value + */ + Optional getAttribute(String name); + /** * Force the creation of a session causing the session id to be sent when * {@link #save()} is called. diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java b/spring-web-reactive/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java index ee6decb2eb..8e9807491c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java @@ -16,6 +16,7 @@ package org.springframework.web.server.adapter; import java.util.Map; +import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import reactor.core.publisher.EmitterProcessor; @@ -78,6 +79,11 @@ public class DefaultServerWebExchange implements ServerWebExchange { return this.attributes; } + @Override @SuppressWarnings("unchecked") + public Optional getAttribute(String name) { + return Optional.ofNullable((T) this.attributes.get(name)); + } + @Override public Mono getSession() { if (this.sessionMono == null) { diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/session/DefaultWebSession.java b/spring-web-reactive/src/main/java/org/springframework/web/server/session/DefaultWebSession.java index 0301563994..146c4e0a8e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/session/DefaultWebSession.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/session/DefaultWebSession.java @@ -20,6 +20,7 @@ import java.time.Clock; import java.time.Duration; import java.time.Instant; import java.util.Map; +import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; @@ -101,6 +102,11 @@ public class DefaultWebSession implements ConfigurableWebSession, Serializable { return this.attributes; } + @Override @SuppressWarnings("unchecked") + public Optional getAttribute(String name) { + return Optional.ofNullable((T) this.attributes.get(name)); + } + @Override public Instant getCreationTime() { return this.creationTime; From d626b5e833ecd76f55e9118060a4e7d523b11971 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Wed, 20 Apr 2016 13:30:40 +0200 Subject: [PATCH 316/498] Various DataBuffer improvements This commit introduces two DataBuffer improvements: - The capability to read a Flux from an input stream or channel. - The capability to limit a Publisher to publish up until a given maximum byte count. --- .../core/io/buffer/DefaultDataBuffer.java | 6 +- .../io/buffer/support/DataBufferUtils.java | 226 ++++++++++++++++-- .../buffer/support/DataBufferUtilsTests.java | 123 ++++++++++ .../buffer/support/DataBufferUtilsTests.txt | 4 + 4 files changed, 334 insertions(+), 25 deletions(-) create mode 100644 spring-web-reactive/src/test/java/org/springframework/core/io/buffer/support/DataBufferUtilsTests.java create mode 100644 spring-web-reactive/src/test/resources/org/springframework/core/io/buffer/support/DataBufferUtilsTests.txt diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java index 0dca055bec..823f644eec 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java @@ -179,7 +179,7 @@ public class DefaultDataBuffer implements DataBuffer { } /** - * Internal write method that keeps track of the {@link #writePosition} befor eand + * Internal write method that keeps track of the {@link #writePosition} before and * after applying the given function on {@link #byteBuffer}. */ private T writeInternal(Function function) { @@ -239,7 +239,9 @@ public class DefaultDataBuffer implements DataBuffer { } else if (obj instanceof DefaultDataBuffer) { DefaultDataBuffer other = (DefaultDataBuffer) obj; - return this.byteBuffer.equals(other.byteBuffer); + return this.readPosition == other.readPosition && + this.writePosition == other.writePosition && + this.byteBuffer.equals(other.byteBuffer); } return false; } diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java index 745b42d42e..16fd7e5b8e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java @@ -16,47 +16,44 @@ package org.springframework.core.io.buffer.support; +import java.io.Closeable; +import java.io.IOException; import java.io.InputStream; import java.io.SequenceInputStream; +import java.nio.ByteBuffer; +import java.nio.channels.ReadableByteChannel; import java.util.Enumeration; import java.util.Iterator; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Consumer; import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; import reactor.core.publisher.Flux; +import reactor.core.subscriber.SubscriberWithContext; import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferAllocator; import org.springframework.util.Assert; -/** +/**i * Utility class for working with {@link DataBuffer}s. * * @author Arjen Poutsma */ public abstract class DataBufferUtils { - /** - * Returns the given {@link DataBuffer} as a {@link Flux} of bytes. - * @param buffer the buffer to return the bytes of - * @return the bytes as a flux - */ - public static Flux toPublisher(DataBuffer buffer) { - Assert.notNull(buffer, "'buffer' must not be null"); - - byte[] bytes = new byte[buffer.readableByteCount()]; - buffer.read(bytes); - - Byte[] bytesObjects = box(bytes); - - return Flux.fromArray(bytesObjects); - } - - private static Byte[] box(byte[] bytes) { - Byte[] bytesObjects = new Byte[bytes.length]; - for (int i = 0; i < bytes.length; i++) { - bytesObjects[i] = bytes[i]; + private static final Consumer CLOSE_CONSUMER = closeable -> { + try { + if (closeable != null) { + closeable.close(); + } } - return bytesObjects; - } + catch (IOException ignored) { + } + }; + /** * Returns the given data buffer publisher as a blocking input stream, streaming over @@ -75,6 +72,103 @@ public abstract class DataBufferUtils { return new SequenceInputStream(enumeration); } + /** + * Reads the given {@code ReadableByteChannel} into a {@code Flux} of + * {@code DataBuffer}s. Closes the channel when the flux is terminated. + * @param channel the channel to read from + * @param allocator the allocator to create data buffers with + * @param bufferSize the maximum size of the data buffers + * @return a flux of data buffers read from the given channel + */ + public static Flux read(ReadableByteChannel channel, + DataBufferAllocator allocator, int bufferSize) { + Assert.notNull(channel, "'channel' must not be null"); + Assert.notNull(allocator, "'allocator' must not be null"); + + return Flux.create(new ReadableByteChannelConsumer(allocator, bufferSize), + subscriber -> channel, closeConsumer()); + } + + /** + * Reads the given {@code InputStream} into a {@code Flux} of + * {@code DataBuffer}s. Closes the stream when the flux inputStream terminated. + * @param inputStream the input stream to read from + * @param allocator the allocator to create data buffers with + * @param bufferSize the maximum size of the data buffers + * @return a flux of data buffers read from the given channel + */ + public static Flux read(InputStream inputStream, + DataBufferAllocator allocator, int bufferSize) { + Assert.notNull(inputStream, "'inputStream' must not be null"); + Assert.notNull(allocator, "'allocator' must not be null"); + + return Flux.create(new InputStreamConsumer(allocator, bufferSize), + subscriber -> inputStream, closeConsumer()); + } + + @SuppressWarnings("unchecked") + private static Consumer closeConsumer() { + return (Consumer) CLOSE_CONSUMER; + } + + /** + * Relays buffers from the given {@link Publisher} until the total + * {@linkplain DataBuffer#readableByteCount() byte count} reaches the given maximum + * byte count, or until the publisher is complete. + * @param publisher the publisher to filter + * @param maxByteCount the maximum byte count + * @return a flux whose maximum byte count is {@code maxByteCount} + */ + public static Flux takeUntilByteCount(Publisher publisher, + long maxByteCount) { + Assert.notNull(publisher, "'publisher' must not be null"); + Assert.isTrue(maxByteCount >= 0, "'maxByteCount' must be a positive number"); + + return Flux.from(publisher).lift(subscriber -> new Subscriber() { + + private Subscription subscription; + + private final AtomicLong byteCount = new AtomicLong(); + + @Override + public void onSubscribe(Subscription s) { + this.subscription = s; + subscriber.onSubscribe(s); + } + + @Override + public void onNext(DataBuffer dataBuffer) { + long currentCount = + this.byteCount.addAndGet(dataBuffer.readableByteCount()); + if (currentCount > maxByteCount) { + int size = (int) (currentCount - maxByteCount + 1); + ByteBuffer byteBuffer = + (ByteBuffer) dataBuffer.asByteBuffer().limit(size); + DataBuffer partialBuffer = + dataBuffer.allocator().allocateBuffer(size); + partialBuffer.write(byteBuffer); + + subscriber.onNext(partialBuffer); + subscriber.onComplete(); + this.subscription.cancel(); + } + else { + subscriber.onNext(dataBuffer); + } + } + + @Override + public void onError(Throwable t) { + subscriber.onError(t); + } + + @Override + public void onComplete() { + subscriber.onComplete(); + } + }); + } + /** * Enumeration wrapping an Iterator. */ @@ -98,4 +192,90 @@ public abstract class DataBufferUtils { } } + private static class ReadableByteChannelConsumer + implements Consumer> { + + private final DataBufferAllocator allocator; + + private final int chunkSize; + + public ReadableByteChannelConsumer(DataBufferAllocator allocator, int chunkSize) { + this.allocator = allocator; + this.chunkSize = chunkSize; + } + + @Override + public void accept(SubscriberWithContext sub) { + try { + ByteBuffer byteBuffer = ByteBuffer.allocate(chunkSize); + int read; + ReadableByteChannel channel = sub.context(); + if ((read = channel.read(byteBuffer)) > 0) { + byteBuffer.flip(); + boolean release = true; + DataBuffer dataBuffer = this.allocator.allocateBuffer(read); + try { + dataBuffer.write(byteBuffer); + release = false; + sub.onNext(dataBuffer); + } + finally { + if (release) { + // TODO: release buffer when we have PooledDataBuffer + } + } + } + else { + sub.onComplete(); + } + } + catch (IOException ex) { + sub.onError(ex); + } + } + } + + private static class InputStreamConsumer + implements Consumer> { + + private final DataBufferAllocator allocator; + + private final int chunkSize; + + public InputStreamConsumer(DataBufferAllocator allocator, int chunkSize) { + this.allocator = allocator; + this.chunkSize = chunkSize; + } + + @Override + public void accept(SubscriberWithContext sub) { + try { + byte[] bytes = new byte[chunkSize]; + int read; + InputStream is = sub.context(); + if ((read = is.read(bytes)) > 0) { + boolean release = true; + DataBuffer dataBuffer = this.allocator.allocateBuffer(read); + try { + dataBuffer.write(bytes, 0, read); + release = false; + sub.onNext(dataBuffer); + } + finally { + if (release) { + // TODO: release buffer when we have PooledDataBuffer + } + } + } + else { + sub.onComplete(); + } + } + catch (IOException ex) { + sub.onError(ex); + } + + } + } + } diff --git a/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/support/DataBufferUtilsTests.java b/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/support/DataBufferUtilsTests.java new file mode 100644 index 0000000000..052c6a2a00 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/support/DataBufferUtilsTests.java @@ -0,0 +1,123 @@ +/* + * 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.io.buffer.support; + +import java.io.InputStream; +import java.net.URI; +import java.nio.channels.FileChannel; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; + +import org.junit.Test; +import reactor.core.publisher.Flux; +import reactor.core.test.TestSubscriber; + +import org.springframework.core.codec.support.AbstractAllocatingTestCase; +import org.springframework.core.io.buffer.DataBuffer; + +import static org.junit.Assert.assertFalse; + +/** + * @author Arjen Poutsma + */ +public class DataBufferUtilsTests extends AbstractAllocatingTestCase { + + @Test + public void readChannel() throws Exception { + URI uri = DataBufferUtilsTests.class.getResource("DataBufferUtilsTests.txt") + .toURI(); + FileChannel channel = FileChannel.open(Paths.get(uri), StandardOpenOption.READ); + + Flux flux = DataBufferUtils.read(channel, allocator, 4); + + TestSubscriber testSubscriber = new TestSubscriber<>(); + testSubscriber.bindTo(flux). + assertNoError(). + assertComplete(). + assertValues(stringBuffer("foo\n"), stringBuffer("bar\n"), + stringBuffer("baz\n"), stringBuffer("qux\n")); + + assertFalse(channel.isOpen()); + } + + @Test + public void readUnalignedChannel() throws Exception { + URI uri = DataBufferUtilsTests.class.getResource("DataBufferUtilsTests.txt") + .toURI(); + FileChannel channel = FileChannel.open(Paths.get(uri), StandardOpenOption.READ); + + Flux flux = DataBufferUtils.read(channel, allocator, 3); + + TestSubscriber testSubscriber = new TestSubscriber<>(); + testSubscriber.bindTo(flux). + assertNoError(). + assertComplete(). + assertValues(stringBuffer("foo"), stringBuffer("\nba"), + stringBuffer("r\nb"), stringBuffer("az\n"), stringBuffer("qux"), + stringBuffer("\n")); + + assertFalse(channel.isOpen()); + } + + @Test + public void readInputStream() { + InputStream is = DataBufferUtilsTests.class + .getResourceAsStream("DataBufferUtilsTests.txt"); + + Flux flux = DataBufferUtils.read(is, allocator, 4); + + TestSubscriber testSubscriber = new TestSubscriber<>(); + testSubscriber.bindTo(flux). + assertNoError(). + assertComplete(). + assertValues(stringBuffer("foo\n"), stringBuffer("bar\n"), + stringBuffer("baz\n"), stringBuffer("qux\n")); + } + + @Test + public void readUnalignedInputStream() throws Exception { + InputStream is = DataBufferUtilsTests.class + .getResourceAsStream("DataBufferUtilsTests.txt"); + + Flux flux = DataBufferUtils.read(is, allocator, 3); + + TestSubscriber testSubscriber = new TestSubscriber<>(); + testSubscriber.bindTo(flux). + assertNoError(). + assertComplete(). + assertValues(stringBuffer("foo"), stringBuffer("\nba"), + stringBuffer("r\nb"), stringBuffer("az\n"), stringBuffer("qux"), + stringBuffer("\n")); + } + + + @Test + public void takeUntilByteCount() { + Flux flux = + Flux.just(stringBuffer("foo"), stringBuffer("bar"), stringBuffer("baz")); + + Flux result = DataBufferUtils.takeUntilByteCount(flux, 5L); + + TestSubscriber testSubscriber = new TestSubscriber<>(); + testSubscriber.bindTo(result). + assertNoError(). + assertComplete(). + assertValues(stringBuffer("foo"), stringBuffer("ba")); + } + + +} \ No newline at end of file diff --git a/spring-web-reactive/src/test/resources/org/springframework/core/io/buffer/support/DataBufferUtilsTests.txt b/spring-web-reactive/src/test/resources/org/springframework/core/io/buffer/support/DataBufferUtilsTests.txt new file mode 100644 index 0000000000..ab9b661144 --- /dev/null +++ b/spring-web-reactive/src/test/resources/org/springframework/core/io/buffer/support/DataBufferUtilsTests.txt @@ -0,0 +1,4 @@ +foo +bar +baz +qux From 7b7f833004581065be475138b37ebe843361be07 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Wed, 20 Apr 2016 13:34:30 +0200 Subject: [PATCH 317/498] Various MimeType and MediaType improvements --- .../http/support/MediaTypeUtils.java | 47 ++++++++++ .../springframework/util/MimeTypeUtils2.java | 87 +++++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 spring-web-reactive/src/main/java/org/springframework/http/support/MediaTypeUtils.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/util/MimeTypeUtils2.java diff --git a/spring-web-reactive/src/main/java/org/springframework/http/support/MediaTypeUtils.java b/spring-web-reactive/src/main/java/org/springframework/http/support/MediaTypeUtils.java new file mode 100644 index 0000000000..68d1612e8e --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/http/support/MediaTypeUtils.java @@ -0,0 +1,47 @@ +/* + * 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.http.support; + +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.http.MediaType; +import org.springframework.util.MimeType; + +/** + * @author Arjen Poutsma + */ +public abstract class MediaTypeUtils { + + /** + * TODO: move to MediaType static method + */ + public static List toMediaTypes(List mimeTypes) { + return mimeTypes.stream().map(MediaTypeUtils::toMediaType) + .collect(Collectors.toList()); + } + + /** + * TODO: move to MediaType constructor + */ + public static MediaType toMediaType(MimeType mimeType) { + return new MediaType(mimeType.getType(), mimeType.getSubtype(), + mimeType.getParameters()); + } + + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/util/MimeTypeUtils2.java b/spring-web-reactive/src/main/java/org/springframework/util/MimeTypeUtils2.java new file mode 100644 index 0000000000..6ef2f9fb6c --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/util/MimeTypeUtils2.java @@ -0,0 +1,87 @@ +/* + * 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.util; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Optional; +import javax.activation.FileTypeMap; +import javax.activation.MimetypesFileTypeMap; + +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; + +/** + * TODO: merge into {@link MimeTypeUtils}, and use wherever we still have a runtime check + * to see if JAF is available (i.e. jafPresent). Since JAF has been included in the JDK + * since 1.6, we don't + * need that check anymore. (i.e. {@link org.springframework.http.converter.ResourceHttpMessageConverter} + * @author Arjen Poutsma + */ +public abstract class MimeTypeUtils2 extends MimeTypeUtils { + + private static final FileTypeMap fileTypeMap; + + static { + fileTypeMap = loadFileTypeMapFromContextSupportModule(); + } + + private static FileTypeMap loadFileTypeMapFromContextSupportModule() { + // See if we can find the extended mime.types from the context-support module... + Resource mappingLocation = + new ClassPathResource("org/springframework/mail/javamail/mime.types"); + if (mappingLocation.exists()) { + InputStream inputStream = null; + try { + inputStream = mappingLocation.getInputStream(); + return new MimetypesFileTypeMap(inputStream); + } + catch (IOException ex) { + // ignore + } + finally { + if (inputStream != null) { + try { + inputStream.close(); + } + catch (IOException ex) { + // ignore + } + } + } + } + return FileTypeMap.getDefaultFileTypeMap(); + } + + /** + * Returns the {@code MimeType} of the given file name, using the Java Activation + * Framework. + * @param filename the filename whose mime type is to be found + * @return the mime type, if any + */ + public static Optional getMimeType(String filename) { + if (filename != null) { + String mimeType = fileTypeMap.getContentType(filename); + if (StringUtils.hasText(mimeType)) { + return Optional.of(parseMimeType(mimeType)); + } + } + return Optional.empty(); + } + + +} From 451e296a78615430ff6ee857c95a285990020f4a Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Wed, 20 Apr 2016 13:37:23 +0200 Subject: [PATCH 318/498] Zero-copy support This commit introduces support for zero-copy file transfers in the HTTP response, through the ZeroCopyHttpOutputMessage subinterface of ReactiveHttpOutputMessage. --- .../http/ZeroCopyHttpOutputMessage.java | 42 ++++++++ .../reactive/AbstractServerHttpResponse.java | 2 +- .../reactive/ReactorServerHttpResponse.java | 13 ++- .../reactive/RxNettyServerHttpResponse.java | 31 ++++++ .../reactive/UndertowHttpHandlerAdapter.java | 12 ++- .../reactive/UndertowServerHttpResponse.java | 34 +++++- .../reactive/ZeroCopyIntegrationTests.java | 98 ++++++++++++++++++ .../http/server/reactive/spring.png | Bin 0 -> 951 bytes 8 files changed, 225 insertions(+), 7 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/http/ZeroCopyHttpOutputMessage.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ZeroCopyIntegrationTests.java create mode 100644 spring-web-reactive/src/test/resources/org/springframework/http/server/reactive/spring.png diff --git a/spring-web-reactive/src/main/java/org/springframework/http/ZeroCopyHttpOutputMessage.java b/spring-web-reactive/src/main/java/org/springframework/http/ZeroCopyHttpOutputMessage.java new file mode 100644 index 0000000000..9499c8d81f --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/http/ZeroCopyHttpOutputMessage.java @@ -0,0 +1,42 @@ +/* + * 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.http; + +import java.io.File; + +import reactor.core.publisher.Mono; + +/** + * Sub-interface of {@code ReactiveOutputMessage} that has support for "zero-copy" + * file transfers. + * + * @author Arjen Poutsma + * @see Zero-copy + */ +public interface ZeroCopyHttpOutputMessage extends ReactiveHttpOutputMessage { + + /** + * Set the body of the message to the given {@link File} which will be + * used to write to the underlying HTTP layer. + * @param file the file to transfer + * @param position the position within the file from which the transfer is to begin + * @param count the number of bytes to be transferred + * @return a publisher that indicates completion or error. + */ + Mono setBody(File file, long position, long count); + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java index edf2a51a23..22041600e8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java @@ -94,7 +94,7 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse { applyBeforeCommit().after(() -> setBodyInternal(writePublisher))); } - private Mono applyBeforeCommit() { + protected Mono applyBeforeCommit() { Mono mono = Mono.empty(); if (this.state.compareAndSet(STATE_NEW, STATE_COMMITTING)) { for (Supplier> action : this.beforeCommitActions) { diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java index 8eabb39314..98dbe3f7aa 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java @@ -16,6 +16,8 @@ package org.springframework.http.server.reactive; +import java.io.File; + import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.handler.codec.http.HttpResponseStatus; @@ -31,6 +33,7 @@ import org.springframework.core.io.buffer.DataBufferAllocator; import org.springframework.core.io.buffer.NettyDataBuffer; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseCookie; +import org.springframework.http.ZeroCopyHttpOutputMessage; import org.springframework.util.Assert; /** @@ -39,7 +42,8 @@ import org.springframework.util.Assert; * @author Stephane Maldini * @author Rossen Stoyanchev */ -public class ReactorServerHttpResponse extends AbstractServerHttpResponse { +public class ReactorServerHttpResponse extends AbstractServerHttpResponse + implements ZeroCopyHttpOutputMessage { private final HttpChannel channel; @@ -99,4 +103,11 @@ public class ReactorServerHttpResponse extends AbstractServerHttpResponse { return Unpooled.wrappedBuffer(buffer.asByteBuffer()); } } + + @Override + public Mono setBody(File file, long position, long count) { + return applyBeforeCommit().after(() -> { + return this.channel.sendFile(file, position, count); + }); + } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java index a45c2b1b17..f22b794433 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java @@ -104,4 +104,35 @@ public class RxNettyServerHttpResponse extends AbstractServerHttpResponse { } } +/* + While the underlying implementation of {@link ZeroCopyHttpOutputMessage} seems to + work; it does bypass {@link #applyBeforeCommit} and more importantly it doesn't change + its {@linkplain #state()). Therefore it's commented out, for now. + + We should revisit this code once + https://github.com/ReactiveX/RxNetty/issues/194 has been fixed. + + + @Override + public Mono setBody(File file, long position, long count) { + Channel channel = this.response.unsafeNettyChannel(); + + HttpResponse httpResponse = + new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); + io.netty.handler.codec.http.HttpHeaders headers = httpResponse.headers(); + + for (Map.Entry> header : getHeaders().entrySet()) { + String headerName = header.getKey(); + for (String headerValue : header.getValue()) { + headers.add(headerName, headerValue); + } + } + Mono responseWrite = MonoChannelFuture.from(channel.write(httpResponse)); + + FileRegion fileRegion = new DefaultFileRegion(file, position, count); + Mono fileWrite = MonoChannelFuture.from(channel.writeAndFlush(fileRegion)); + + return Flux.concat(applyBeforeCommit(), responseWrite, fileWrite).after(); + } +*/ } \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java index 8acffe45bb..05cf650a5e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java @@ -67,9 +67,12 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle requestBody.registerListener(); ServerHttpRequest request = new UndertowServerHttpRequest(exchange, requestBody); - ResponseBodySubscriber responseBody = new ResponseBodySubscriber(exchange); + StreamSinkChannel responseChannel = exchange.getResponseChannel(); + ResponseBodySubscriber responseBody = + new ResponseBodySubscriber(exchange, responseChannel); responseBody.registerListener(); - ServerHttpResponse response = new UndertowServerHttpResponse(exchange, + ServerHttpResponse response = + new UndertowServerHttpResponse(exchange, responseChannel, publisher -> Mono.from(subscriber -> publisher.subscribe(responseBody)), allocator); @@ -202,9 +205,10 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle private Subscription subscription; - public ResponseBodySubscriber(HttpServerExchange exchange) { + public ResponseBodySubscriber(HttpServerExchange exchange, + StreamSinkChannel responseChannel) { this.exchange = exchange; - this.responseChannel = exchange.getResponseChannel(); + this.responseChannel = responseChannel; } public void registerListener() { diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java index 1d3d1598da..67dd0a893c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java @@ -16,6 +16,10 @@ package org.springframework.http.server.reactive; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.channels.FileChannel; import java.util.List; import java.util.Map; import java.util.function.Function; @@ -25,12 +29,14 @@ import io.undertow.server.handlers.Cookie; import io.undertow.server.handlers.CookieImpl; import io.undertow.util.HttpString; import org.reactivestreams.Publisher; +import org.xnio.channels.StreamSinkChannel; import reactor.core.publisher.Mono; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferAllocator; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseCookie; +import org.springframework.http.ZeroCopyHttpOutputMessage; import org.springframework.util.Assert; /** @@ -39,19 +45,25 @@ import org.springframework.util.Assert; * @author Marek Hawrylczak * @author Rossen Stoyanchev */ -public class UndertowServerHttpResponse extends AbstractServerHttpResponse { +public class UndertowServerHttpResponse extends AbstractServerHttpResponse + implements ZeroCopyHttpOutputMessage { private final HttpServerExchange exchange; + private final StreamSinkChannel responseChannel; + private final Function, Mono> responseBodyWriter; public UndertowServerHttpResponse(HttpServerExchange exchange, + StreamSinkChannel responseChannel, Function, Mono> responseBodyWriter, DataBufferAllocator allocator) { super(allocator); Assert.notNull(exchange, "'exchange' is required."); + Assert.notNull(responseChannel, "'responseChannel' must not be null"); Assert.notNull(responseBodyWriter, "'responseBodyWriter' must not be null"); this.exchange = exchange; + this.responseChannel = responseChannel; this.responseBodyWriter = responseBodyWriter; } @@ -71,6 +83,26 @@ public class UndertowServerHttpResponse extends AbstractServerHttpResponse { return this.responseBodyWriter.apply(publisher); } + @Override + public Mono setBody(File file, long position, long count) { + writeHeaders(); + writeCookies(); + try { + FileChannel in = new FileInputStream(file).getChannel(); + long result = this.responseChannel.transferFrom(in, position, count); + if (result < count) { + return Mono.error(new IOException("Could only write " + result + + " out of " + count + " bytes")); + } + else { + return Mono.empty(); + } + } + catch (IOException ex) { + return Mono.error(ex); + } + } + @Override protected void writeHeaders() { for (Map.Entry> entry : getHeaders().entrySet()) { diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ZeroCopyIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ZeroCopyIntegrationTests.java new file mode 100644 index 0000000000..bee5462c1a --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ZeroCopyIntegrationTests.java @@ -0,0 +1,98 @@ +/* + * 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.http.server.reactive; + +import java.io.File; +import java.net.URI; + +import org.junit.Test; +import reactor.core.publisher.Mono; + +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.http.MediaType; +import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.http.ZeroCopyHttpOutputMessage; +import org.springframework.http.server.reactive.boot.ReactorHttpServer; +import org.springframework.http.server.reactive.boot.UndertowHttpServer; +import org.springframework.web.client.RestTemplate; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeTrue; + +/** + * @author Arjen Poutsma + */ +public class ZeroCopyIntegrationTests extends AbstractHttpHandlerIntegrationTests { + + private final ZeroCopyHandler handler = new ZeroCopyHandler(); + + @Override + protected HttpHandler createHttpHandler() { + return handler; + } + + @Test + public void zeroCopy() throws Exception { + // Zero-copy only does not support servlet + assumeTrue(server instanceof ReactorHttpServer || + server instanceof UndertowHttpServer); + + RestTemplate restTemplate = new RestTemplate(); + + RequestEntity request = + RequestEntity.get(new URI("http://localhost:" + port)).build(); + + ResponseEntity response = restTemplate.exchange(request, byte[].class); + + Resource logo = + new ClassPathResource("spring.png", ZeroCopyIntegrationTests.class); + + assertTrue(response.hasBody()); + assertEquals(logo.contentLength(), response.getHeaders().getContentLength()); + assertEquals(logo.contentLength(), response.getBody().length); + assertEquals(MediaType.IMAGE_PNG, response.getHeaders().getContentType()); + + } + + private static class ZeroCopyHandler implements HttpHandler { + + @Override + public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { + try { + ZeroCopyHttpOutputMessage zeroCopyResponse = + (ZeroCopyHttpOutputMessage) response; + + Resource logo = new ClassPathResource("spring.png", + ZeroCopyIntegrationTests.class); + File logoFile = logo.getFile(); + zeroCopyResponse.getHeaders().setContentType(MediaType.IMAGE_PNG); + zeroCopyResponse.getHeaders().setContentLength(logoFile.length()); + return zeroCopyResponse.setBody(logoFile, 0, logoFile.length()); + + } + catch (Throwable ex) { + return Mono.error(ex); + } + + + } + } + +} \ No newline at end of file diff --git a/spring-web-reactive/src/test/resources/org/springframework/http/server/reactive/spring.png b/spring-web-reactive/src/test/resources/org/springframework/http/server/reactive/spring.png new file mode 100644 index 0000000000000000000000000000000000000000..2fec781a5e31ff09dd56ed4e01c1ee1927c67b2b GIT binary patch literal 951 zcmV;o14#UdP)R$ekqBST9o%}ZHk zR|8$mS|QMuiXyr^6htK5e2BCQVxInInzd=2ot<&DorU?}_i|=t=XZ|Z`OcZ6kB=UD z=%I%mDx%|IP@y-=Zvq4ac%ytI=odP$&fDap!FNHPQtB<@<3I)&6v58hJXJk2N4I=A{zxnc%9YW!vscwtsRZ zZvd}6E#J~*AoX{QQr8CLXbs$*_ z@@=*t5?^HEQ%G?Kgk}7*r{%3+a~VgnF0gX<^-FbNqZUSg_c@kC=bK5pP4>QSsSaHC zw0wFEOG5Vrlj>HZt9xxy9r)-eaaw6o&Tu3oGnnj`PaQjTj`Dt$whjMHoQm$M{F5@21e8omm*VI9z$B?T z&^P8K61iS{jTsts%GzV9ah7A@i3IiT(pniHy_Du;wFL@?n+qhHN-t;zh;c6uL;&v>8YG`;o4;(TiQ3 z2+(|`l2sNXIevp7hc2=twTk0QVMMlhn6P-%_tvtmR#-XCEByjn1i80#Czx`U<3}>4 zz|6}@ji@U^gH-%w)?+ZZP{I`vbehUexa% z1xJ8t`2dRO*{GsihuB=uE0TX=zqYNMYl=H+`hq-eKGMh}3Hzl9PA2{gz<@9i54M9G zP$8UbScP Date: Wed, 20 Apr 2016 13:53:36 +0200 Subject: [PATCH 319/498] Reactive HttpMessageConverter This commit introduces a reactive version of the HttpMessageConverter. During the implementation of zero-copy support, it became apparent that it was ueful to have a common abstraction between client and server that operated on HttpMessages rather than DataBuffers. Two HttpMessageConverter implementations are provided: - The CodecHttpMessageConverter, based on Encoder/Decoder. - The ResourceHttpMessageConverter, using zero-copy if available. --- .../reactive/CodecHttpMessageConverter.java | 102 ++++++++ .../reactive/HttpMessageConverter.java | 91 +++++++ .../ResourceHttpMessageConverter.java | 231 ++++++++++++++++++ 3 files changed, 424 insertions(+) create mode 100644 spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/CodecHttpMessageConverter.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/HttpMessageConverter.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/ResourceHttpMessageConverter.java diff --git a/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/CodecHttpMessageConverter.java b/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/CodecHttpMessageConverter.java new file mode 100644 index 0000000000..b55ad9c4cb --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/CodecHttpMessageConverter.java @@ -0,0 +1,102 @@ +/* + * 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.http.converter.reactive; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.core.ResolvableType; +import org.springframework.core.codec.Decoder; +import org.springframework.core.codec.Encoder; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferAllocator; +import org.springframework.http.MediaType; +import org.springframework.http.ReactiveHttpInputMessage; +import org.springframework.http.ReactiveHttpOutputMessage; +import org.springframework.http.support.MediaTypeUtils; + +/** + * @author Arjen Poutsma + */ +public class CodecHttpMessageConverter implements HttpMessageConverter { + + private final Encoder encoder; + + private final Decoder decoder; + + public CodecHttpMessageConverter(Encoder encoder, Decoder decoder) { + this.encoder = encoder; + this.decoder = decoder; + } + + @Override + public boolean canRead(ResolvableType type, MediaType mediaType) { + return this.decoder != null && this.decoder.canDecode(type, mediaType); + } + + @Override + public boolean canWrite(ResolvableType type, MediaType mediaType) { + return this.encoder != null && this.encoder.canEncode(type, mediaType); + } + + @Override + public List getReadableMediaTypes() { + return this.decoder != null ? this.decoder.getSupportedMimeTypes().stream(). + map(MediaTypeUtils::toMediaType). + collect(Collectors.toList()) : Collections.emptyList(); + } + + @Override + public List getWritableMediaTypes() { + return this.encoder != null ? this.encoder.getSupportedMimeTypes().stream(). + map(MediaTypeUtils::toMediaType). + collect(Collectors.toList()) : Collections.emptyList(); + } + + @Override + public Flux read(ResolvableType type, ReactiveHttpInputMessage inputMessage) { + if (this.decoder == null) { + return Flux.error(new IllegalStateException("No decoder set")); + } + MediaType contentType = inputMessage.getHeaders().getContentType(); + if (contentType == null) { + contentType = MediaType.APPLICATION_OCTET_STREAM; + } + + Flux body = inputMessage.getBody(); + + return this.decoder.decode(body, type, contentType); + } + + @Override + public Mono write(Publisher inputStream, ResolvableType type, + MediaType contentType, + ReactiveHttpOutputMessage outputMessage) { + if (this.encoder == null) { + return Mono.error(new IllegalStateException("No decoder set")); + } + outputMessage.getHeaders().setContentType(contentType); + DataBufferAllocator allocator = outputMessage.allocator(); + Flux body = encoder.encode(inputStream, allocator, type, contentType); + return outputMessage.setBody(body); + } +} diff --git a/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/HttpMessageConverter.java b/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/HttpMessageConverter.java new file mode 100644 index 0000000000..ce001c5d08 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/HttpMessageConverter.java @@ -0,0 +1,91 @@ +/* + * 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.http.converter.reactive; + +import java.util.List; + +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.core.ResolvableType; +import org.springframework.http.MediaType; +import org.springframework.http.ReactiveHttpInputMessage; +import org.springframework.http.ReactiveHttpOutputMessage; + +/** + * Strategy interface that specifies a converter that can convert from and to HTTP + * requests and responses. + * @author Arjen Poutsma + */ +public interface HttpMessageConverter { + + /** + * Indicates whether the given class can be read by this converter. + * @param type the type to test for readability + * @param mediaType the media type to read, can be {@code null} if not specified. + * Typically the value of a {@code Content-Type} header. + * @return {@code true} if readable; {@code false} otherwise + */ + boolean canRead(ResolvableType type, MediaType mediaType); + + /** + * Return the list of {@link MediaType} objects that can be read by this converter. + * @return the list of supported readable media types + */ + List getReadableMediaTypes(); + + /** + * Read an object of the given type form the given input message, and returns it. + * @param type the type of object to return. This type must have previously been + * passed to the + * {@link #canRead canRead} method of this interface, which must have returned {@code + * true}. + * @param inputMessage the HTTP input message to read from + * @return the converted object + */ + Flux read(ResolvableType type, ReactiveHttpInputMessage inputMessage); + + /** + * Indicates whether the given class can be written by this converter. + * @param type the class to test for writability + * @param mediaType the media type to write, can be {@code null} if not specified. + * Typically the value of an {@code Accept} header. + * @return {@code true} if writable; {@code false} otherwise + */ + boolean canWrite(ResolvableType type, MediaType mediaType); + + /** + * Return the list of {@link MediaType} objects that can be written by this + * converter. + * @return the list of supported readable media types + */ + List getWritableMediaTypes(); + + /** + * Write an given object to the given output message. + * @param inputStream the input stream to write + * @param type the stream element type to process. + * @param contentType the content type to use when writing. May be {@code null} to + * indicate that the default content type of the converter must be used. + * @param outputMessage the message to write to + * @return + */ + Mono write(Publisher inputStream, + ResolvableType type, MediaType contentType, + ReactiveHttpOutputMessage outputMessage); +} \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/ResourceHttpMessageConverter.java b/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/ResourceHttpMessageConverter.java new file mode 100644 index 0000000000..5091fc6fd2 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/ResourceHttpMessageConverter.java @@ -0,0 +1,231 @@ +/* + * 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.http.converter.reactive; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.core.ResolvableType; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.DescriptiveResource; +import org.springframework.core.io.InputStreamResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.support.DataBufferUtils; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpRangeResource; +import org.springframework.http.MediaType; +import org.springframework.http.ReactiveHttpInputMessage; +import org.springframework.http.ReactiveHttpOutputMessage; +import org.springframework.http.ZeroCopyHttpOutputMessage; +import org.springframework.http.support.MediaTypeUtils; +import org.springframework.util.MimeTypeUtils2; +import org.springframework.util.ResourceUtils; +import org.springframework.util.StreamUtils; + +/** + * @author Arjen Poutsma + */ +public class ResourceHttpMessageConverter implements HttpMessageConverter { + + private static final int BUFFER_SIZE = StreamUtils.BUFFER_SIZE; + + private static final List SUPPORTED_MEDIA_TYPES = + Collections.singletonList(MediaType.ALL); + + @Override + public boolean canRead(ResolvableType type, MediaType mediaType) { + return Resource.class.isAssignableFrom(type.getRawClass()); + } + + @Override + public boolean canWrite(ResolvableType type, MediaType mediaType) { + return Resource.class.isAssignableFrom(type.getRawClass()); + } + + @Override + public List getReadableMediaTypes() { + return SUPPORTED_MEDIA_TYPES; + } + + @Override + public List getWritableMediaTypes() { + return SUPPORTED_MEDIA_TYPES; + } + + @Override + public Flux read(ResolvableType type, + ReactiveHttpInputMessage inputMessage) { + Class clazz = type.getRawClass(); + + Flux body = inputMessage.getBody(); + + if (InputStreamResource.class.equals(clazz)) { + InputStream is = DataBufferUtils.toInputStream(body); + return Flux.just(new InputStreamResource(is)); + } + else if (clazz.isAssignableFrom(ByteArrayResource.class)) { + Mono singleBuffer = body.reduce(DataBuffer::write); + return Flux.from(singleBuffer.map(buffer -> { + byte[] bytes = new byte[buffer.readableByteCount()]; + buffer.read(bytes); + return new ByteArrayResource(bytes); + })); + } + else { + return Flux.error(new IllegalStateException( + "Unsupported resource class: " + clazz)); + } + } + + @Override + public Mono write(Publisher inputStream, + ResolvableType type, MediaType contentType, + ReactiveHttpOutputMessage outputMessage) { + + if (inputStream instanceof Mono) { + // single resource + return Mono.from(Flux.from(inputStream). + flatMap(resource -> { + HttpHeaders headers = outputMessage.getHeaders(); + addHeaders(headers, resource, contentType); + + if (resource instanceof HttpRangeResource) { + return writePartialContent((HttpRangeResource) resource, + outputMessage); + } + else { + return writeContent(resource, outputMessage, 0, -1); + } + + + })); + } + else { + // multiple resources, not supported! + return Mono.error(new IllegalArgumentException( + "Multiple resources not yet supported")); + } + } + + protected void addHeaders(HttpHeaders headers, Resource resource, + MediaType contentType) { + if (headers.getContentType() == null) { + if (contentType == null || + !contentType.isConcrete() || + MediaType.APPLICATION_OCTET_STREAM.equals(contentType)) { + contentType = MimeTypeUtils2.getMimeType(resource.getFilename()). + map(MediaTypeUtils::toMediaType). + orElse(MediaType.APPLICATION_OCTET_STREAM); + } + headers.setContentType(contentType); + } + if (headers.getContentLength() < 0) { + contentLength(resource).ifPresent(headers::setContentLength); + } + headers.add(HttpHeaders.ACCEPT_RANGES, "bytes"); + } + + private Mono writeContent(Resource resource, + ReactiveHttpOutputMessage outputMessage, long position, long count) { + if (outputMessage instanceof ZeroCopyHttpOutputMessage) { + Optional file = getFile(resource); + if (file.isPresent()) { + ZeroCopyHttpOutputMessage zeroCopyResponse = + (ZeroCopyHttpOutputMessage) outputMessage; + + if (count < 0) { + count = file.get().length(); + } + + return zeroCopyResponse.setBody(file.get(), position, count); + } + } + + // non-zero copy fallback + try { + InputStream is = resource.getInputStream(); + long skipped = is.skip(position); + if (skipped < position) { + return Mono.error(new IOException( + "Skipped only " + skipped + " bytes out of " + count + + " required.")); + } + + Flux responseBody = + DataBufferUtils.read(is, outputMessage.allocator(), BUFFER_SIZE); + if (count > 0) { + responseBody = DataBufferUtils.takeUntilByteCount(responseBody, count); + } + + return outputMessage.setBody(responseBody); + } + catch (IOException ex) { + return Mono.error(ex); + } + } + + protected Mono writePartialContent(HttpRangeResource resource, + ReactiveHttpOutputMessage outputMessage) { + + // TODO: implement + + return Mono.empty(); + } + + private static Optional contentLength(Resource resource) { + // Don't try to determine contentLength on InputStreamResource - cannot be read afterwards... + // Note: custom InputStreamResource subclasses could provide a pre-calculated content length! + if (InputStreamResource.class != resource.getClass()) { + try { + return Optional.of(resource.contentLength()); + } + catch (IOException ignored) { + } + } + return Optional.empty(); + } + + private static Optional getFile(Resource resource) { + // TODO: introduce Resource.hasFile() property to bypass the potential IOException thrown in Resource.getFile() + // the following Resource implementations do not support getURI/getFile + if (!(resource instanceof ByteArrayResource || + resource instanceof DescriptiveResource || + resource instanceof InputStreamResource)) { + try { + URI resourceUri = resource.getURI(); + if (ResourceUtils.URL_PROTOCOL_FILE.equals(resourceUri.getScheme())) { + return Optional.of(ResourceUtils.getFile(resourceUri)); + } + } + catch (IOException ignored) { + } + } + return Optional.empty(); + } + + +} From 804f69c8b6e8156462449a310a1e0865eb329b61 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Wed, 20 Apr 2016 14:00:35 +0200 Subject: [PATCH 320/498] Wrapping up zero-copy support This commit wraps up the previous commits: - It uses HttpMessageConverter in the web.reactive.server package instead of Encoder/Decoder. - It introduces tests for the Resource @ResponseBodies. --- .../PathExtensionContentTypeResolver.java | 83 ++-------- .../RequestBodyArgumentResolver.java | 44 ++--- .../RequestMappingHandlerAdapter.java | 31 ++-- .../annotation/ResponseBodyResultHandler.java | 151 +++++++++--------- .../reactive/DispatcherHandlerErrorTests.java | 11 +- .../RequestMappingIntegrationTests.java | 76 +++++---- .../ResponseBodyResultHandlerTests.java | 3 +- 7 files changed, 184 insertions(+), 215 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/PathExtensionContentTypeResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/PathExtensionContentTypeResolver.java index 57b98d666f..2326797943 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/PathExtensionContentTypeResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/PathExtensionContentTypeResolver.java @@ -13,23 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.web.reactive.accept; -import java.io.IOException; -import java.io.InputStream; import java.util.Locale; import java.util.Map; -import javax.activation.FileTypeMap; -import javax.activation.MimetypesFileTypeMap; +import java.util.Optional; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.http.MediaType; +import org.springframework.http.support.MediaTypeUtils; import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; +import org.springframework.util.MimeType; +import org.springframework.util.MimeTypeUtils2; import org.springframework.util.StringUtils; import org.springframework.web.server.NotAcceptableStatusException; import org.springframework.web.server.ServerWebExchange; @@ -48,12 +44,6 @@ import org.springframework.web.util.WebUtils; */ public class PathExtensionContentTypeResolver extends AbstractMappingContentTypeResolver { - private static final Log logger = LogFactory.getLog(PathExtensionContentTypeResolver.class); - - private static final boolean JAF_PRESENT = ClassUtils.isPresent("javax.activation.FileTypeMap", - PathExtensionContentTypeResolver.class.getClassLoader()); - - private boolean useJaf = true; private boolean ignoreUnknownExtensions = true; @@ -103,8 +93,9 @@ public class PathExtensionContentTypeResolver extends AbstractMappingContentType @Override protected MediaType handleNoMatch(String key) throws NotAcceptableStatusException { - if (this.useJaf && JAF_PRESENT) { - MediaType mediaType = JafMediaTypeFactory.getMediaType("file." + key); + if (this.useJaf) { + Optional mimeType = MimeTypeUtils2.getMimeType("file." + key); + MediaType mediaType = mimeType.map(MediaTypeUtils::toMediaType).orElse(null); if (mediaType != null && !MediaType.APPLICATION_OCTET_STREAM.equals(mediaType)) { return mediaType; } @@ -130,8 +121,10 @@ public class PathExtensionContentTypeResolver extends AbstractMappingContentType if (extension != null) { mediaType = getMediaType(extension); } - if (mediaType == null && JAF_PRESENT) { - mediaType = JafMediaTypeFactory.getMediaType(filename); + if (mediaType == null) { + mediaType = + MimeTypeUtils2.getMimeType(filename).map(MediaTypeUtils::toMediaType) + .orElse(null); } if (MediaType.APPLICATION_OCTET_STREAM.equals(mediaType)) { mediaType = null; @@ -139,56 +132,4 @@ public class PathExtensionContentTypeResolver extends AbstractMappingContentType return mediaType; } - - /** - * Inner class to avoid hard-coded dependency on JAF. - */ - private static class JafMediaTypeFactory { - - private static final FileTypeMap fileTypeMap; - - static { - fileTypeMap = initFileTypeMap(); - } - - /** - * Find extended mime.types from the spring-context-support module. - */ - private static FileTypeMap initFileTypeMap() { - Resource resource = new ClassPathResource("org/springframework/mail/javamail/mime.types"); - if (resource.exists()) { - if (logger.isTraceEnabled()) { - logger.trace("Loading JAF FileTypeMap from " + resource); - } - InputStream inputStream = null; - try { - inputStream = resource.getInputStream(); - return new MimetypesFileTypeMap(inputStream); - } - catch (IOException ex) { - // ignore - } - finally { - if (inputStream != null) { - try { - inputStream.close(); - } - catch (IOException ex) { - // ignore - } - } - } - } - if (logger.isTraceEnabled()) { - logger.trace("Loading default Java Activation Framework FileTypeMap"); - } - return FileTypeMap.getDefaultFileTypeMap(); - } - - public static MediaType getMediaType(String filename) { - String mediaType = fileTypeMap.getContentType(filename); - return (StringUtils.hasText(mediaType) ? MediaType.parseMediaType(mediaType) : null); - } - } - } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java index 216a62cc4c..fca79df098 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java @@ -24,10 +24,10 @@ import reactor.core.publisher.Mono; import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; -import org.springframework.core.codec.Decoder; import org.springframework.core.convert.ConversionService; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.MediaType; +import org.springframework.http.converter.reactive.HttpMessageConverter; import org.springframework.ui.ModelMap; import org.springframework.util.Assert; import org.springframework.web.bind.annotation.RequestBody; @@ -40,15 +40,15 @@ import org.springframework.web.server.ServerWebExchange; */ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolver { - private final List> decoders; + private final List> messageConverters; private final ConversionService conversionService; - - public RequestBodyArgumentResolver(List> decoders, ConversionService service) { - Assert.notEmpty(decoders, "At least one decoder is required."); + public RequestBodyArgumentResolver(List> messageConverters, + ConversionService service) { + Assert.notEmpty(messageConverters, "At least one message converter is required."); Assert.notNull(service, "'conversionService' is required."); - this.decoders = decoders; + this.messageConverters = messageConverters; this.conversionService = service; } @@ -62,22 +62,29 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve public Mono resolveArgument(MethodParameter parameter, ModelMap model, ServerWebExchange exchange) { + ResolvableType type = ResolvableType.forMethodParameter(parameter); + ResolvableType elementType = type.hasGenerics() ? type.getGeneric(0) : type; + MediaType mediaType = exchange.getRequest().getHeaders().getContentType(); if (mediaType == null) { mediaType = MediaType.APPLICATION_OCTET_STREAM; } - ResolvableType type = ResolvableType.forMethodParameter(parameter); - Flux body = exchange.getRequest().getBody(); - Flux elementFlux = body; - ResolvableType elementType = type.hasGenerics() ? type.getGeneric(0) : type; - Decoder decoder = resolveDecoder(elementType, mediaType); - if (decoder != null) { - elementFlux = decoder.decode(body, elementType, mediaType); + Flux body = exchange.getRequest().getBody(); + Flux elementFlux; + + HttpMessageConverter messageConverter = + resolveMessageConverter(elementType, mediaType); + if (messageConverter != null) { + elementFlux = messageConverter.read(elementType, exchange.getRequest()); + } + else { + elementFlux = body; } if (this.conversionService.canConvert(Publisher.class, type.getRawClass())) { - return Mono.just(this.conversionService.convert(elementFlux, type.getRawClass())); + return Mono.just(this.conversionService + .convert(elementFlux, type.getRawClass())); } else if (type.getRawClass() == Flux.class) { return Mono.just(elementFlux); @@ -90,10 +97,11 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve return elementFlux.next().map(o -> o); } - private Decoder resolveDecoder(ResolvableType type, MediaType mediaType, Object... hints) { - for (Decoder decoder : this.decoders) { - if (decoder.canDecode(type, mediaType, hints)) { - return decoder; + private HttpMessageConverter resolveMessageConverter(ResolvableType type, + MediaType mediaType) { + for (HttpMessageConverter messageConverter : this.messageConverters) { + if (messageConverter.canRead(type, mediaType)) { + return messageConverter; } } return null; 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 e906bf7a83..d9d2527e62 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 @@ -17,6 +17,7 @@ package org.springframework.web.reactive.result.method.annotation; import java.lang.reflect.Method; +import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -28,16 +29,19 @@ import org.apache.commons.logging.LogFactory; import reactor.core.publisher.Mono; import org.springframework.beans.factory.InitializingBean; -import org.springframework.core.codec.Decoder; import org.springframework.core.codec.support.ByteBufferDecoder; +import org.springframework.core.codec.support.ByteBufferEncoder; import org.springframework.core.codec.support.JacksonJsonDecoder; +import org.springframework.core.codec.support.JacksonJsonEncoder; import org.springframework.core.codec.support.Jaxb2Decoder; +import org.springframework.core.codec.support.Jaxb2Encoder; import org.springframework.core.codec.support.JsonObjectDecoder; import org.springframework.core.codec.support.StringDecoder; +import org.springframework.core.codec.support.StringEncoder; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.DefaultConversionService; -import org.springframework.core.io.buffer.DataBufferAllocator; -import org.springframework.core.io.buffer.DefaultDataBufferAllocator; +import org.springframework.http.converter.reactive.CodecHttpMessageConverter; +import org.springframework.http.converter.reactive.HttpMessageConverter; import org.springframework.ui.ExtendedModelMap; import org.springframework.ui.ModelMap; import org.springframework.util.ObjectUtils; @@ -62,8 +66,6 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Initializin private ConversionService conversionService = new DefaultConversionService(); - private DataBufferAllocator allocator = new DefaultDataBufferAllocator(); - private final Map, ExceptionHandlerMethodResolver> exceptionHandlerCache = new ConcurrentHashMap<>(64); @@ -92,20 +94,23 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Initializin return this.conversionService; } - public void setAllocator(DataBufferAllocator allocator) { - this.allocator = allocator; - } - @Override public void afterPropertiesSet() throws Exception { if (ObjectUtils.isEmpty(this.argumentResolvers)) { + List> messageConverters = Arrays.asList( + new CodecHttpMessageConverter(new ByteBufferEncoder(), + new ByteBufferDecoder()), + new CodecHttpMessageConverter(new StringEncoder(), + new StringDecoder()), + new CodecHttpMessageConverter(new Jaxb2Encoder(), + new Jaxb2Decoder()), + new CodecHttpMessageConverter(new JacksonJsonEncoder(), + new JacksonJsonDecoder(new JsonObjectDecoder()))); - List> decoders = Arrays.asList(new ByteBufferDecoder(), - new StringDecoder(), new Jaxb2Decoder(), - new JacksonJsonDecoder(new JsonObjectDecoder())); this.argumentResolvers.add(new RequestParamArgumentResolver()); - this.argumentResolvers.add(new RequestBodyArgumentResolver(decoders, this.conversionService)); + this.argumentResolvers.add(new RequestBodyArgumentResolver(messageConverters, + this.conversionService)); this.argumentResolvers.add(new ModelArgumentResolver()); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java index c69a315401..5e5c61a4e9 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java @@ -34,10 +34,9 @@ import org.springframework.core.MethodParameter; import org.springframework.core.Ordered; import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.core.codec.Encoder; import org.springframework.core.convert.ConversionService; -import org.springframework.core.io.buffer.DataBufferAllocator; import org.springframework.http.MediaType; +import org.springframework.http.converter.reactive.HttpMessageConverter; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.util.Assert; @@ -54,60 +53,51 @@ import org.springframework.web.server.ServerWebExchange; * @author Rossen Stoyanchev * @author Stephane Maldini * @author Sebastien Deleuze + * @author Arjen Poutsma */ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered { private static final MediaType MEDIA_TYPE_APPLICATION = new MediaType("application"); - private final List> encoders; + private final List> messageConverters; private final ConversionService conversionService; private final List allMediaTypes; - private final Map, List> mediaTypesByEncoder; + private final Map, List> mediaTypesByEncoder; private int order = 0; // TODO: should be MAX_VALUE - - public ResponseBodyResultHandler(List> encoders, ConversionService service) { - Assert.notEmpty(encoders, "At least one encoders is required."); + public ResponseBodyResultHandler(List> messageConverters, + ConversionService service) { + Assert.notEmpty(messageConverters, "At least one message converter is required."); Assert.notNull(service, "'conversionService' is required."); - this.encoders = encoders; + this.messageConverters = messageConverters; this.conversionService = service; - this.allMediaTypes = getAllMediaTypes(encoders); - this.mediaTypesByEncoder = getMediaTypesByEncoder(encoders); + this.allMediaTypes = getAllMediaTypes(messageConverters); + this.mediaTypesByEncoder = getMediaTypesByConverter(messageConverters); } - private static List getAllMediaTypes(List> encoders) { + private static List getAllMediaTypes( + List> messageConverters) { Set set = new LinkedHashSet<>(); - encoders.forEach(encoder -> set.addAll(toMediaTypes(encoder.getSupportedMimeTypes()))); + messageConverters.forEach( + converter -> set.addAll(converter.getWritableMediaTypes())); List result = new ArrayList<>(set); MediaType.sortBySpecificity(result); return Collections.unmodifiableList(result); } - private static Map, List> getMediaTypesByEncoder(List> encoders) { - Map, List> result = new HashMap<>(encoders.size()); - encoders.forEach(encoder -> result.put(encoder, toMediaTypes(encoder.getSupportedMimeTypes()))); + private static Map, List> getMediaTypesByConverter( + List> converters) { + Map, List> result = + new HashMap<>(converters.size()); + converters.forEach(converter -> result + .put(converter, converter.getWritableMediaTypes())); return Collections.unmodifiableMap(result); } - /** - * TODO: MediaType static method - */ - private static List toMediaTypes(List mimeTypes) { - return mimeTypes.stream().map(ResponseBodyResultHandler::toMediaType).collect(Collectors.toList()); - } - - /** - * TODO: MediaType constructor - */ - private static MediaType toMediaType(MimeType mimeType) { - return new MediaType(mimeType.getType(), mimeType.getSubtype(), mimeType.getParameters()); - } - - public void setOrder(int order) { this.order = order; } @@ -154,65 +144,77 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered elementType = returnType; } - List requestedMediaTypes = getAcceptableMediaTypes(exchange.getRequest()); - List producibleMediaTypes = getProducibleMediaTypes(elementType); - - if (producibleMediaTypes.isEmpty()) { - producibleMediaTypes.add(MediaType.ALL); - } - - Set compatibleMediaTypes = new LinkedHashSet<>(); - for (MediaType requestedType : requestedMediaTypes) { - for (MediaType producibleType : producibleMediaTypes) { - if (requestedType.isCompatibleWith(producibleType)) { - compatibleMediaTypes.add(getMostSpecificMediaType(requestedType, producibleType)); - } - } - } + List compatibleMediaTypes = + getCompatibleMediaTypes(exchange.getRequest(), elementType); if (compatibleMediaTypes.isEmpty()) { - return Mono.error(new NotAcceptableStatusException(producibleMediaTypes)); + return Mono.error(new NotAcceptableStatusException( + getProducibleMediaTypes(elementType))); } - List mediaTypes = new ArrayList<>(compatibleMediaTypes); - MediaType.sortBySpecificityAndQuality(mediaTypes); + Optional selectedMediaType = selectBestMediaType(compatibleMediaTypes); - MediaType selectedMediaType = null; - for (MediaType mediaType : mediaTypes) { - if (mediaType.isConcrete()) { - selectedMediaType = mediaType; - break; - } - else if (mediaType.equals(MediaType.ALL) || mediaType.equals(MEDIA_TYPE_APPLICATION)) { - selectedMediaType = MediaType.APPLICATION_OCTET_STREAM; - break; - } - } - - if (selectedMediaType != null) { - Encoder encoder = resolveEncoder(elementType, selectedMediaType); - if (encoder != null) { + if (selectedMediaType.isPresent()) { + HttpMessageConverter converter = + resolveEncoder(elementType, selectedMediaType.get()); + if (converter != null) { ServerHttpResponse response = exchange.getResponse(); - response.getHeaders().setContentType(selectedMediaType); - DataBufferAllocator allocator = response.allocator(); - return response.setBody( - encoder.encode((Publisher) publisher, allocator, elementType, - selectedMediaType)); + return converter.write((Publisher) publisher, elementType, + selectedMediaType.get(), + response); } } return Mono.error(new NotAcceptableStatusException(this.allMediaTypes)); } + private List getCompatibleMediaTypes(ServerHttpRequest request, + ResolvableType elementType) { + + List acceptableMediaTypes = getAcceptableMediaTypes(request); + List producibleMediaTypes = getProducibleMediaTypes(elementType); + + Set compatibleMediaTypes = new LinkedHashSet<>(); + for (MediaType acceptableMediaType : acceptableMediaTypes) { + compatibleMediaTypes.addAll(producibleMediaTypes.stream(). + filter(acceptableMediaType::isCompatibleWith). + map(producibleType -> getMostSpecificMediaType(acceptableMediaType, + producibleType)).collect(Collectors.toList())); + } + + List result = new ArrayList<>(compatibleMediaTypes); + MediaType.sortBySpecificityAndQuality(result); + return result; + } + private List getAcceptableMediaTypes(ServerHttpRequest request) { List mediaTypes = request.getHeaders().getAccept(); return (mediaTypes.isEmpty() ? Collections.singletonList(MediaType.ALL) : mediaTypes); } + private Optional selectBestMediaType( + List compatibleMediaTypes) { + for (MediaType mediaType : compatibleMediaTypes) { + if (mediaType.isConcrete()) { + return Optional.of(mediaType); + } + else if (mediaType.equals(MediaType.ALL) || + mediaType.equals(MEDIA_TYPE_APPLICATION)) { + return Optional.of(MediaType.APPLICATION_OCTET_STREAM); + } + } + return Optional.empty(); + } + private List getProducibleMediaTypes(ResolvableType type) { - return this.encoders.stream() - .filter(encoder -> encoder.canEncode(type, null)) + List result = this.messageConverters.stream() + .filter(converter -> converter.canWrite(type, null)) .flatMap(encoder -> this.mediaTypesByEncoder.get(encoder).stream()) .collect(Collectors.toList()); + if (result.isEmpty()) { + result.add(MediaType.ALL); + } + + return result; } /** @@ -225,10 +227,11 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered return (comparator.compare(acceptType, produceType) <= 0 ? acceptType : produceType); } - private Encoder resolveEncoder(ResolvableType type, MediaType mediaType, Object... hints) { - for (Encoder encoder : this.encoders) { - if (encoder.canEncode(type, mediaType, hints)) { - return encoder; + private HttpMessageConverter resolveEncoder(ResolvableType type, + MediaType mediaType) { + for (HttpMessageConverter converter : this.messageConverters) { + if (converter.canWrite(type, mediaType)) { + return converter; } } return null; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java index 91c91bc9aa..c7f84a8028 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java @@ -30,7 +30,7 @@ import reactor.core.util.SignalKind; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.core.codec.Encoder; +import org.springframework.core.codec.support.StringDecoder; import org.springframework.core.codec.support.StringEncoder; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.io.buffer.DataBuffer; @@ -38,6 +38,8 @@ import org.springframework.core.io.buffer.DefaultDataBufferAllocator; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.http.converter.reactive.CodecHttpMessageConverter; +import org.springframework.http.converter.reactive.HttpMessageConverter; import org.springframework.http.server.reactive.MockServerHttpRequest; import org.springframework.http.server.reactive.MockServerHttpResponse; import org.springframework.stereotype.Controller; @@ -230,8 +232,11 @@ public class DispatcherHandlerErrorTests { @Bean public ResponseBodyResultHandler resultHandler() { - List> encoders = Collections.singletonList(new StringEncoder()); - return new ResponseBodyResultHandler(encoders, new DefaultConversionService()); + List> converters = Collections.singletonList( + new CodecHttpMessageConverter<>(new StringEncoder(), + new StringDecoder())); + return new ResponseBodyResultHandler(converters, + new DefaultConversionService()); } @Bean diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java index 66d595e495..579de341e6 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java @@ -40,14 +40,18 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ResolvableType; -import org.springframework.core.codec.Encoder; +import org.springframework.core.codec.support.ByteBufferDecoder; import org.springframework.core.codec.support.ByteBufferEncoder; +import org.springframework.core.codec.support.JacksonJsonDecoder; import org.springframework.core.codec.support.JacksonJsonEncoder; +import org.springframework.core.codec.support.StringDecoder; import org.springframework.core.codec.support.StringEncoder; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter; import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferAllocator; import org.springframework.core.io.buffer.DefaultDataBufferAllocator; @@ -55,14 +59,19 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.reactive.CodecHttpMessageConverter; +import org.springframework.http.converter.reactive.HttpMessageConverter; +import org.springframework.http.converter.reactive.ResourceHttpMessageConverter; import org.springframework.http.server.reactive.AbstractHttpHandlerIntegrationTests; import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.http.server.reactive.ZeroCopyIntegrationTests; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; import org.springframework.web.reactive.DispatcherHandler; @@ -73,8 +82,7 @@ import org.springframework.web.reactive.view.freemarker.FreeMarkerConfigurer; import org.springframework.web.reactive.view.freemarker.FreeMarkerViewResolver; import org.springframework.web.server.adapter.WebHttpHandlerBuilder; -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; +import static org.junit.Assert.*; /** * @author Rossen Stoyanchev @@ -85,6 +93,8 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati private AnnotationConfigApplicationContext wac; + private RestTemplate restTemplate = new RestTemplate(); + @Override protected HttpHandler createHttpHandler() { @@ -100,9 +110,6 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @Test public void helloWithQueryParam() throws Exception { - - RestTemplate restTemplate = new RestTemplate(); - URI url = new URI("http://localhost:" + port + "/param?name=George"); RequestEntity request = RequestEntity.get(url).build(); ResponseEntity response = restTemplate.exchange(request, String.class); @@ -112,9 +119,6 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @Test public void rawPojoResponse() throws Exception { - - RestTemplate restTemplate = new RestTemplate(); - URI url = new URI("http://localhost:" + port + "/raw"); RequestEntity request = RequestEntity.get(url).accept(MediaType.APPLICATION_JSON).build(); @@ -125,9 +129,6 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @Test public void rawFluxResponse() throws Exception { - - RestTemplate restTemplate = new RestTemplate(); - URI url = new URI("http://localhost:" + port + "/raw-flux"); RequestEntity request = RequestEntity.get(url).build(); ResponseEntity response = restTemplate.exchange(request, String.class); @@ -137,9 +138,6 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @Test public void rawObservableResponse() throws Exception { - - RestTemplate restTemplate = new RestTemplate(); - URI url = new URI("http://localhost:" + port + "/raw-observable"); RequestEntity request = RequestEntity.get(url).build(); ResponseEntity response = restTemplate.exchange(request, String.class); @@ -149,9 +147,6 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @Test public void handleWithThrownException() throws Exception { - - RestTemplate restTemplate = new RestTemplate(); - URI url = new URI("http://localhost:" + port + "/thrown-exception"); RequestEntity request = RequestEntity.get(url).build(); ResponseEntity response = restTemplate.exchange(request, String.class); @@ -161,9 +156,6 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @Test public void handleWithErrorSignal() throws Exception { - - RestTemplate restTemplate = new RestTemplate(); - URI url = new URI("http://localhost:" + port + "/error-signal"); RequestEntity request = RequestEntity.get(url).build(); ResponseEntity response = restTemplate.exchange(request, String.class); @@ -174,8 +166,6 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @Test @Ignore public void streamResult() throws Exception { - RestTemplate restTemplate = new RestTemplate(); - URI url = new URI("http://localhost:" + port + "/stream-result"); RequestEntity request = RequestEntity.get(url).build(); ResponseEntity response = restTemplate.exchange(request, String[].class); @@ -295,9 +285,6 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @Test public void html() throws Exception { - - RestTemplate restTemplate = new RestTemplate(); - URI url = new URI("http://localhost:" + port + "/html?name=Jason"); RequestEntity request = RequestEntity.get(url).accept(MediaType.TEXT_HTML).build(); ResponseEntity response = restTemplate.exchange(request, String.class); @@ -305,9 +292,20 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati assertEquals("Hello: Jason!", response.getBody()); } + @Test + public void resource() throws Exception { + URI url = new URI("http://localhost:" + port + "/resource"); + RequestEntity request = RequestEntity.get(url).build(); + ResponseEntity response = restTemplate.exchange(request, byte[].class); + + assertTrue(response.hasBody()); + assertEquals(951, response.getHeaders().getContentLength()); + assertEquals(951, response.getBody().length); + assertEquals(new MediaType("image", "x-png"), + response.getHeaders().getContentType()); + } private void serializeAsPojo(String requestUrl) throws Exception { - RestTemplate restTemplate = new RestTemplate(); RequestEntity request = RequestEntity.get(new URI(requestUrl)) .accept(MediaType.APPLICATION_JSON) .build(); @@ -317,7 +315,6 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati } private void serializeAsCollection(String requestUrl) throws Exception { - RestTemplate restTemplate = new RestTemplate(); RequestEntity request = RequestEntity.get(new URI(requestUrl)) .accept(MediaType.APPLICATION_JSON) .build(); @@ -331,7 +328,6 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati private void capitalizePojo(String requestUrl) throws Exception { - RestTemplate restTemplate = new RestTemplate(); RequestEntity request = RequestEntity.post(new URI(requestUrl)) .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON) @@ -342,7 +338,6 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati } private void capitalizeCollection(String requestUrl) throws Exception { - RestTemplate restTemplate = new RestTemplate(); RequestEntity> request = RequestEntity.post(new URI(requestUrl)) .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON) @@ -356,7 +351,6 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati } private void createJson(String requestUrl) throws Exception { - RestTemplate restTemplate = new RestTemplate(); URI url = new URI(requestUrl); RequestEntity> request = RequestEntity.post(url) .contentType(MediaType.APPLICATION_JSON) @@ -368,7 +362,6 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati } private void createXml(String requestUrl) throws Exception { - RestTemplate restTemplate = new RestTemplate(); URI url = new URI(requestUrl); People people = new People(); people.getPerson().add(new Person("Robert")); @@ -413,9 +406,16 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @Bean public ResponseBodyResultHandler responseBodyResultHandler() { - List> encoders = Arrays.asList(new ByteBufferEncoder(), - new StringEncoder(), new JacksonJsonEncoder()); - ResponseBodyResultHandler resultHandler = new ResponseBodyResultHandler(encoders, conversionService()); + List> converters = + Arrays.asList(new ResourceHttpMessageConverter(), + new CodecHttpMessageConverter( + new ByteBufferEncoder(), new ByteBufferDecoder()), + new CodecHttpMessageConverter(new StringEncoder(), + new StringDecoder()), + new CodecHttpMessageConverter( + new JacksonJsonEncoder(), new JacksonJsonDecoder())); + ResponseBodyResultHandler resultHandler = + new ResponseBodyResultHandler(converters, conversionService()); resultHandler.setOrder(1); return resultHandler; } @@ -626,6 +626,12 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati return Mono.just("Recovered from error: " + ex.getMessage()); } + @RequestMapping("/resource") + @ResponseBody + public Resource resource() { + return new ClassPathResource("spring.png", ZeroCopyIntegrationTests.class); + } + //TODO add mixed and T request mappings tests } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java index f290593a8d..38106e7465 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java @@ -24,6 +24,7 @@ import org.reactivestreams.Publisher; import org.springframework.core.ResolvableType; import org.springframework.core.codec.support.StringEncoder; import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.http.converter.reactive.CodecHttpMessageConverter; import org.springframework.ui.ExtendedModelMap; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.method.HandlerMethod; @@ -41,7 +42,7 @@ public class ResponseBodyResultHandlerTests { @Test public void supports() throws NoSuchMethodException { ResponseBodyResultHandler handler = new ResponseBodyResultHandler(Collections.singletonList( - new StringEncoder()), + new CodecHttpMessageConverter(new StringEncoder(), null)), new DefaultConversionService()); TestController controller = new TestController(); From dd607d3e534e26680d46bef175e81633f1a247a0 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 20 Apr 2016 13:31:46 -0400 Subject: [PATCH 321/498] BadRequestStatusException -> ServerWebInputException The renaming makes it clear this exception is for use where 400 error applies within a Spring web application where the error may be associated with a MethodParameter, a BindingResult, and so on. There is no need for BadRequestStatusException which can be expressed with ResponseStatusException(HttpStatus.BAD_REQUEST, "reason"). --- .../method/RequestMappingInfoHandlerMapping.java | 6 +++--- ...usException.java => ServerWebInputException.java} | 12 +++++++----- .../RequestMappingInfoHandlerMappingTests.java | 4 ++-- 3 files changed, 12 insertions(+), 10 deletions(-) rename spring-web-reactive/src/main/java/org/springframework/web/server/{BadRequestStatusException.java => ServerWebInputException.java} (76%) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java index 640fed7eda..8712b1c69b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java @@ -43,7 +43,7 @@ import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerMapping; import org.springframework.web.reactive.result.condition.ParamsRequestCondition; -import org.springframework.web.server.BadRequestStatusException; +import org.springframework.web.server.ServerWebInputException; import org.springframework.web.server.MethodNotAllowedException; import org.springframework.web.server.NotAcceptableStatusException; import org.springframework.web.server.ServerWebExchange; @@ -203,7 +203,7 @@ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMe * and HTTP method but not by consumable media types * @throws NotAcceptableStatusException if there are matches by URL and HTTP * method but not by producible media types - * @throws BadRequestStatusException if there are matches by URL and HTTP + * @throws ServerWebInputException if there are matches by URL and HTTP * method but not by query parameter conditions */ @Override @@ -278,7 +278,7 @@ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMe .collect(Collectors.toMap(Entry::getKey, entry -> entry.getValue().toArray(new String[entry.getValue().size()])) ); - throw new BadRequestStatusException("Unsatisfied query parameter conditions: " + + throw new ServerWebInputException("Unsatisfied query parameter conditions: " + paramConditions + ", actual parameters: " + params); } else { diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/BadRequestStatusException.java b/spring-web-reactive/src/main/java/org/springframework/web/server/ServerWebInputException.java similarity index 76% rename from spring-web-reactive/src/main/java/org/springframework/web/server/BadRequestStatusException.java rename to spring-web-reactive/src/main/java/org/springframework/web/server/ServerWebInputException.java index 5f469b0dd7..26af9f6a95 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/BadRequestStatusException.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/ServerWebInputException.java @@ -21,11 +21,13 @@ import org.springframework.core.MethodParameter; import org.springframework.http.HttpStatus; /** - * Exception for errors that fit response status 400 (bad request). + * Exception for errors that fit response status 400 (bad request) for use in + * Spring Web applications. The exception provides additional fields (e.g. + * an optional {@link MethodParameter} if related to the error). * * @author Rossen Stoyanchev */ -public class BadRequestStatusException extends ResponseStatusException { +public class ServerWebInputException extends ResponseStatusException { private final MethodParameter parameter; @@ -33,21 +35,21 @@ public class BadRequestStatusException extends ResponseStatusException { /** * Constructor with an explanation only. */ - public BadRequestStatusException(String reason) { + public ServerWebInputException(String reason) { this(reason, null); } /** * Constructor for a 400 error linked to a specific {@code MethodParameter}. */ - public BadRequestStatusException(String reason, MethodParameter parameter) { + public ServerWebInputException(String reason, MethodParameter parameter) { this(reason, parameter, null); } /** * Constructor for a 400 error with a root cause. */ - public BadRequestStatusException(String reason, MethodParameter parameter, Throwable cause) { + public ServerWebInputException(String reason, MethodParameter parameter, Throwable cause) { super(HttpStatus.BAD_REQUEST, reason, cause); this.parameter = parameter; } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java index 2c26457328..ada1c7a058 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java @@ -50,7 +50,7 @@ import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerMapping; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.result.method.RequestMappingInfo.BuilderConfiguration; -import org.springframework.web.server.BadRequestStatusException; +import org.springframework.web.server.ServerWebInputException; import org.springframework.web.server.MethodNotAllowedException; import org.springframework.web.server.NotAcceptableStatusException; import org.springframework.web.server.ServerWebExchange; @@ -208,7 +208,7 @@ public class RequestMappingInfoHandlerMappingTests { public void getHandlerUnsatisfiedServletRequestParameterException() throws Exception { ServerWebExchange exchange = createExchange(HttpMethod.GET, "/params"); Mono mono = this.handlerMapping.getHandler(exchange); - assertError(mono, BadRequestStatusException.class, ex -> { + assertError(mono, ServerWebInputException.class, ex -> { assertEquals(ex.getReason(), "Unsatisfied query parameter conditions: " + "[[bar=baz], [foo=bar]], actual parameters: {}"); }); From df3cf69a963081045559468fd457497935af8a62 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Thu, 21 Apr 2016 12:23:44 +0200 Subject: [PATCH 322/498] Polishing --- .../core/codec/support/Jaxb2Decoder.java | 5 +- .../io/buffer/support/DataBufferUtils.java | 27 +------ .../util/CollectionUtils2.java | 60 +++++++++++++++ .../xml/AbstractXMLEventReader.java} | 74 ++++++++----------- .../util/xml/ListBasedXMLEventReader.java | 72 ++++++++++++++++++ .../springframework/util/xml/StaxUtils2.java | 38 ++++++++++ .../xml/ListBasedXMLEventReaderTests.java | 67 +++++++++++++++++ 7 files changed, 273 insertions(+), 70 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/util/CollectionUtils2.java rename spring-web-reactive/src/main/java/org/springframework/{core/codec/support/ListBasedXMLEventReader.java => util/xml/AbstractXMLEventReader.java} (75%) create mode 100644 spring-web-reactive/src/main/java/org/springframework/util/xml/ListBasedXMLEventReader.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/util/xml/StaxUtils2.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/util/xml/ListBasedXMLEventReaderTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Decoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Decoder.java index 42287d10e7..bcd86ff262 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Decoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Decoder.java @@ -40,6 +40,7 @@ import org.springframework.core.io.buffer.DataBuffer; import org.springframework.util.ClassUtils; import org.springframework.util.MimeType; import org.springframework.util.MimeTypeUtils; +import org.springframework.util.xml.StaxUtils2; /** * Decode from a bytes stream of XML elements to a stream of {@code Object} (POJO). @@ -193,10 +194,10 @@ public class Jaxb2Decoder extends AbstractDecoder { }); } - private Object unmarshal(List eventFlux, Class outputClass) { + private Object unmarshal(List events, Class outputClass) { try { Unmarshaller unmarshaller = this.jaxbContexts.createUnmarshaller(outputClass); - XMLEventReader eventReader = new ListBasedXMLEventReader(eventFlux); + XMLEventReader eventReader = StaxUtils2.createXMLEventReader(events); if (outputClass.isAnnotationPresent(XmlRootElement.class)) { return unmarshaller.unmarshal(eventReader); } diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java index 16fd7e5b8e..9e495aa3aa 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java @@ -23,7 +23,6 @@ import java.io.SequenceInputStream; import java.nio.ByteBuffer; import java.nio.channels.ReadableByteChannel; import java.util.Enumeration; -import java.util.Iterator; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Consumer; @@ -36,6 +35,7 @@ import reactor.core.subscriber.SubscriberWithContext; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferAllocator; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils2; /**i * Utility class for working with {@link DataBuffer}s. @@ -67,7 +67,7 @@ public abstract class DataBufferUtils { toIterable(1); Enumeration enumeration = - new IteratorEnumeration(streams.iterator()); + CollectionUtils2.toEnumeration(streams.iterator()); return new SequenceInputStream(enumeration); } @@ -169,29 +169,6 @@ public abstract class DataBufferUtils { }); } - /** - * Enumeration wrapping an Iterator. - */ - // TODO: move to CollectionUtils when we merge with Spring Framework? - private static class IteratorEnumeration implements Enumeration { - - private final Iterator iterator; - - public IteratorEnumeration(Iterator iterator) { - this.iterator = iterator; - } - - @Override - public boolean hasMoreElements() { - return this.iterator.hasNext(); - } - - @Override - public T nextElement() { - return this.iterator.next(); - } - } - private static class ReadableByteChannelConsumer implements Consumer> { diff --git a/spring-web-reactive/src/main/java/org/springframework/util/CollectionUtils2.java b/spring-web-reactive/src/main/java/org/springframework/util/CollectionUtils2.java new file mode 100644 index 0000000000..45f2c0983a --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/util/CollectionUtils2.java @@ -0,0 +1,60 @@ +/* + * 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.util; + +import java.util.Enumeration; +import java.util.Iterator; + +/** + * TODO: to be merged with {@link CollectionUtils} + * @author Arjen Poutsma + */ +public abstract class CollectionUtils2 { + + /** + * Adapt an iterator to an enumeration. + * @param iterator the iterator + * @return the enumeration + */ + public static Enumeration toEnumeration(Iterator iterator) { + return new IteratorEnumeration(iterator); + } + + /** + * Enumeration wrapping an Iterator. + */ + private static class IteratorEnumeration implements Enumeration { + + private final Iterator iterator; + + public IteratorEnumeration(Iterator iterator) { + this.iterator = iterator; + } + + @Override + public boolean hasMoreElements() { + return this.iterator.hasNext(); + } + + @Override + public T nextElement() { + return this.iterator.next(); + } + } + + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ListBasedXMLEventReader.java b/spring-web-reactive/src/main/java/org/springframework/util/xml/AbstractXMLEventReader.java similarity index 75% rename from spring-web-reactive/src/main/java/org/springframework/core/codec/support/ListBasedXMLEventReader.java rename to spring-web-reactive/src/main/java/org/springframework/util/xml/AbstractXMLEventReader.java index 95a22c9080..5342d932ff 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ListBasedXMLEventReader.java +++ b/spring-web-reactive/src/main/java/org/springframework/util/xml/AbstractXMLEventReader.java @@ -14,9 +14,8 @@ * limitations under the License. */ -package org.springframework.core.codec.support; +package org.springframework.util.xml; -import java.util.List; import java.util.NoSuchElementException; import javax.xml.stream.XMLEventReader; import javax.xml.stream.XMLStreamConstants; @@ -24,60 +23,26 @@ import javax.xml.stream.XMLStreamException; import javax.xml.stream.events.Characters; import javax.xml.stream.events.XMLEvent; -import org.springframework.util.Assert; import org.springframework.util.ClassUtils; /** - * TODO: move to org.springframework.util.xml when merging, hidden behind StaxUtils - * + * Abstract base class for {@code XMLEventReader}s. * @author Arjen Poutsma */ -class ListBasedXMLEventReader implements XMLEventReader { +abstract class AbstractXMLEventReader implements XMLEventReader { - private final XMLEvent[] events; - - private int cursor = 0; - - public ListBasedXMLEventReader(List events) { - Assert.notNull(events, "'events' must not be null"); - this.events = events.toArray(new XMLEvent[events.size()]); - } + private boolean closed; @Override - public boolean hasNext() { - Assert.notNull(events, "'events' must not be null"); - return cursor != events.length; - } - - @Override - public XMLEvent nextEvent() { - if (cursor < events.length) { - return events[cursor++]; + public Object next() { + try { + return nextEvent(); } - else { + catch (XMLStreamException ex) { throw new NoSuchElementException(); } } - @Override - public XMLEvent peek() { - if (cursor < events.length) { - return events[cursor]; - } - else { - return null; - } - } - - @Override - public Object next() { - return nextEvent(); - } - - /** - * Throws an {@code UnsupportedOperationException} when called. - * @throws UnsupportedOperationException when called - */ @Override public void remove() { throw new UnsupportedOperationException( @@ -86,6 +51,7 @@ class ListBasedXMLEventReader implements XMLEventReader { @Override public String getElementText() throws XMLStreamException { + checkIfClosed(); if (!peek().isStartElement()) { throw new XMLStreamException("Not at START_ELEMENT"); } @@ -110,6 +76,7 @@ class ListBasedXMLEventReader implements XMLEventReader { @Override public XMLEvent nextTag() throws XMLStreamException { + checkIfClosed(); while (true) { XMLEvent event = nextEvent(); switch (event.getEventType()) { @@ -145,7 +112,28 @@ class ListBasedXMLEventReader implements XMLEventReader { throw new IllegalArgumentException("Property not supported: [" + name + "]"); } + /** + * Returns {@code true} if closed; {@code false} otherwise. + * @see #close() + */ + protected boolean isClosed() { + return closed; + } + + /** + * Checks if the reader is closed, and throws a {@code XMLStreamException} if so. + * @throws XMLStreamException if the reader is closed + * @see #close() + * @see #isClosed() + */ + protected void checkIfClosed() throws XMLStreamException { + if (isClosed()) { + throw new XMLStreamException("XMLEventReader has been closed"); + } + } + @Override public void close() { + closed = true; } } diff --git a/spring-web-reactive/src/main/java/org/springframework/util/xml/ListBasedXMLEventReader.java b/spring-web-reactive/src/main/java/org/springframework/util/xml/ListBasedXMLEventReader.java new file mode 100644 index 0000000000..a8d2345cfa --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/util/xml/ListBasedXMLEventReader.java @@ -0,0 +1,72 @@ +/* + * 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.util.xml; + +import java.util.Collections; +import java.util.List; +import java.util.NoSuchElementException; +import javax.xml.stream.events.XMLEvent; + +import org.springframework.util.Assert; + +/** + * Implementation of {@code XMLEventReader} based on a list of {@link XMLEvent}s. + * + * @author Arjen Poutsma + */ +class ListBasedXMLEventReader extends AbstractXMLEventReader { + + private final List events; + + private int cursor = 0; + + public ListBasedXMLEventReader(List events) { + Assert.notNull(events, "'events' must not be null"); + this.events = Collections.unmodifiableList(events); + } + + @Override + public boolean hasNext() { + return cursor != events.size(); + } + + @Override + public XMLEvent nextEvent() { + if (cursor < events.size()) { + return events.get(cursor++); + } + else { + throw new NoSuchElementException(); + } + } + + @Override + public XMLEvent peek() { + if (cursor < events.size()) { + return events.get(cursor); + } + else { + return null; + } + } + + @Override + public void close() { + super.close(); + this.events.clear(); + } +} diff --git a/spring-web-reactive/src/main/java/org/springframework/util/xml/StaxUtils2.java b/spring-web-reactive/src/main/java/org/springframework/util/xml/StaxUtils2.java new file mode 100644 index 0000000000..7d4c2c3f8f --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/util/xml/StaxUtils2.java @@ -0,0 +1,38 @@ +/* + * 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.util.xml; + +import java.util.List; +import javax.xml.stream.XMLEventReader; +import javax.xml.stream.events.XMLEvent; + +/** + * TODO: to be merged with {@link StaxUtils}. + * @author Arjen Poutsma + */ +public abstract class StaxUtils2 { + + /** + * Create a {@link XMLEventReader} from the given list of {@link XMLEvent}. + * @param events the list of {@link XMLEvent}s. + * @return an {@code XMLEventReader} that reads from the given events + */ + public static XMLEventReader createXMLEventReader(List events) { + return new ListBasedXMLEventReader(events); + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/util/xml/ListBasedXMLEventReaderTests.java b/spring-web-reactive/src/test/java/org/springframework/util/xml/ListBasedXMLEventReaderTests.java new file mode 100644 index 0000000000..30005c7f47 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/util/xml/ListBasedXMLEventReaderTests.java @@ -0,0 +1,67 @@ +/* + * 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.util.xml; + +import java.io.StringReader; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.List; +import javax.xml.stream.XMLEventReader; +import javax.xml.stream.XMLEventWriter; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.events.XMLEvent; + +import org.junit.Test; + +import static org.custommonkey.xmlunit.XMLAssert.assertXMLEqual; + +/** + * @author Arjen Poutsma + */ +public class ListBasedXMLEventReaderTests { + + private final XMLInputFactory inputFactory = XMLInputFactory.newFactory(); + + private final XMLOutputFactory outputFactory = XMLOutputFactory.newFactory(); + + @Test + public void standard() throws Exception { + String xml = "baz"; + List events = readEvents(xml); + + ListBasedXMLEventReader reader = new ListBasedXMLEventReader(events); + + StringWriter resultWriter = new StringWriter(); + XMLEventWriter writer = this.outputFactory.createXMLEventWriter(resultWriter); + writer.add(reader); + + assertXMLEqual(xml, resultWriter.toString()); + } + + private List readEvents(String xml) throws XMLStreamException { + XMLEventReader reader = + this.inputFactory.createXMLEventReader(new StringReader(xml)); + List events = new ArrayList<>(); + while (reader.hasNext()) { + events.add(reader.nextEvent()); + } + return events; + } + +} \ No newline at end of file From 53f8b84bcc1fa5411c6e52870f61335e58d60601 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Thu, 21 Apr 2016 12:46:57 +0200 Subject: [PATCH 323/498] Polishing --- .../io/buffer/support/DataBufferUtils.java | 48 ++----------------- 1 file changed, 3 insertions(+), 45 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java index 9e495aa3aa..a8c304a150 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java @@ -21,6 +21,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.SequenceInputStream; import java.nio.ByteBuffer; +import java.nio.channels.Channels; import java.nio.channels.ReadableByteChannel; import java.util.Enumeration; import java.util.concurrent.atomic.AtomicLong; @@ -102,8 +103,8 @@ public abstract class DataBufferUtils { Assert.notNull(inputStream, "'inputStream' must not be null"); Assert.notNull(allocator, "'allocator' must not be null"); - return Flux.create(new InputStreamConsumer(allocator, bufferSize), - subscriber -> inputStream, closeConsumer()); + ReadableByteChannel channel = Channels.newChannel(inputStream); + return read(channel, allocator, bufferSize); } @SuppressWarnings("unchecked") @@ -212,47 +213,4 @@ public abstract class DataBufferUtils { } } - private static class InputStreamConsumer - implements Consumer> { - - private final DataBufferAllocator allocator; - - private final int chunkSize; - - public InputStreamConsumer(DataBufferAllocator allocator, int chunkSize) { - this.allocator = allocator; - this.chunkSize = chunkSize; - } - - @Override - public void accept(SubscriberWithContext sub) { - try { - byte[] bytes = new byte[chunkSize]; - int read; - InputStream is = sub.context(); - if ((read = is.read(bytes)) > 0) { - boolean release = true; - DataBuffer dataBuffer = this.allocator.allocateBuffer(read); - try { - dataBuffer.write(bytes, 0, read); - release = false; - sub.onNext(dataBuffer); - } - finally { - if (release) { - // TODO: release buffer when we have PooledDataBuffer - } - } - } - else { - sub.onComplete(); - } - } - catch (IOException ex) { - sub.onError(ex); - } - - } - } - } From a23f792052d664a82bc6a405380b67666a14f1a6 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 21 Apr 2016 17:02:15 -0400 Subject: [PATCH 324/498] Add SimpleUrlHandlerMapping This commit replaces the temporary SimpleUrlHandlerMapping with a full-featured one. --- .../web/reactive/HandlerMapping.java | 11 + .../handler/AbstractUrlHandlerMapping.java | 257 ++++++++++++++++++ .../handler/SimpleUrlHandlerMapping.java | 127 +++++++-- .../handler/SimpleUrlHandlerMappingTests.java | 155 +++++++++++ .../result/WebHandlerIntegrationTests.java | 6 +- .../web/reactive/handler/map.xml | 40 +++ 6 files changed, 566 insertions(+), 30 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/AbstractUrlHandlerMapping.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingTests.java create mode 100644 spring-web-reactive/src/test/resources/org/springframework/web/reactive/handler/map.xml diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerMapping.java index 6253cb8020..45d33469cb 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerMapping.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerMapping.java @@ -39,6 +39,17 @@ public interface HandlerMapping { */ String BEST_MATCHING_PATTERN_ATTRIBUTE = HandlerMapping.class.getName() + ".bestMatchingPattern"; + /** + * Name of the {@link ServerWebExchange} attribute that contains the path + * within the handler mapping, in case of a pattern match, or the full + * relevant URI (typically within the DispatcherServlet's mapping) else. + *

    Note: This attribute is not required to be supported by all + * HandlerMapping implementations. URL-based HandlerMappings will + * typically support it, but handlers should not necessarily expect + * this request attribute to be present in all scenarios. + */ + String PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE = HandlerMapping.class.getName() + ".pathWithinHandlerMapping"; + /** * Name of the {@link ServerWebExchange} attribute that contains the URI * templates map, mapping variable names to values. diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/AbstractUrlHandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/AbstractUrlHandlerMapping.java new file mode 100644 index 0000000000..1711e43af2 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/AbstractUrlHandlerMapping.java @@ -0,0 +1,257 @@ +/* + * 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.web.reactive.handler; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import reactor.core.publisher.Mono; + +import org.springframework.beans.BeansException; +import org.springframework.util.Assert; +import org.springframework.web.server.ServerWebExchange; + +/** + * Abstract base class for URL-mapped + * {@link org.springframework.web.reactive.HandlerMapping} implementations. + * + *

    Supports direct matches, e.g. a registered "/test" matches "/test", and + * various Ant-style pattern matches, e.g. a registered "/t*" pattern matches + * both "/test" and "/team", "/test/*" matches all paths under "/test", + * "/test/**" matches all paths below "/test". For details, see the + * {@link org.springframework.util.AntPathMatcher AntPathMatcher} javadoc. + * + *

    Will search all path patterns to find the most exact match for the + * current request path. The most exact match is defined as the longest + * path pattern that matches the current request path. + * + * @author Rossen Stoyanchev + */ +public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping { + + private boolean useTrailingSlashMatch = false; + + private boolean lazyInitHandlers = false; + + private final Map handlerMap = new LinkedHashMap<>(); + + + /** + * Whether to match to URLs irrespective of the presence of a trailing slash. + * If enabled a URL pattern such as "/users" also matches to "/users/". + *

    The default value is {@code false}. + */ + public void setUseTrailingSlashMatch(boolean useTrailingSlashMatch) { + this.useTrailingSlashMatch = useTrailingSlashMatch; + } + + /** + * Whether to match to URLs irrespective of the presence of a trailing slash. + */ + public boolean useTrailingSlashMatch() { + return this.useTrailingSlashMatch; + } + + /** + * Set whether to lazily initialize handlers. Only applicable to + * singleton handlers, as prototypes are always lazily initialized. + * Default is "false", as eager initialization allows for more efficiency + * through referencing the controller objects directly. + *

    If you want to allow your controllers to be lazily initialized, + * make them "lazy-init" and set this flag to true. Just making them + * "lazy-init" will not work, as they are initialized through the + * references from the handler mapping in this case. + */ + public void setLazyInitHandlers(boolean lazyInitHandlers) { + this.lazyInitHandlers = lazyInitHandlers; + } + + /** + * Return the registered handlers as an unmodifiable Map, with the registered path + * as key and the handler object (or handler bean name in case of a lazy-init handler) + * as value. + */ + public final Map getHandlerMap() { + return Collections.unmodifiableMap(this.handlerMap); + } + + + @Override + public Mono getHandler(ServerWebExchange exchange) { + String lookupPath = getPathHelper().getLookupPathForRequest(exchange); + Object handler = null; + try { + handler = lookupHandler(lookupPath, exchange); + } + catch (Exception ex) { + return Mono.error(ex); + } + + if (handler != null && logger.isDebugEnabled()) { + logger.debug("Mapping [" + lookupPath + "] to " + handler); + } + else if (handler == null && logger.isTraceEnabled()) { + logger.trace("No handler mapping found for [" + lookupPath + "]"); + } + + return Mono.justOrEmpty(handler); + } + + /** + * Look up a handler instance for the given URL path. + * + *

    Supports direct matches, e.g. a registered "/test" matches "/test", + * and various Ant-style pattern matches, e.g. a registered "/t*" matches + * both "/test" and "/team". For details, see the AntPathMatcher class. + * + *

    Looks for the most exact pattern, where most exact is defined as + * the longest path pattern. + * + * @param urlPath URL the bean is mapped to + * @param exchange the current exchange + * @return the associated handler instance, or {@code null} if not found + * @see org.springframework.util.AntPathMatcher + */ + protected Object lookupHandler(String urlPath, ServerWebExchange exchange) throws Exception { + // Direct match? + Object handler = this.handlerMap.get(urlPath); + if (handler != null) { + return handleMatch(handler, urlPath, urlPath, exchange); + } + // Pattern match? + List matches = new ArrayList<>(); + for (String pattern : this.handlerMap.keySet()) { + if (getPathMatcher().match(pattern, urlPath)) { + matches.add(pattern); + } + else if (useTrailingSlashMatch()) { + if (!pattern.endsWith("/") && getPathMatcher().match(pattern + "/", urlPath)) { + matches.add(pattern +"/"); + } + } + } + String bestMatch = null; + Comparator comparator = getPathMatcher().getPatternComparator(urlPath); + if (!matches.isEmpty()) { + Collections.sort(matches, comparator); + if (logger.isDebugEnabled()) { + logger.debug("Matching patterns for request [" + urlPath + "] are " + matches); + } + bestMatch = matches.get(0); + } + if (bestMatch != null) { + handler = this.handlerMap.get(bestMatch); + if (handler == null) { + Assert.isTrue(bestMatch.endsWith("/")); + handler = this.handlerMap.get(bestMatch.substring(0, bestMatch.length() - 1)); + } + String pathWithinMapping = getPathMatcher().extractPathWithinPattern(bestMatch, urlPath); + return handleMatch(handler, bestMatch, pathWithinMapping, exchange); + } + // No handler found... + return null; + } + + private Object handleMatch(Object handler, String bestMatch, String pathWithinMapping, + ServerWebExchange exchange) throws Exception { + + // Bean name or resolved handler? + if (handler instanceof String) { + String handlerName = (String) handler; + handler = getApplicationContext().getBean(handlerName); + } + + validateHandler(handler, exchange); + + exchange.getAttributes().put(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, pathWithinMapping); + exchange.getAttributes().put(BEST_MATCHING_PATTERN_ATTRIBUTE, bestMatch); + + return handler; + } + + /** + * Validate the given handler against the current request. + *

    The default implementation is empty. Can be overridden in subclasses, + * for example to enforce specific preconditions expressed in URL mappings. + * @param handler the handler object to validate + * @param exchange current exchange + * @throws Exception if validation failed + */ + @SuppressWarnings("UnusedParameters") + protected void validateHandler(Object handler, ServerWebExchange exchange) throws Exception { + } + + /** + * Register the specified handler for the given URL paths. + * @param urlPaths the URLs that the bean should be mapped to + * @param beanName the name of the handler bean + * @throws BeansException if the handler couldn't be registered + * @throws IllegalStateException if there is a conflicting handler registered + */ + protected void registerHandler(String[] urlPaths, String beanName) throws BeansException, IllegalStateException { + Assert.notNull(urlPaths, "URL path array must not be null"); + for (String urlPath : urlPaths) { + registerHandler(urlPath, beanName); + } + } + + /** + * Register the specified handler for the given URL path. + * @param urlPath the URL the bean should be mapped to + * @param handler the handler instance or handler bean name String + * (a bean name will automatically be resolved into the corresponding handler bean) + * @throws BeansException if the handler couldn't be registered + * @throws IllegalStateException if there is a conflicting handler registered + */ + protected void registerHandler(String urlPath, Object handler) throws BeansException, IllegalStateException { + Assert.notNull(urlPath, "URL path must not be null"); + Assert.notNull(handler, "Handler object must not be null"); + Object resolvedHandler = handler; + + // Eagerly resolve handler if referencing singleton via name. + if (!this.lazyInitHandlers && handler instanceof String) { + String handlerName = (String) handler; + if (getApplicationContext().isSingleton(handlerName)) { + resolvedHandler = getApplicationContext().getBean(handlerName); + } + } + + Object mappedHandler = this.handlerMap.get(urlPath); + if (mappedHandler != null) { + if (mappedHandler != resolvedHandler) { + throw new IllegalStateException( + "Cannot map " + getHandlerDescription(handler) + " to URL path [" + urlPath + + "]: There is already " + getHandlerDescription(mappedHandler) + " mapped."); + } + } + else { + this.handlerMap.put(urlPath, resolvedHandler); + if (logger.isInfoEnabled()) { + logger.info("Mapped URL path [" + urlPath + "] onto " + getHandlerDescription(handler)); + } + } + } + + private String getHandlerDescription(Object handler) { + return "handler " + (handler instanceof String ? "'" + handler + "'" : "of type [" + handler.getClass() + "]"); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMapping.java index 0f1709ad85..fc96cbe07e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMapping.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMapping.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -16,41 +16,116 @@ package org.springframework.web.reactive.handler; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; +import java.util.Properties; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -import org.springframework.web.reactive.HandlerMapping; -import org.springframework.web.server.ServerWebExchange; +import org.springframework.beans.BeansException; +import org.springframework.util.CollectionUtils; /** + * Implementation of the {@link org.springframework.web.reactive.HandlerMapping} + * interface to map from URLs to request handler beans. Supports both mapping + * to bean instances and mapping to bean names; the latter is required for + * non-singleton handlers. + * + *

    The "urlMap" property is suitable for populating the handler map with + * bean instances. Mappings to bean names can be set via the "mappings" + * property, in a form accepted by the {@code java.util.Properties} class, + * like as follows: + * + *

    + * /welcome.html=ticketController
    + * /show.html=ticketController
    + * 
    + * + *

    The syntax is {@code PATH=HANDLER_BEAN_NAME}. If the path doesn't begin + * with a slash, one is prepended. + * + *

    Supports direct matches, e.g. a registered "/test" matches "/test", and + * various Ant-style pattern matches, e.g. a registered "/t*" pattern matches + * both "/test" and "/team", "/test/*" matches all paths under "/test", + * "/test/**" matches all paths below "/test". For details, see the + * {@link org.springframework.util.AntPathMatcher AntPathMatcher} javadoc. + * * @author Rossen Stoyanchev */ -public class SimpleUrlHandlerMapping implements HandlerMapping { +public class SimpleUrlHandlerMapping extends AbstractUrlHandlerMapping { - private final Map handlerMap = new HashMap<>(); + private final Map urlMap = new LinkedHashMap<>(); - public void setHandlers(Map handlers) { - this.handlerMap.clear(); - if (handlers != null) { - this.handlerMap.putAll(handlers); + /** + * Map URL paths to handler bean names. + * This is the typical way of configuring this HandlerMapping. + *

    Supports direct URL matches and Ant-style pattern matches. For syntax + * details, see the {@link org.springframework.util.AntPathMatcher} javadoc. + * @param mappings properties with URLs as keys and bean names as values + * @see #setUrlMap + */ + public void setMappings(Properties mappings) { + CollectionUtils.mergePropertiesIntoMap(mappings, this.urlMap); + } + + /** + * Set a Map with URL paths as keys and handler beans (or handler bean names) + * as values. Convenient for population with bean references. + *

    Supports direct URL matches and Ant-style pattern matches. For syntax + * details, see the {@link org.springframework.util.AntPathMatcher} javadoc. + * @param urlMap map with URLs as keys and beans as values + * @see #setMappings + */ + public void setUrlMap(Map urlMap) { + this.urlMap.putAll(urlMap); + } + + /** + * Allow Map access to the URL path mappings, with the option to add or + * override specific entries. + *

    Useful for specifying entries directly, for example via "urlMap[myKey]". + * This is particularly useful for adding or overriding entries in child + * bean definitions. + */ + public Map getUrlMap() { + return this.urlMap; + } + + + /** + * Calls the {@link #registerHandlers} method in addition to the + * superclass's initialization. + */ + @Override + public void initApplicationContext() throws BeansException { + super.initApplicationContext(); + registerHandlers(this.urlMap); + } + + /** + * Register all handlers specified in the URL map for the corresponding paths. + * @param urlMap Map with URL paths as keys and handler beans or bean names as values + * @throws BeansException if a handler couldn't be registered + * @throws IllegalStateException if there is a conflicting handler registered + */ + protected void registerHandlers(Map urlMap) throws BeansException { + if (urlMap.isEmpty()) { + logger.warn("Neither 'urlMap' nor 'mappings' set on SimpleUrlHandlerMapping"); + } + else { + for (Map.Entry entry : urlMap.entrySet()) { + String url = entry.getKey(); + Object handler = entry.getValue(); + // Prepend with slash if not already present. + if (!url.startsWith("/")) { + url = "/" + url; + } + // Remove whitespace from handler bean name. + if (handler instanceof String) { + handler = ((String) handler).trim(); + } + registerHandler(url, handler); + } } } - - @Override - public Mono getHandler(ServerWebExchange exchange) { - return Flux.create(subscriber -> { - String path = exchange.getRequest().getURI().getPath(); - Object handler = this.handlerMap.get(path); - if (handler != null) { - subscriber.onNext(handler); - } - subscriber.onComplete(); - }).next(); - } - } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingTests.java new file mode 100644 index 0000000000..eccda562f6 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingTests.java @@ -0,0 +1,155 @@ +/* + * 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.web.reactive.handler; + +import java.net.URI; +import java.net.URISyntaxException; + +import org.junit.Test; + +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.http.HttpMethod; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.web.reactive.HandlerMapping; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.WebSessionManager; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.mockito.Mockito.mock; +import static org.springframework.web.reactive.HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE; + +/** + * Unit tests for {@link SimpleUrlHandlerMapping}. + * + * @author Rossen Stoyanchev + */ +public class SimpleUrlHandlerMappingTests { + + @Test + public void handlerMappingJavaConfig() throws Exception { + AnnotationConfigApplicationContext wac = new AnnotationConfigApplicationContext(); + wac.register(WebConfig.class); + wac.refresh(); + + HandlerMapping handlerMapping = (HandlerMapping) wac.getBean("handlerMapping"); + Object mainController = wac.getBean("mainController"); + Object otherController = wac.getBean("otherController"); + + testUrl("/welcome.html", mainController, handlerMapping, "/welcome.html"); + testUrl("/welcome.x", otherController, handlerMapping, "welcome.x"); + testUrl("/welcome/", otherController, handlerMapping, "welcome"); + testUrl("/show.html", mainController, handlerMapping, "/show.html"); + testUrl("/bookseats.html", mainController, handlerMapping, "/bookseats.html"); + } + + @Test + public void handlerMappingXmlConfig() throws Exception { + ClassPathXmlApplicationContext wac = new ClassPathXmlApplicationContext("map.xml", getClass()); + wac.refresh(); + + HandlerMapping handlerMapping = wac.getBean("mapping", HandlerMapping.class); + Object mainController = wac.getBean("mainController"); + + testUrl("/pathmatchingTest.html", mainController, handlerMapping, "pathmatchingTest.html"); + testUrl("welcome.html", null, handlerMapping, null); + testUrl("/pathmatchingAA.html", mainController, handlerMapping, "pathmatchingAA.html"); + testUrl("/pathmatchingA.html", null, handlerMapping, null); + testUrl("/administrator/pathmatching.html", mainController, handlerMapping, "pathmatching.html"); + testUrl("/administrator/test/pathmatching.html", mainController, handlerMapping, "test/pathmatching.html"); + testUrl("/administratort/pathmatching.html", null, handlerMapping, null); + testUrl("/administrator/another/bla.xml", mainController, handlerMapping, "/administrator/another/bla.xml"); + testUrl("/administrator/another/bla.gif", null, handlerMapping, null); + testUrl("/administrator/test/testlastbit", mainController, handlerMapping, "test/testlastbit"); + testUrl("/administrator/test/testla", null, handlerMapping, null); + testUrl("/administrator/testing/longer/bla", mainController, handlerMapping, "bla"); + testUrl("/administrator/testing/longer2/notmatching/notmatching", null, handlerMapping, null); + testUrl("/shortpattern/testing/toolong", null, handlerMapping, null); + testUrl("/XXpathXXmatching.html", mainController, handlerMapping, "XXpathXXmatching.html"); + testUrl("/pathXXmatching.html", mainController, handlerMapping, "pathXXmatching.html"); + testUrl("/XpathXXmatching.html", null, handlerMapping, null); + testUrl("/XXpathmatching.html", null, handlerMapping, null); + testUrl("/show12.html", mainController, handlerMapping, "show12.html"); + testUrl("/show123.html", mainController, handlerMapping, "/show123.html"); + testUrl("/show1.html", mainController, handlerMapping, "show1.html"); + testUrl("/reallyGood-test-is-this.jpeg", mainController, handlerMapping, "reallyGood-test-is-this.jpeg"); + testUrl("/reallyGood-tst-is-this.jpeg", null, handlerMapping, null); + testUrl("/testing/test.jpeg", mainController, handlerMapping, "testing/test.jpeg"); + testUrl("/testing/test.jpg", null, handlerMapping, null); + testUrl("/anotherTest", mainController, handlerMapping, "anotherTest"); + testUrl("/stillAnotherTest", null, handlerMapping, null); + testUrl("outofpattern*ye", null, handlerMapping, null); + testUrl("/test%26t%20est/path%26m%20atching.html", null, handlerMapping, null); + + } + + private void testUrl(String url, Object bean, HandlerMapping handlerMapping, String pathWithinMapping) + throws URISyntaxException { + + ServerWebExchange exchange = createExchange(url); + Object actual = handlerMapping.getHandler(exchange).get(); + if (bean != null) { + assertNotNull(actual); + assertSame(bean, actual); + //noinspection OptionalGetWithoutIsPresent + assertEquals(pathWithinMapping, exchange.getAttribute(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE).get()); + } + else { + assertNull(actual); + } + } + + private ServerWebExchange createExchange(String path) throws URISyntaxException { + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI(path)); + WebSessionManager sessionManager = mock(WebSessionManager.class); + return new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); + } + + + @Configuration + static class WebConfig { + + @Bean @SuppressWarnings("unused") + public SimpleUrlHandlerMapping handlerMapping() { + SimpleUrlHandlerMapping hm = new SimpleUrlHandlerMapping(); + hm.setUseTrailingSlashMatch(true); + hm.registerHandler("/welcome*", otherController()); + hm.registerHandler("/welcome.html", mainController()); + hm.registerHandler("/show.html", mainController()); + hm.registerHandler("/bookseats.html", mainController()); + return hm; + } + + @Bean + public Object mainController() { + return new Object(); + } + + @Bean + public Object otherController() { + return new Object(); + } + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/WebHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/WebHandlerIntegrationTests.java index 49d4aa1e26..dc9a56baf3 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/WebHandlerIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/WebHandlerIntegrationTests.java @@ -39,10 +39,8 @@ import org.springframework.web.client.RestTemplate; import org.springframework.web.reactive.DispatcherHandler; import org.springframework.web.reactive.ResponseStatusExceptionHandler; import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping; -import org.springframework.web.reactive.result.SimpleResultHandler; -import org.springframework.web.reactive.result.WebHandlerHandlerAdapter; -import org.springframework.web.server.WebHandler; import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebHandler; import org.springframework.web.server.adapter.WebHttpHandlerBuilder; import static org.junit.Assert.assertArrayEquals; @@ -138,7 +136,7 @@ public class WebHandlerIntegrationTests extends AbstractHttpHandlerIntegrationTe map.put("/foo", new FooHandler()); map.put("/bar", new BarHandler()); map.put("/header", new HeaderSettingHandler()); - setHandlers(map); + setUrlMap(map); } } diff --git a/spring-web-reactive/src/test/resources/org/springframework/web/reactive/handler/map.xml b/spring-web-reactive/src/test/resources/org/springframework/web/reactive/handler/map.xml new file mode 100644 index 0000000000..be0f6f067f --- /dev/null +++ b/spring-web-reactive/src/test/resources/org/springframework/web/reactive/handler/map.xml @@ -0,0 +1,40 @@ + + + + + + + + welcome.html=mainController + /**/pathmatchingTest.html=mainController + /**/pathmatching??.html=mainController + /**/path??matching.html=mainController + /**/??path??matching.html=mainController + /**/*.jsp=mainController + /administrator/**/pathmatching.html=mainController + /administrator/**/testlast*=mainController + /administrator/another/bla.xml=mainController + /administrator/testing/longer/**/**/**/**/**=mainController + /administrator/testing/longer2/**/**/bla/**=mainController + /*test*.jpeg=mainController + /*/test.jpeg=mainController + /outofpattern*yeah=mainController + /anotherTest*=mainController + /stillAnotherTestYeah=mainController + /shortpattern/testing=mainController + /show123.html=mainController + /sho*=mainController + /bookseats.html=mainController + /reservation.html=mainController + /payment.html=mainController + /confirmation.html=mainController + /test%26t%20est/path%26m%20atching.html=mainController + + + + + + + From df7c8e550d3218ee413bebbc8508d0c6ced5d951 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 21 Apr 2016 20:35:47 -0400 Subject: [PATCH 325/498] Updates to WebHandler support Rename two classes each adapting to WebHandler to avoid confusing them: 1. HttpWebHandlerAdapter adapts from the low level HttpHandler to any WebHandler (e.g. DispatcherHandler). 2. SimpleHandlerAdapter adapts the plain WebHandler for use within the DispatcherHandler. This commit also fixes an issue in WebHttpHandlerBuilder to ensure that WebExceptionHandler's are inserted before and not after WebFilter's. --- ...Adapter.java => SimpleHandlerAdapter.java} | 14 ++-- .../web/server/WebHandler.java | 7 +- ...dapter.java => HttpWebHandlerAdapter.java} | 6 +- .../server/adapter/WebHttpHandlerBuilder.java | 68 +++++++------------ .../server/handler/FilteringWebHandler.java | 6 +- .../result/WebHandlerIntegrationTests.java | 13 ++-- .../handler/FilteringWebHandlerTests.java | 42 +++++++++++- 7 files changed, 84 insertions(+), 72 deletions(-) rename spring-web-reactive/src/main/java/org/springframework/web/reactive/result/{WebHandlerHandlerAdapter.java => SimpleHandlerAdapter.java} (75%) rename spring-web-reactive/src/main/java/org/springframework/web/server/adapter/{WebHttpHandlerAdapter.java => HttpWebHandlerAdapter.java} (94%) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/WebHandlerHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/SimpleHandlerAdapter.java similarity index 75% rename from spring-web-reactive/src/main/java/org/springframework/web/reactive/result/WebHandlerHandlerAdapter.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/result/SimpleHandlerAdapter.java index c9f8932eb4..a4836426e8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/WebHandlerHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/SimpleHandlerAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -16,7 +16,6 @@ package org.springframework.web.reactive.result; -import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; import org.springframework.core.ResolvableType; @@ -27,15 +26,16 @@ import org.springframework.web.server.WebHandler; import org.springframework.web.server.ServerWebExchange; /** - * Adapter to use a {@link WebHandler} through the {@link DispatcherHandler}. + * HandlerAdapter that allows using the plain {@link WebHandler} contract with + * the generic {@link DispatcherHandler}. * * @author Rossen Stoyanchev * @author Sebastien Deleuze */ -public class WebHandlerHandlerAdapter implements HandlerAdapter { +public class SimpleHandlerAdapter implements HandlerAdapter { - private static final ResolvableType PUBLISHER_VOID = ResolvableType.forClassWithGenerics( - Publisher.class, Void.class); + private static final ResolvableType MONO_VOID = ResolvableType.forClassWithGenerics( + Mono.class, Void.class); @Override @@ -47,7 +47,7 @@ public class WebHandlerHandlerAdapter implements HandlerAdapter { public Mono handle(ServerWebExchange exchange, Object handler) { WebHandler webHandler = (WebHandler) handler; Mono mono = webHandler.handle(exchange); - return Mono.just(new HandlerResult(webHandler, mono, PUBLISHER_VOID)); + return Mono.just(new HandlerResult(webHandler, mono, MONO_VOID)); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/WebHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/server/WebHandler.java index b2c8045b19..45e2da9595 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/WebHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/WebHandler.java @@ -18,19 +18,18 @@ package org.springframework.web.server; import reactor.core.publisher.Mono; -import org.springframework.web.server.adapter.WebHttpHandlerAdapter; +import org.springframework.web.server.adapter.HttpWebHandlerAdapter; import org.springframework.web.server.adapter.WebHttpHandlerBuilder; /** - * Contract to handle a web server exchange. + * Contract to handle a web request. * - *

    Use {@link WebHttpHandlerAdapter} to adapt a {@code WebHandler} to an + *

    Use {@link HttpWebHandlerAdapter} to adapt a {@code WebHandler} to an * {@link org.springframework.http.server.reactive.HttpHandler HttpHandler}. * The {@link WebHttpHandlerBuilder} provides a convenient way to do that while * also optionally configuring one or more filters and/or exception handlers. * * @author Rossen Stoyanchev - * @see WebHttpHandlerBuilder */ public interface WebHandler { diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/adapter/WebHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/server/adapter/HttpWebHandlerAdapter.java similarity index 94% rename from spring-web-reactive/src/main/java/org/springframework/web/server/adapter/WebHttpHandlerAdapter.java rename to spring-web-reactive/src/main/java/org/springframework/web/server/adapter/HttpWebHandlerAdapter.java index fab3ffcdcb..da9deadaab 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/adapter/WebHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/adapter/HttpWebHandlerAdapter.java @@ -38,15 +38,15 @@ import org.springframework.web.server.session.WebSessionManager; * * @author Rossen Stoyanchev */ -public class WebHttpHandlerAdapter extends WebHandlerDecorator implements HttpHandler { +public class HttpWebHandlerAdapter extends WebHandlerDecorator implements HttpHandler { - private static Log logger = LogFactory.getLog(WebHttpHandlerAdapter.class); + private static Log logger = LogFactory.getLog(HttpWebHandlerAdapter.class); private WebSessionManager sessionManager = new DefaultWebSessionManager(); - public WebHttpHandlerAdapter(WebHandler delegate) { + public HttpWebHandlerAdapter(WebHandler delegate) { super(delegate); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/adapter/WebHttpHandlerBuilder.java b/spring-web-reactive/src/main/java/org/springframework/web/server/adapter/WebHttpHandlerBuilder.java index 38dfda1b54..8893bf0460 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/adapter/WebHttpHandlerBuilder.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/adapter/WebHttpHandlerBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -31,28 +31,20 @@ import org.springframework.web.server.handler.FilteringWebHandler; import org.springframework.web.server.session.WebSessionManager; /** - * Build an {@link org.springframework.http.server.reactive.HttpHandler HttpHandler} - * to handle requests with a chain of {@link #filters(WebFilter...) web filters}, - * a target {@link #webHandler(WebHandler) web handler}, and apply one or more - * {@link #exceptionHandlers(WebExceptionHandler...) exception handlers}. - * - *

    Effective this sets up the following {@code WebHandler} delegation:
    - * {@link WebHttpHandlerAdapter} {@code -->} - * {@link ExceptionHandlingWebHandler} {@code -->} - * {@link FilteringWebHandler} {@code -->} - * {@link WebHandler} + * Builder for an {@link HttpHandler} that adapts to a target {@link WebHandler} + * along with a chain of {@link WebFilter}s and a set of + * {@link WebExceptionHandler}s. * *

    Example usage: *

    - * WebFilter myFilter = ... ;
    - * WebHandler myHandler = ... ;
    + * WebFilter filter = ... ;
    + * WebHandler webHandler = ... ;
    + * WebExceptionHandler exceptionHandler = ...;
      *
    - * HttpHandler httpHandler = WebToHttpHandlerBuilder.webHandler(myHandler)
    - *         .filters(myFilter)
    - *         .exceptionHandlers(new ResponseStatusExceptionHandler())
    + * HttpHandler httpHandler = WebHttpHandlerBuilder.webHandler(webHandler)
    + *         .filters(filter)
    + *         .exceptionHandlers(exceptionHandler)
      *         .build();
    - *
    - * // Configure the HttpServer with the created httpHandler
      * 
    * * @author Rossen Stoyanchev @@ -70,7 +62,7 @@ public class WebHttpHandlerBuilder { /** * Private constructor. - * See static factory method {@link #webHandler(WebHandler)}. + * See factory method {@link #webHandler(WebHandler)}. */ private WebHttpHandlerBuilder(WebHandler targetHandler) { Assert.notNull(targetHandler, "'targetHandler' must not be null"); @@ -80,10 +72,10 @@ public class WebHttpHandlerBuilder { /** * Factory method to create a new builder instance. - * @param targetHandler the target handler to process requests with + * @param webHandler the target handler for the request */ - public static WebHttpHandlerBuilder webHandler(WebHandler targetHandler) { - return new WebHttpHandlerBuilder(targetHandler); + public static WebHttpHandlerBuilder webHandler(WebHandler webHandler) { + return new WebHttpHandlerBuilder(webHandler); } @@ -114,6 +106,7 @@ public class WebHttpHandlerBuilder { * {@link ServerWebExchange WebServerExchange} * created for each HTTP request. * @param sessionManager the session manager + * @see HttpWebHandlerAdapter#setSessionManager(WebSessionManager) */ public WebHttpHandlerBuilder sessionManager(WebSessionManager sessionManager) { this.sessionManager = sessionManager; @@ -124,35 +117,20 @@ public class WebHttpHandlerBuilder { * Build the {@link HttpHandler}. */ public HttpHandler build() { - WebHandler handler = createWebHandler(); - return adaptWebHandler(handler); - } - - /** - * Create the final (decorated) {@link WebHandler} to use. - */ - protected WebHandler createWebHandler() { WebHandler webHandler = this.targetHandler; - if (!this.exceptionHandlers.isEmpty()) { - WebExceptionHandler[] array = new WebExceptionHandler[this.exceptionHandlers.size()]; - webHandler = new ExceptionHandlingWebHandler(webHandler, this.exceptionHandlers.toArray(array)); - } if (!this.filters.isEmpty()) { WebFilter[] array = new WebFilter[this.filters.size()]; webHandler = new FilteringWebHandler(webHandler, this.filters.toArray(array)); } - return webHandler; - } - - /** - * Adapt the {@link WebHandler} to {@link HttpHandler}. - */ - protected WebHttpHandlerAdapter adaptWebHandler(WebHandler handler) { - WebHttpHandlerAdapter adapter = new WebHttpHandlerAdapter(handler); - if (this.sessionManager != null) { - adapter.setSessionManager(this.sessionManager); + if (!this.exceptionHandlers.isEmpty()) { + WebExceptionHandler[] array = new WebExceptionHandler[this.exceptionHandlers.size()]; + webHandler = new ExceptionHandlingWebHandler(webHandler, this.exceptionHandlers.toArray(array)); } - return adapter; + HttpWebHandlerAdapter httpHandler = new HttpWebHandlerAdapter(webHandler); + if (this.sessionManager != null) { + httpHandler.setSessionManager(this.sessionManager); + } + return httpHandler; } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/handler/FilteringWebHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/server/handler/FilteringWebHandler.java index 8dc1d7396a..de12581575 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/handler/FilteringWebHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/handler/FilteringWebHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -27,8 +27,8 @@ import org.springframework.web.server.WebHandler; import org.springframework.web.server.ServerWebExchange; /** - * WebHandler that delegates to a chain of {@link WebFilter} instances followed - * by a target {@link WebHandler}. + * WebHandler that delegates to a chain of {@link WebFilter} instances and then + * to the target {@link WebHandler}. * * @author Rossen Stoyanchev */ diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/WebHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/WebHandlerIntegrationTests.java index dc9a56baf3..7eee8878ed 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/WebHandlerIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/WebHandlerIntegrationTests.java @@ -48,7 +48,7 @@ import static org.junit.Assert.assertEquals; /** - * Integration tests with simple WebHandler's processing requests. + * Integration tests with requests mapped to plain {@link WebHandler}s. * * @author Rossen Stoyanchev */ @@ -61,9 +61,9 @@ public class WebHandlerIntegrationTests extends AbstractHttpHandlerIntegrationTe protected HttpHandler createHttpHandler() { StaticApplicationContext wac = new StaticApplicationContext(); - wac.registerSingleton("hm", TestHandlerMapping.class); - wac.registerSingleton("ha", WebHandlerHandlerAdapter.class); - wac.registerSingleton("rh", SimpleResultHandler.class); + wac.registerSingleton("handlerMapping", TestSimpleUrlHandlerMapping.class); + wac.registerSingleton("handlerAdapter", SimpleHandlerAdapter.class); + wac.registerSingleton("resultHandler", SimpleResultHandler.class); wac.refresh(); DispatcherHandler dispatcherHandler = new DispatcherHandler(); @@ -129,9 +129,9 @@ public class WebHandlerIntegrationTests extends AbstractHttpHandlerIntegrationTe } - private static class TestHandlerMapping extends SimpleUrlHandlerMapping { + private static class TestSimpleUrlHandlerMapping extends SimpleUrlHandlerMapping { - public TestHandlerMapping() { + public TestSimpleUrlHandlerMapping() { Map map = new HashMap<>(); map.put("/foo", new FooHandler()); map.put("/bar", new BarHandler()); @@ -140,7 +140,6 @@ public class WebHandlerIntegrationTests extends AbstractHttpHandlerIntegrationTe } } - private static DataBuffer asDataBuffer(String text) { return new DefaultDataBufferAllocator().allocateBuffer().write(text.getBytes(StandardCharsets.UTF_8)); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/server/handler/FilteringWebHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/server/handler/FilteringWebHandlerTests.java index 3ba4e6966d..57078bbe02 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/server/handler/FilteringWebHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/server/handler/FilteringWebHandlerTests.java @@ -23,20 +23,25 @@ import org.apache.commons.logging.LogFactory; import org.junit.Before; import org.junit.Test; import reactor.core.publisher.Mono; +import reactor.core.test.TestSubscriber; import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.HttpHandler; import org.springframework.http.server.reactive.MockServerHttpRequest; import org.springframework.http.server.reactive.MockServerHttpResponse; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.web.server.WebExceptionHandler; import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilterChain; import org.springframework.web.server.WebHandler; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.WebHttpHandlerBuilder; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; /** @@ -47,9 +52,9 @@ public class FilteringWebHandlerTests { private static Log logger = LogFactory.getLog(FilteringWebHandlerTests.class); - private ServerHttpRequest request; + private MockServerHttpRequest request; - private ServerHttpResponse response; + private MockServerHttpResponse response; @Before @@ -108,6 +113,20 @@ public class FilteringWebHandlerTests { assertTrue(webHandler.invoked()); } + @Test + public void handleErrorFromFilter() throws Exception { + TestExceptionHandler exceptionHandler = new TestExceptionHandler(); + HttpHandler handler = WebHttpHandlerBuilder.webHandler(new StubWebHandler()) + .filters(new ExceptionFilter()).exceptionHandlers(exceptionHandler).build(); + handler.handle(this.request, this.response).get(); + + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, this.response.getStatus()); + + Throwable savedException = exceptionHandler.ex; + assertNotNull(savedException); + assertEquals("boo", savedException.getMessage()); + } + private HttpHandler createHttpHandler(StubWebHandler webHandler, WebFilter... filters) { return WebHttpHandlerBuilder.webHandler(webHandler).filters(filters).build(); } @@ -117,7 +136,6 @@ public class FilteringWebHandlerTests { private volatile boolean invoked; - public boolean invoked() { return this.invoked; } @@ -156,6 +174,24 @@ public class FilteringWebHandlerTests { } } + private static class ExceptionFilter implements WebFilter { + + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + return Mono.error(new IllegalStateException("boo")); + } + } + + private static class TestExceptionHandler implements WebExceptionHandler { + + private Throwable ex; + + @Override + public Mono handle(ServerWebExchange exchange, Throwable ex) { + this.ex = ex; + return Mono.error(ex); + } + } private static class StubWebHandler implements WebHandler { From 52c9b3b2353e2f8bfb2c6f277b5f059d688a8bfc Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Thu, 21 Apr 2016 12:42:40 +0200 Subject: [PATCH 326/498] Introduce ResourceEncoder and ResourceDecoder This commit introduces the ResourceEncoder and ResourceDecoder, and uses these in ResourceHttpMessageConverter as a non-zero-copy fallback method. --- .../core/codec/support/ResourceDecoder.java | 77 +++++++++ .../core/codec/support/ResourceEncoder.java | 78 +++++++++ .../core/codec/support/StringEncoder.java | 2 +- .../core/io/support/ResourceUtils2.java | 59 +++++++ .../reactive/CodecHttpMessageConverter.java | 38 ++++- .../ResourceHttpMessageConverter.java | 156 ++++-------------- .../codec/support/ResourceDecoderTests.java | 80 +++++++++ .../codec/support/ResourceEncoderTests.java | 71 ++++++++ 8 files changed, 430 insertions(+), 131 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/core/codec/support/ResourceDecoder.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/core/codec/support/ResourceEncoder.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/core/io/support/ResourceUtils2.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/core/codec/support/ResourceDecoderTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/core/codec/support/ResourceEncoderTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ResourceDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ResourceDecoder.java new file mode 100644 index 0000000000..71b3ac8810 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ResourceDecoder.java @@ -0,0 +1,77 @@ +/* + * 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.support; + +import java.io.InputStream; + +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.core.ResolvableType; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.InputStreamResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.support.DataBufferUtils; +import org.springframework.util.MimeType; +import org.springframework.util.MimeTypeUtils; + +/** + * A decoder for {@link Resource}s. + * + * @author Arjen Poutsma + */ +public class ResourceDecoder extends AbstractDecoder { + + public ResourceDecoder() { + super(MimeTypeUtils.ALL); + } + + @Override + public boolean canDecode(ResolvableType type, MimeType mimeType, Object... hints) { + Class clazz = type.getRawClass(); + return (InputStreamResource.class.equals(clazz) || + clazz.isAssignableFrom(ByteArrayResource.class)) && + super.canDecode(type, mimeType, hints); + } + + @Override + public Flux decode(Publisher inputStream, ResolvableType type, + MimeType mimeType, Object... hints) { + Class clazz = type.getRawClass(); + + Flux body = Flux.from(inputStream); + + if (InputStreamResource.class.equals(clazz)) { + InputStream is = DataBufferUtils.toInputStream(body); + return Flux.just(new InputStreamResource(is)); + } + else if (clazz.isAssignableFrom(ByteArrayResource.class)) { + Mono singleBuffer = body.reduce(DataBuffer::write); + return Flux.from(singleBuffer.map(buffer -> { + byte[] bytes = new byte[buffer.readableByteCount()]; + buffer.read(bytes); + return new ByteArrayResource(bytes); + })); + } + else { + return Flux.error(new IllegalStateException( + "Unsupported resource class: " + clazz)); + } + } +} diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ResourceEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ResourceEncoder.java new file mode 100644 index 0000000000..2451ecf9a8 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ResourceEncoder.java @@ -0,0 +1,78 @@ +/* + * 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.support; + +import java.io.IOException; +import java.io.InputStream; + +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.core.ResolvableType; +import org.springframework.core.io.Resource; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferAllocator; +import org.springframework.core.io.buffer.support.DataBufferUtils; +import org.springframework.util.Assert; +import org.springframework.util.MimeType; +import org.springframework.util.MimeTypeUtils; +import org.springframework.util.StreamUtils; + +/** + * An encoder for {@link Resource}s. + * @author Arjen Poutsma + */ +public class ResourceEncoder extends AbstractEncoder { + + public static final int DEFAULT_BUFFER_SIZE = StreamUtils.BUFFER_SIZE; + + private final int bufferSize; + + public ResourceEncoder() { + this(DEFAULT_BUFFER_SIZE); + } + + public ResourceEncoder(int bufferSize) { + super(MimeTypeUtils.ALL); + Assert.isTrue(bufferSize > 0, "'bufferSize' must be larger than 0"); + this.bufferSize = bufferSize; + } + + @Override + public boolean canEncode(ResolvableType type, MimeType mimeType, Object... hints) { + Class clazz = type.getRawClass(); + return (super.canEncode(type, mimeType, hints) && + Resource.class.isAssignableFrom(clazz)); + } + + @Override + public Flux encode(Publisher inputStream, + DataBufferAllocator allocator, ResolvableType type, MimeType mimeType, + Object... hints) { + return Flux.from(inputStream). + concatMap(resource -> { + try { + InputStream is = resource.getInputStream(); + return DataBufferUtils.read(is, allocator, this.bufferSize); + } + catch (IOException ex) { + return Mono.error(ex); + } + }); + } +} diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringEncoder.java index 810d6de311..14914e4b15 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringEncoder.java @@ -45,7 +45,7 @@ public class StringEncoder extends AbstractEncoder { @Override public boolean canEncode(ResolvableType type, MimeType mimeType, Object... hints) { Class clazz = type.getRawClass(); - return (super.canEncode(type, mimeType, hints) && String.class.isAssignableFrom(clazz)); + return (super.canEncode(type, mimeType, hints) && String.class.equals(clazz)); } @Override diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/support/ResourceUtils2.java b/spring-web-reactive/src/main/java/org/springframework/core/io/support/ResourceUtils2.java new file mode 100644 index 0000000000..94a3af3f14 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/support/ResourceUtils2.java @@ -0,0 +1,59 @@ +/* + * 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.io.support; + +import java.io.IOException; +import java.net.URI; + +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.DescriptiveResource; +import org.springframework.core.io.InputStreamResource; +import org.springframework.core.io.Resource; +import org.springframework.util.Assert; +import org.springframework.util.ResourceUtils; + +/** + * @author Arjen Poutsma + */ +public abstract class ResourceUtils2 { + + /** + * Indicates whether the given resource has a file, so that {@link + * Resource#getFile()} + * can be called without an {@link java.io.IOException}. + * @param resource the resource to check + * @return {@code true} if the given resource has a file; {@code false} otherwise + */ + // TODO: refactor into Resource.hasFile() method + public static boolean hasFile(Resource resource) { + Assert.notNull(resource, "'resource' must not be null"); + + // the following Resource implementations do not support getURI/getFile + if (resource instanceof ByteArrayResource || + resource instanceof DescriptiveResource || + resource instanceof InputStreamResource) { + return false; + } + try { + URI resourceUri = resource.getURI(); + return ResourceUtils.URL_PROTOCOL_FILE.equals(resourceUri.getScheme()); + } + catch (IOException ignored) { + } + return false; + } +} diff --git a/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/CodecHttpMessageConverter.java b/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/CodecHttpMessageConverter.java index b55ad9c4cb..4444a42619 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/CodecHttpMessageConverter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/CodecHttpMessageConverter.java @@ -29,12 +29,16 @@ import org.springframework.core.codec.Decoder; import org.springframework.core.codec.Encoder; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferAllocator; +import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ReactiveHttpInputMessage; import org.springframework.http.ReactiveHttpOutputMessage; import org.springframework.http.support.MediaTypeUtils; /** + * Implementation of the {@link HttpMessageConverter} interface that delegates to + * {@link Encoder} and {@link Decoder}. + * * @author Arjen Poutsma */ public class CodecHttpMessageConverter implements HttpMessageConverter { @@ -43,6 +47,32 @@ public class CodecHttpMessageConverter implements HttpMessageConverter { private final Decoder decoder; + /** + * Create a {@code CodecHttpMessageConverter} with the given {@link Encoder}. When + * using this constructor, all read-related methods will in {@code false} or an + * {@link IllegalStateException}. + * @param encoder the encoder to use + */ + public CodecHttpMessageConverter(Encoder encoder) { + this(encoder, null); + } + + /** + * Create a {@code CodecHttpMessageConverter} with the given {@link Decoder}. When + * using this constructor, all write-related methods will in {@code false} or an + * {@link IllegalStateException}. + * @param decoder the decoder to use + */ + public CodecHttpMessageConverter(Decoder decoder) { + this(null, decoder); + } + + /** + * Create a {@code CodecHttpMessageConverter} with the given {@link Encoder} and + * {@link Decoder}. + * @param encoder the encoder to use, can be {@code null} + * @param decoder the decoder to use, can be {@code null} + */ public CodecHttpMessageConverter(Encoder encoder, Decoder decoder) { this.encoder = encoder; this.decoder = decoder; @@ -94,9 +124,13 @@ public class CodecHttpMessageConverter implements HttpMessageConverter { if (this.encoder == null) { return Mono.error(new IllegalStateException("No decoder set")); } - outputMessage.getHeaders().setContentType(contentType); + HttpHeaders headers = outputMessage.getHeaders(); + if (headers.getContentType() == null) { + headers.setContentType(contentType); + } DataBufferAllocator allocator = outputMessage.allocator(); - Flux body = encoder.encode(inputStream, allocator, type, contentType); + Flux body = + this.encoder.encode(inputStream, allocator, type, contentType); return outputMessage.setBody(body); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/ResourceHttpMessageConverter.java b/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/ResourceHttpMessageConverter.java index 5091fc6fd2..79392a518d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/ResourceHttpMessageConverter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/ResourceHttpMessageConverter.java @@ -18,10 +18,6 @@ package org.springframework.http.converter.reactive; import java.io.File; import java.io.IOException; -import java.io.InputStream; -import java.net.URI; -import java.util.Collections; -import java.util.List; import java.util.Optional; import org.reactivestreams.Publisher; @@ -29,106 +25,46 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.core.ResolvableType; -import org.springframework.core.io.ByteArrayResource; -import org.springframework.core.io.DescriptiveResource; +import org.springframework.core.codec.support.ResourceDecoder; +import org.springframework.core.codec.support.ResourceEncoder; import org.springframework.core.io.InputStreamResource; import org.springframework.core.io.Resource; -import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.support.DataBufferUtils; +import org.springframework.core.io.support.ResourceUtils2; import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpRangeResource; import org.springframework.http.MediaType; -import org.springframework.http.ReactiveHttpInputMessage; import org.springframework.http.ReactiveHttpOutputMessage; import org.springframework.http.ZeroCopyHttpOutputMessage; import org.springframework.http.support.MediaTypeUtils; import org.springframework.util.MimeTypeUtils2; -import org.springframework.util.ResourceUtils; -import org.springframework.util.StreamUtils; /** + * Implementation of {@link HttpMessageConverter} that can read and write + * {@link Resource Resources} and supports byte range requests. + ** * @author Arjen Poutsma */ -public class ResourceHttpMessageConverter implements HttpMessageConverter { +public class ResourceHttpMessageConverter extends CodecHttpMessageConverter { - private static final int BUFFER_SIZE = StreamUtils.BUFFER_SIZE; - - private static final List SUPPORTED_MEDIA_TYPES = - Collections.singletonList(MediaType.ALL); - - @Override - public boolean canRead(ResolvableType type, MediaType mediaType) { - return Resource.class.isAssignableFrom(type.getRawClass()); + public ResourceHttpMessageConverter() { + super(new ResourceEncoder(), new ResourceDecoder()); } - @Override - public boolean canWrite(ResolvableType type, MediaType mediaType) { - return Resource.class.isAssignableFrom(type.getRawClass()); - } - - @Override - public List getReadableMediaTypes() { - return SUPPORTED_MEDIA_TYPES; - } - - @Override - public List getWritableMediaTypes() { - return SUPPORTED_MEDIA_TYPES; - } - - @Override - public Flux read(ResolvableType type, - ReactiveHttpInputMessage inputMessage) { - Class clazz = type.getRawClass(); - - Flux body = inputMessage.getBody(); - - if (InputStreamResource.class.equals(clazz)) { - InputStream is = DataBufferUtils.toInputStream(body); - return Flux.just(new InputStreamResource(is)); - } - else if (clazz.isAssignableFrom(ByteArrayResource.class)) { - Mono singleBuffer = body.reduce(DataBuffer::write); - return Flux.from(singleBuffer.map(buffer -> { - byte[] bytes = new byte[buffer.readableByteCount()]; - buffer.read(bytes); - return new ByteArrayResource(bytes); - })); - } - else { - return Flux.error(new IllegalStateException( - "Unsupported resource class: " + clazz)); - } + public ResourceHttpMessageConverter(int bufferSize) { + super(new ResourceEncoder(bufferSize), new ResourceDecoder()); } @Override public Mono write(Publisher inputStream, ResolvableType type, MediaType contentType, ReactiveHttpOutputMessage outputMessage) { + return Mono.from(Flux.from(inputStream). + take(1). + concatMap(resource -> { + HttpHeaders headers = outputMessage.getHeaders(); + addHeaders(headers, resource, contentType); - if (inputStream instanceof Mono) { - // single resource - return Mono.from(Flux.from(inputStream). - flatMap(resource -> { - HttpHeaders headers = outputMessage.getHeaders(); - addHeaders(headers, resource, contentType); - - if (resource instanceof HttpRangeResource) { - return writePartialContent((HttpRangeResource) resource, - outputMessage); - } - else { - return writeContent(resource, outputMessage, 0, -1); - } - - - })); - } - else { - // multiple resources, not supported! - return Mono.error(new IllegalArgumentException( - "Multiple resources not yet supported")); - } + return writeContent(resource, type, contentType, outputMessage); + })); } protected void addHeaders(HttpHeaders headers, Resource resource, @@ -146,54 +82,24 @@ public class ResourceHttpMessageConverter implements HttpMessageConverter writeContent(Resource resource, - ReactiveHttpOutputMessage outputMessage, long position, long count) { + private Mono writeContent(Resource resource, ResolvableType type, + MediaType contentType, ReactiveHttpOutputMessage outputMessage) { if (outputMessage instanceof ZeroCopyHttpOutputMessage) { Optional file = getFile(resource); if (file.isPresent()) { ZeroCopyHttpOutputMessage zeroCopyResponse = (ZeroCopyHttpOutputMessage) outputMessage; - if (count < 0) { - count = file.get().length(); - } - - return zeroCopyResponse.setBody(file.get(), position, count); + return zeroCopyResponse + .setBody(file.get(), (long) 0, file.get().length()); } } - // non-zero copy fallback - try { - InputStream is = resource.getInputStream(); - long skipped = is.skip(position); - if (skipped < position) { - return Mono.error(new IOException( - "Skipped only " + skipped + " bytes out of " + count + - " required.")); - } - - Flux responseBody = - DataBufferUtils.read(is, outputMessage.allocator(), BUFFER_SIZE); - if (count > 0) { - responseBody = DataBufferUtils.takeUntilByteCount(responseBody, count); - } - - return outputMessage.setBody(responseBody); - } - catch (IOException ex) { - return Mono.error(ex); - } - } - - protected Mono writePartialContent(HttpRangeResource resource, - ReactiveHttpOutputMessage outputMessage) { - - // TODO: implement - - return Mono.empty(); + // non-zero copy fallback, using ResourceEncoder + return super.write(Mono.just(resource), type, + outputMessage.getHeaders().getContentType(), outputMessage); } private static Optional contentLength(Resource resource) { @@ -210,18 +116,12 @@ public class ResourceHttpMessageConverter implements HttpMessageConverter getFile(Resource resource) { - // TODO: introduce Resource.hasFile() property to bypass the potential IOException thrown in Resource.getFile() - // the following Resource implementations do not support getURI/getFile - if (!(resource instanceof ByteArrayResource || - resource instanceof DescriptiveResource || - resource instanceof InputStreamResource)) { + if (ResourceUtils2.hasFile(resource)) { try { - URI resourceUri = resource.getURI(); - if (ResourceUtils.URL_PROTOCOL_FILE.equals(resourceUri.getScheme())) { - return Optional.of(ResourceUtils.getFile(resourceUri)); - } + return Optional.of(resource.getFile()); } catch (IOException ignored) { + // should not happen } } return Optional.empty(); diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ResourceDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ResourceDecoderTests.java new file mode 100644 index 0000000000..e2df242364 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ResourceDecoderTests.java @@ -0,0 +1,80 @@ +/* + * 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.support; + +import java.io.IOException; + +import org.junit.Test; +import reactor.core.publisher.Flux; +import reactor.core.test.TestSubscriber; + +import org.springframework.core.ResolvableType; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.InputStreamResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.http.MediaType; +import org.springframework.util.StreamUtils; + +import static org.junit.Assert.*; + +/** + * @author Arjen Poutsma + */ +public class ResourceDecoderTests extends AbstractAllocatingTestCase { + + private final ResourceDecoder decoder = new ResourceDecoder(); + + @Test + public void canDecode() throws Exception { + assertTrue(decoder.canDecode(ResolvableType.forClass(InputStreamResource.class), + MediaType.TEXT_PLAIN)); + assertTrue(decoder.canDecode(ResolvableType.forClass(ByteArrayResource.class), + MediaType.TEXT_PLAIN)); + assertTrue(decoder.canDecode(ResolvableType.forClass(Resource.class), + MediaType.TEXT_PLAIN)); + assertTrue(decoder.canDecode(ResolvableType.forClass(InputStreamResource.class), + MediaType.APPLICATION_JSON)); + } + + @Test + public void decode() throws Exception { + DataBuffer fooBuffer = stringBuffer("foo"); + DataBuffer barBuffer = stringBuffer("bar"); + Flux source = Flux.just(fooBuffer, barBuffer); + + Flux result = + decoder.decode(source, ResolvableType.forClass(Resource.class), null); + + TestSubscriber testSubscriber = new TestSubscriber<>(); + testSubscriber.bindTo(result). + assertNoError(). + assertComplete(). + assertValuesWith(resource -> { + try { + byte[] bytes = + StreamUtils.copyToByteArray(resource.getInputStream()); + assertEquals("foobar", new String(bytes)); + } + catch (IOException e) { + fail(e.getMessage()); + } + }); + + } + +} \ No newline at end of file diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ResourceEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ResourceEncoderTests.java new file mode 100644 index 0000000000..45421ccdcf --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ResourceEncoderTests.java @@ -0,0 +1,71 @@ +/* + * 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.support; + +import java.nio.charset.StandardCharsets; + +import org.junit.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.test.TestSubscriber; + +import org.springframework.core.ResolvableType; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.InputStreamResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.http.MediaType; + +import static org.junit.Assert.assertTrue; + +/** + * @author Arjen Poutsma + */ +public class ResourceEncoderTests extends AbstractAllocatingTestCase { + + private final ResourceEncoder encoder = new ResourceEncoder(); + + @Test + public void canEncode() throws Exception { + assertTrue(encoder.canEncode(ResolvableType.forClass(InputStreamResource.class), + MediaType.TEXT_PLAIN)); + assertTrue(encoder.canEncode(ResolvableType.forClass(ByteArrayResource.class), + MediaType.TEXT_PLAIN)); + assertTrue(encoder.canEncode(ResolvableType.forClass(Resource.class), + MediaType.TEXT_PLAIN)); + assertTrue(encoder.canEncode(ResolvableType.forClass(InputStreamResource.class), + MediaType.APPLICATION_JSON)); + } + + @Test + public void encode() throws Exception { + String s = "foo"; + Resource resource = new ByteArrayResource(s.getBytes(StandardCharsets.UTF_8)); + + Mono source = Mono.just(resource); + + Flux output = + encoder.encode(source, allocator, ResolvableType.forClass(Resource.class), + null); + + TestSubscriber testSubscriber = new TestSubscriber<>(); + testSubscriber.bindTo(output).assertNoError().assertComplete() + .assertValues(stringBuffer(s)); + + } + +} \ No newline at end of file From 9ed4bff4a933df692505a56ecd9e30440fdcf11e Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Fri, 22 Apr 2016 13:01:18 +0200 Subject: [PATCH 327/498] Fixed DataBufferUtils.takeUntilByteCount --- .../core/io/buffer/support/DataBufferUtils.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java index a8c304a150..3d41b007bf 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java @@ -139,10 +139,10 @@ public abstract class DataBufferUtils { @Override public void onNext(DataBuffer dataBuffer) { - long currentCount = - this.byteCount.addAndGet(dataBuffer.readableByteCount()); + int delta = dataBuffer.readableByteCount(); + long currentCount = this.byteCount.addAndGet(delta); if (currentCount > maxByteCount) { - int size = (int) (currentCount - maxByteCount + 1); + int size = (int) (maxByteCount - currentCount + delta); ByteBuffer byteBuffer = (ByteBuffer) dataBuffer.asByteBuffer().limit(size); DataBuffer partialBuffer = From ad0a4e0cf8d424d8b81cf5da3f0b0e3078710797 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Fri, 22 Apr 2016 13:02:23 +0200 Subject: [PATCH 328/498] Introduce AbstractSingleValueEncoder This commit introduces the AbstractSingleValueEncoder, an abstract base class for encoders that only handle a single value. --- .../core/codec/support/AbstractDecoder.java | 5 +- .../core/codec/support/AbstractEncoder.java | 5 +- .../support/AbstractSingleValueEncoder.java | 68 +++++++++++++++++++ .../core/codec/support/Jaxb2Encoder.java | 43 +++++------- .../core/codec/support/ResourceEncoder.java | 22 ++---- 5 files changed, 94 insertions(+), 49 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractSingleValueEncoder.java diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractDecoder.java index fb00faab0c..fa1b0102b5 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractDecoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -31,8 +31,7 @@ public abstract class AbstractDecoder implements Decoder { private List supportedMimeTypes = Collections.emptyList(); - - public AbstractDecoder(MimeType... supportedMimeTypes) { + protected AbstractDecoder(MimeType... supportedMimeTypes) { this.supportedMimeTypes = Arrays.asList(supportedMimeTypes); } diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractEncoder.java index d0d1920194..98d1c022a4 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractEncoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -31,8 +31,7 @@ public abstract class AbstractEncoder implements Encoder { private List supportedMimeTypes = Collections.emptyList(); - - public AbstractEncoder(MimeType... supportedMimeTypes) { + protected AbstractEncoder(MimeType... supportedMimeTypes) { this.supportedMimeTypes = Arrays.asList(supportedMimeTypes); } diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractSingleValueEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractSingleValueEncoder.java new file mode 100644 index 0000000000..15c8ffcc42 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractSingleValueEncoder.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.support; + +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.DataBufferAllocator; +import org.springframework.util.MimeType; + +/** + * Abstract base class for {@link org.springframework.core.codec.Encoder} classes that + * can only deal with a single value. + * @author Arjen Poutsma + */ +public abstract class AbstractSingleValueEncoder extends AbstractEncoder { + + public AbstractSingleValueEncoder(MimeType... supportedMimeTypes) { + super(supportedMimeTypes); + } + + @Override + public final Flux encode(Publisher inputStream, + DataBufferAllocator allocator, ResolvableType type, MimeType mimeType, + Object... hints) { + return Flux.from(inputStream). + take(1). + concatMap(t -> { + try { + return encode(t, allocator, type, mimeType); + } + catch (Exception ex) { + return Flux.error(ex); + } + }); + } + + /** + * Encodes {@code T} to an output {@link DataBuffer} stream. + * @param t the value to process + * @param allocator a buffer allocator used to create the output + * @param type the stream element type to process + * @param mimeType the mime type to process + * @param hints Additional information about how to do decode, optional + * @return the output stream + * @throws Exception in case of errors + */ + protected abstract Flux encode(T t, DataBufferAllocator allocator, + ResolvableType type, MimeType mimeType, Object... hints) throws Exception; + + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Encoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Encoder.java index d299722342..9674c8b79b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Encoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Encoder.java @@ -19,16 +19,13 @@ package org.springframework.core.codec.support; import java.io.OutputStream; import java.nio.charset.StandardCharsets; import javax.xml.bind.JAXBException; -import javax.xml.bind.MarshalException; import javax.xml.bind.Marshaller; import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.XmlType; -import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import org.springframework.core.ResolvableType; -import org.springframework.core.codec.CodecException; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferAllocator; import org.springframework.util.ClassUtils; @@ -42,7 +39,7 @@ import org.springframework.util.MimeTypeUtils; * @author Arjen Poutsma * @see Jaxb2Decoder */ -public class Jaxb2Encoder extends AbstractEncoder { +public class Jaxb2Encoder extends AbstractSingleValueEncoder { private final JaxbContextContainer jaxbContexts = new JaxbContextContainer(); @@ -64,29 +61,21 @@ public class Jaxb2Encoder extends AbstractEncoder { } @Override - public Flux encode(Publisher inputStream, - DataBufferAllocator allocator, ResolvableType type, MimeType mimeType, - Object... hints) { - - return Flux.from(inputStream). - take(1). // only map 1 value to ensure valid XML output - map(value -> { - try { - DataBuffer buffer = allocator.allocateBuffer(1024); - OutputStream outputStream = buffer.asOutputStream(); - Class clazz = ClassUtils.getUserClass(value); - Marshaller marshaller = jaxbContexts.createMarshaller(clazz); - marshaller.setProperty(Marshaller.JAXB_ENCODING, StandardCharsets.UTF_8.name()); - marshaller.marshal(value, outputStream); - return buffer; - } - catch (MarshalException ex) { - throw new CodecException("Could not marshal [" + value + "]: " + ex.getMessage(), ex); - } - catch (JAXBException ex) { - throw new CodecException("Could not instantiate JAXBContext: " + ex.getMessage(), ex); - } - }); + protected Flux encode(Object value, DataBufferAllocator allocator, + ResolvableType type, MimeType mimeType, Object... hints) { + try { + DataBuffer buffer = allocator.allocateBuffer(1024); + OutputStream outputStream = buffer.asOutputStream(); + Class clazz = ClassUtils.getUserClass(value); + Marshaller marshaller = jaxbContexts.createMarshaller(clazz); + marshaller + .setProperty(Marshaller.JAXB_ENCODING, StandardCharsets.UTF_8.name()); + marshaller.marshal(value, outputStream); + return Flux.just(buffer); + } + catch (JAXBException ex) { + return Flux.error(ex); + } } diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ResourceEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ResourceEncoder.java index 2451ecf9a8..497b79efd8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ResourceEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ResourceEncoder.java @@ -19,9 +19,7 @@ package org.springframework.core.codec.support; import java.io.IOException; import java.io.InputStream; -import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; import org.springframework.core.ResolvableType; import org.springframework.core.io.Resource; @@ -37,7 +35,7 @@ import org.springframework.util.StreamUtils; * An encoder for {@link Resource}s. * @author Arjen Poutsma */ -public class ResourceEncoder extends AbstractEncoder { +public class ResourceEncoder extends AbstractSingleValueEncoder { public static final int DEFAULT_BUFFER_SIZE = StreamUtils.BUFFER_SIZE; @@ -61,18 +59,10 @@ public class ResourceEncoder extends AbstractEncoder { } @Override - public Flux encode(Publisher inputStream, - DataBufferAllocator allocator, ResolvableType type, MimeType mimeType, - Object... hints) { - return Flux.from(inputStream). - concatMap(resource -> { - try { - InputStream is = resource.getInputStream(); - return DataBufferUtils.read(is, allocator, this.bufferSize); - } - catch (IOException ex) { - return Mono.error(ex); - } - }); + protected Flux encode(Resource resource, DataBufferAllocator allocator, + ResolvableType type, MimeType mimeType, Object... hints) throws IOException { + InputStream is = resource.getInputStream(); + return DataBufferUtils.read(is, allocator, bufferSize); } + } From 72b66c9715035d8263a55ca726d90426afca1fff Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Tue, 26 Jan 2016 12:33:03 +0100 Subject: [PATCH 329/498] Introduction of PooledDataBuffer This commit introduces a pooled data buffer as a subtype of DataBuffer, as well as various utility methods related to reference counting. Additionally, Crelease calls have been introduced throughout the codebase to properly dispose of pooled databuffers. --- .../codec/support/JacksonJsonDecoder.java | 7 +- .../codec/support/JacksonJsonEncoder.java | 19 ++--- .../core/codec/support/JsonObjectDecoder.java | 3 + .../core/codec/support/XmlEventDecoder.java | 1 + .../core/io/buffer/NettyDataBuffer.java | 13 +++- .../core/io/buffer/PooledDataBuffer.java | 40 ++++++++++ .../io/buffer/support/DataBufferUtils.java | 15 +++- .../reactive/ServletHttpHandlerAdapter.java | 3 +- .../core/io/buffer/DataBufferTests.java | 26 ++++++- .../core/io/buffer/PooledDataBufferTests.java | 73 +++++++++++++++++++ 10 files changed, 183 insertions(+), 17 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/core/io/buffer/PooledDataBuffer.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/core/io/buffer/PooledDataBufferTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonDecoder.java index 05f6dbf2c5..3d6f406f7e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonDecoder.java @@ -28,6 +28,7 @@ import org.springframework.core.ResolvableType; import org.springframework.core.codec.CodecException; import org.springframework.core.codec.Decoder; import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.support.DataBufferUtils; import org.springframework.util.MimeType; @@ -70,9 +71,11 @@ public class JacksonJsonDecoder extends AbstractDecoder { stream = this.preProcessor.decode(inputStream, type, mimeType, hints); } - return stream.map(content -> { + return stream.map(dataBuffer -> { try { - return reader.readValue(content.asInputStream()); + Object value = reader.readValue(dataBuffer.asInputStream()); + DataBufferUtils.release(dataBuffer); + return value; } catch (IOException e) { throw new CodecException("Error while reading the data", e); diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonEncoder.java index 01b2bcbc41..fc08cfd878 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonEncoder.java @@ -18,6 +18,7 @@ package org.springframework.core.codec.support; import java.io.IOException; import java.io.OutputStream; +import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import com.fasterxml.jackson.databind.ObjectMapper; @@ -43,6 +44,12 @@ public class JacksonJsonEncoder extends AbstractEncoder { private final ObjectMapper mapper; + private static final ByteBuffer START_ARRAY_BUFFER = ByteBuffer.wrap(new byte[]{'['}); + + private static final ByteBuffer SEPARATOR_BUFFER = ByteBuffer.wrap(new byte[]{','}); + + private static final ByteBuffer END_ARRAY_BUFFER = ByteBuffer.wrap(new byte[]{']'}); + public JacksonJsonEncoder() { this(new ObjectMapper()); } @@ -65,10 +72,10 @@ public class JacksonJsonEncoder extends AbstractEncoder { } else { // array - Mono startArray = Mono.just(charBuffer('[', allocator)); + Mono startArray = Mono.just(allocator.wrap(START_ARRAY_BUFFER)); Flux arraySeparators = - Flux.create(sub -> sub.onNext(charBuffer(',', allocator))); - Mono endArray = Mono.just(charBuffer(']', allocator)); + Flux.create(sub -> sub.onNext(allocator.wrap(SEPARATOR_BUFFER))); + Mono endArray = Mono.just(allocator.wrap(END_ARRAY_BUFFER)); Flux serializedObjects = Flux.from(inputStream).map(value -> serialize(value, allocator)); @@ -94,11 +101,5 @@ public class JacksonJsonEncoder extends AbstractEncoder { return buffer; } - private DataBuffer charBuffer(char ch, DataBufferAllocator allocator) { - DataBuffer buffer = allocator.allocateBuffer(1); - buffer.write((byte) ch); - return buffer; - } - } diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectDecoder.java index 42d204916a..1c37fe14f3 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectDecoder.java @@ -30,6 +30,7 @@ import reactor.core.publisher.Flux; import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferAllocator; +import org.springframework.core.io.buffer.support.DataBufferUtils; import org.springframework.util.MimeType; /** @@ -110,11 +111,13 @@ public class JsonObjectDecoder extends AbstractDecoder { List chunks = new ArrayList<>(); if (this.input == null) { this.input = Unpooled.copiedBuffer(b.asByteBuffer()); + DataBufferUtils.release(b); this.writerIndex = this.input.writerIndex(); } else { this.input = Unpooled.copiedBuffer(this.input, Unpooled.copiedBuffer(b.asByteBuffer())); + DataBufferUtils.release(b); this.writerIndex = this.input.writerIndex(); } if (this.state == ST_CORRUPTED) { diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/XmlEventDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/XmlEventDecoder.java index 1a2a77ca60..36a1badb28 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/XmlEventDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/XmlEventDecoder.java @@ -134,6 +134,7 @@ public class XmlEventDecoder extends AbstractDecoder { } } } + DataBufferUtils.release(dataBuffer); return Flux.fromIterable(events); } catch (XMLStreamException ex) { diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/NettyDataBuffer.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/NettyDataBuffer.java index 8beb2f41b7..7078142d6e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/NettyDataBuffer.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/NettyDataBuffer.java @@ -36,7 +36,7 @@ import org.springframework.util.ObjectUtils; * * @author Arjen Poutsma */ -public class NettyDataBuffer implements DataBuffer { +public class NettyDataBuffer implements PooledDataBuffer { private final NettyDataBufferAllocator allocator; @@ -181,6 +181,17 @@ public class NettyDataBuffer implements DataBuffer { return new ByteBufOutputStream(this.byteBuf); } + @Override + public PooledDataBuffer retain() { + this.byteBuf.retain(); + return this; + } + + @Override + public boolean release() { + return this.byteBuf.release(); + } + @Override public int hashCode() { return this.byteBuf.hashCode(); diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/PooledDataBuffer.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/PooledDataBuffer.java new file mode 100644 index 0000000000..ae2b2f7ca0 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/PooledDataBuffer.java @@ -0,0 +1,40 @@ +/* + * 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.io.buffer; + +/** + * Extension of {@link DataBuffer} that allows for buffer that share a memory pool. + * Introduces methods for reference counting. + * + * @author Arjen Poutsma + */ +public interface PooledDataBuffer extends DataBuffer { + + /** + * Increases the reference count for this buffer by one. + * @return this buffer + */ + PooledDataBuffer retain(); + + /** + * Decreases the reference count for this buffer by one, and releases it once the + * count reaches zero. + * @return {@code true} if the buffer was released; {@code false} otherwise. + */ + boolean release(); + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java index 3d41b007bf..2bb2bcdf0c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java @@ -35,6 +35,7 @@ import reactor.core.subscriber.SubscriberWithContext; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferAllocator; +import org.springframework.core.io.buffer.PooledDataBuffer; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils2; @@ -170,6 +171,18 @@ public abstract class DataBufferUtils { }); } + /** + * Releases the given data buffer, if it is a {@link PooledDataBuffer}. + * @param dataBuffer the data buffer to release + * @return {@code true} if the buffer was released; {@code false} otherwise. + */ + public static boolean release(DataBuffer dataBuffer) { + if (dataBuffer instanceof PooledDataBuffer) { + return ((PooledDataBuffer) dataBuffer).release(); + } + return false; + } + private static class ReadableByteChannelConsumer implements Consumer> { @@ -199,7 +212,7 @@ public abstract class DataBufferUtils { } finally { if (release) { - // TODO: release buffer when we have PooledDataBuffer + release(dataBuffer); } } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java index 329949bee5..f65ec1a16a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java @@ -39,6 +39,7 @@ import reactor.core.util.BackpressureUtils; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferAllocator; import org.springframework.core.io.buffer.DefaultDataBufferAllocator; +import org.springframework.core.io.buffer.support.DataBufferUtils; import org.springframework.http.HttpStatus; import org.springframework.util.Assert; @@ -361,7 +362,7 @@ public class ServletHttpHandlerAdapter extends HttpServlet { } private void releaseBuffer() { - // TODO: call PooledDataBuffer.release() when we it is introduced + DataBufferUtils.release(dataBuffer); dataBuffer = null; } diff --git a/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/DataBufferTests.java b/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/DataBufferTests.java index 8169f5e142..687e485e1f 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/DataBufferTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/DataBufferTests.java @@ -28,6 +28,8 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; +import org.springframework.core.io.buffer.support.DataBufferUtils; + import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; @@ -56,6 +58,10 @@ public class DataBufferTests { return allocator.allocateBuffer(capacity); } + private void release(DataBuffer... buffers) { + Arrays.stream(buffers).forEach(DataBufferUtils::release); + } + @Test public void writeAndRead() { @@ -72,6 +78,8 @@ public class DataBufferTests { buffer.read(result); assertArrayEquals(new byte[]{'b', 'c', 'd', 'e'}, result); + + release(buffer); } @Test @@ -103,6 +111,8 @@ public class DataBufferTests { len = inputStream.read(bytes); assertEquals(1, len); assertArrayEquals(new byte[]{'e', (byte) 0}, bytes); + + release(buffer); } @Test @@ -118,6 +128,8 @@ public class DataBufferTests { byte[] bytes = new byte[5]; buffer.read(bytes); assertArrayEquals(new byte[]{'a', 'b', 'c', 'd', 'e'}, bytes); + + release(buffer); } @Test @@ -135,6 +147,8 @@ public class DataBufferTests { result = new byte[2]; buffer.read(result); assertArrayEquals(new byte[]{'c', 'd'}, result); + + release(buffer); } @Test @@ -156,6 +170,12 @@ public class DataBufferTests { buffer1.read(result); assertArrayEquals(new byte[]{'a', 'b', 'c', 'd'}, result); + + release(buffer1); + } + + private ByteBuffer createByteBuffer(int capacity) { + return ByteBuffer.allocate(capacity); } @Test @@ -175,10 +195,8 @@ public class DataBufferTests { buffer1.read(result); assertArrayEquals(new byte[]{'a', 'b', 'c', 'd'}, result); - } - private ByteBuffer createByteBuffer(int capacity) { - return ByteBuffer.allocate(capacity); + release(buffer1); } @Test @@ -195,6 +213,8 @@ public class DataBufferTests { buffer.read(resultBytes); assertArrayEquals(new byte[]{'b', 'c'}, resultBytes); + release(buffer); + } diff --git a/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/PooledDataBufferTests.java b/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/PooledDataBufferTests.java new file mode 100644 index 0000000000..c0f4ae591f --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/PooledDataBufferTests.java @@ -0,0 +1,73 @@ +/* + * 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.io.buffer; + +import io.netty.buffer.PooledByteBufAllocator; +import io.netty.buffer.UnpooledByteBufAllocator; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * @author Arjen Poutsma + */ +@RunWith(Parameterized.class) +public class PooledDataBufferTests { + + @Parameterized.Parameter + public DataBufferAllocator allocator; + + @Parameterized.Parameters(name = "{0}") + public static Object[][] buffers() { + + return new Object[][]{ + {new NettyDataBufferAllocator(new UnpooledByteBufAllocator(true))}, + {new NettyDataBufferAllocator(new UnpooledByteBufAllocator(false))}, + {new NettyDataBufferAllocator(new PooledByteBufAllocator(true))}, + {new NettyDataBufferAllocator(new PooledByteBufAllocator(false))}}; + } + + private PooledDataBuffer createDataBuffer(int capacity) { + return (PooledDataBuffer) allocator.allocateBuffer(capacity); + } + + @Test + public void retainAndRelease() { + PooledDataBuffer buffer = createDataBuffer(1); + buffer.write((byte) 'a'); + + buffer.retain(); + boolean result = buffer.release(); + assertFalse(result); + result = buffer.release(); + assertTrue(result); + } + + @Test(expected = IllegalStateException.class) + public void tooManyReleases() { + PooledDataBuffer buffer = createDataBuffer(1); + buffer.write((byte) 'a'); + + buffer.release(); + buffer.release(); + } + + +} \ No newline at end of file From 09fdc8a384af099b4ffa1da0bcf4eb30a74dd1c2 Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Wed, 27 Apr 2016 21:06:45 +0100 Subject: [PATCH 330/498] update consume to subscribe update after to then (WIP flux) --- .../http/client/reactive/AbstractClientHttpRequest.java | 4 ++-- .../http/client/reactive/ReactorClientHttpRequest.java | 4 ++-- .../http/client/reactive/RxNettyClientHttpRequest.java | 2 +- .../http/server/reactive/AbstractServerHttpResponse.java | 6 +++--- .../http/server/reactive/ReactorServerHttpResponse.java | 2 +- .../http/server/reactive/RxNettyServerHttpResponse.java | 4 ++-- .../web/server/adapter/HttpWebHandlerAdapter.java | 2 +- .../web/server/session/DefaultWebSessionManager.java | 2 +- .../http/server/reactive/MockServerHttpResponse.java | 2 +- .../http/server/reactive/ServerHttpResponseTests.java | 2 +- .../method/annotation/RequestMappingIntegrationTests.java | 6 +++--- .../web/server/session/WebSessionIntegrationTests.java | 2 +- 12 files changed, 19 insertions(+), 19 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/AbstractClientHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/AbstractClientHttpRequest.java index 8018698e76..6ecb0caea8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/AbstractClientHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/AbstractClientHttpRequest.java @@ -76,14 +76,14 @@ public abstract class AbstractClientHttpRequest implements ClientHttpRequest { Mono mono = Mono.empty(); if (this.state.compareAndSet(State.NEW, State.COMMITTING)) { for (Supplier> action : this.beforeCommitActions) { - mono = mono.after(() -> action.get()); + mono = mono.then(() -> action.get()); } return mono .otherwise(ex -> { // Ignore errors from beforeCommit actions return Mono.empty(); }) - .after(() -> { + .then(() -> { this.state.set(State.COMITTED); //writeHeaders(); //writeCookies(); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java index a8331a50aa..07ce7068ca 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java @@ -106,14 +106,14 @@ public class ReactorClientHttpRequest extends AbstractClientHttpRequest { channel.removeTransferEncodingChunked(); } return applyBeforeCommit() - .after(() -> { + .then(() -> { getHeaders().entrySet().stream().forEach(e -> channel.headers().set(e.getKey(), e.getValue())); getCookies().values().stream().flatMap(Collection::stream).forEach(cookie -> channel.addCookie(new DefaultCookie(cookie.getName(), cookie.getValue()))); return Mono.empty(); }) - .after(() -> { + .then(() -> { if (body != null) { return channel.send(body); } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpRequest.java index 16887b0225..cf42da74bf 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpRequest.java @@ -103,7 +103,7 @@ public class RxNettyClientHttpRequest extends AbstractClientHttpRequest { .createRequest(io.netty.handler.codec.http.HttpMethod.valueOf(this.httpMethod.name()), uri.getRawPath()); return applyBeforeCommit() - .after(() -> Mono.just(request)) + .then(() -> Mono.just(request)) .map(req -> { for (Map.Entry> entry : getHeaders().entrySet()) { for (String value : entry.getValue()) { diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java index 22041600e8..57d03643c0 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java @@ -91,20 +91,20 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse { @Override public Mono setBody(Publisher publisher) { return new ChannelSendOperator<>(publisher, writePublisher -> - applyBeforeCommit().after(() -> setBodyInternal(writePublisher))); + applyBeforeCommit().then(() -> setBodyInternal(writePublisher))); } protected Mono applyBeforeCommit() { Mono mono = Mono.empty(); if (this.state.compareAndSet(STATE_NEW, STATE_COMMITTING)) { for (Supplier> action : this.beforeCommitActions) { - mono = mono.after(action); + mono = mono.then(action); } mono = mono.otherwise(ex -> { // Ignore errors from beforeCommit actions return Mono.empty(); }); - mono = mono.after(() -> { + mono = mono.then(() -> { this.state.set(STATE_COMMITTED); writeHeaders(); writeCookies(); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java index 98dbe3f7aa..361e4e9630 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java @@ -106,7 +106,7 @@ public class ReactorServerHttpResponse extends AbstractServerHttpResponse @Override public Mono setBody(File file, long position, long count) { - return applyBeforeCommit().after(() -> { + return applyBeforeCommit().then(() -> { return this.channel.sendFile(file, position, count); }); } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java index f22b794433..7fa7aac5bb 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java @@ -67,7 +67,7 @@ public class RxNettyServerHttpResponse extends AbstractServerHttpResponse { Observable content = RxJava1ObservableConverter.from(publisher).map(this::toByteBuf); Observable completion = this.response.write(content); - return RxJava1ObservableConverter.from(completion).after(); + return RxJava1ObservableConverter.from(completion).then(); } private ByteBuf toByteBuf(DataBuffer buffer) { @@ -132,7 +132,7 @@ public class RxNettyServerHttpResponse extends AbstractServerHttpResponse { FileRegion fileRegion = new DefaultFileRegion(file, position, count); Mono fileWrite = MonoChannelFuture.from(channel.writeAndFlush(fileRegion)); - return Flux.concat(applyBeforeCommit(), responseWrite, fileWrite).after(); + return Flux.concat(applyBeforeCommit(), responseWrite, fileWrite).then(); } */ } \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/adapter/HttpWebHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/server/adapter/HttpWebHandlerAdapter.java index da9deadaab..8f8ccf2f1f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/adapter/HttpWebHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/adapter/HttpWebHandlerAdapter.java @@ -82,7 +82,7 @@ public class HttpWebHandlerAdapter extends WebHandlerDecorator implements HttpHa response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR); return Mono.empty(); }) - .after(response::setComplete); + .then(response::setComplete); } protected ServerWebExchange createExchange(ServerHttpRequest request, ServerHttpResponse response) { diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/session/DefaultWebSessionManager.java b/spring-web-reactive/src/main/java/org/springframework/web/server/session/DefaultWebSessionManager.java index 1eb0e84871..7f8db375d0 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/session/DefaultWebSessionManager.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/session/DefaultWebSessionManager.java @@ -110,7 +110,7 @@ public class DefaultWebSessionManager implements WebSessionManager { protected Mono validateSession(ServerWebExchange exchange, WebSession session) { if (session.isExpired()) { this.sessionIdResolver.setSessionId(exchange, ""); - return this.sessionStore.removeSession(session.getId()).after(Mono::empty); + return this.sessionStore.removeSession(session.getId()).then(Mono::empty); } else { return Mono.just(session); diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java index 75a54e0415..91f370a056 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java @@ -73,7 +73,7 @@ public class MockServerHttpResponse implements ServerHttpResponse { @Override public Mono setBody(Publisher body) { this.body = body; - return Flux.from(this.body).after(); + return Flux.from(this.body).then(); } @Override diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java index 6b2977598f..1d0577d5b7 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java @@ -170,7 +170,7 @@ public class ServerHttpResponseTests { return Flux.from(publisher).map(b -> { this.content.add(b); return b; - }).after(); + }).then(); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java index 579de341e6..594f2ede8c 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java @@ -572,7 +572,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @RequestMapping("/stream-create") public Publisher streamCreate(@RequestBody Flux personStream) { - return personStream.toList().doOnSuccess(persons::addAll).after(); + return personStream.toList().doOnSuccess(persons::addAll).then(); } @RequestMapping("/person-capitalize") @@ -598,12 +598,12 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @RequestMapping("/publisher-create") public Publisher publisherCreate(@RequestBody Publisher personStream) { - return Flux.from(personStream).doOnNext(persons::add).after(); + return Flux.from(personStream).doOnNext(persons::add).then(); } @RequestMapping("/flux-create") public Mono fluxCreate(@RequestBody Flux personStream) { - return personStream.doOnNext(persons::add).after(); + return personStream.doOnNext(persons::add).then(); } @RequestMapping("/observable-create") diff --git a/spring-web-reactive/src/test/java/org/springframework/web/server/session/WebSessionIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/server/session/WebSessionIntegrationTests.java index 45cde4ff18..fc2df4716f 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/server/session/WebSessionIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/server/session/WebSessionIntegrationTests.java @@ -160,7 +160,7 @@ public class WebSessionIntegrationTests extends AbstractHttpHandlerIntegrationTe map.put("counter", value); this.currentValue.set(value); return session; - }).after(); + }).then(); } } From 6fbaa7dbb4a20710ad7eee8232df19c8414e0e72 Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Wed, 27 Apr 2016 22:08:51 +0100 Subject: [PATCH 331/498] compile time issue --- .../web/server/session/DefaultWebSessionManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/session/DefaultWebSessionManager.java b/spring-web-reactive/src/main/java/org/springframework/web/server/session/DefaultWebSessionManager.java index 7f8db375d0..64a5e58b2e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/session/DefaultWebSessionManager.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/session/DefaultWebSessionManager.java @@ -110,7 +110,7 @@ public class DefaultWebSessionManager implements WebSessionManager { protected Mono validateSession(ServerWebExchange exchange, WebSession session) { if (session.isExpired()) { this.sessionIdResolver.setSessionId(exchange, ""); - return this.sessionStore.removeSession(session.getId()).then(Mono::empty); + return this.sessionStore.removeSession(session.getId()).cast(WebSession.class); } else { return Mono.just(session); From 8aa7b42cbf4fc8c9c3d9162834999e605a78b3b2 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Thu, 28 Apr 2016 15:14:52 +0200 Subject: [PATCH 332/498] Resolved DataBuffer leaks in codec package --- .../core/codec/support/ByteBufferDecoder.java | 10 +++- .../core/codec/support/ResourceDecoder.java | 1 + .../core/codec/support/StringDecoder.java | 6 ++- .../codec/support/ByteBufferDecoderTests.java | 37 ++++++------- .../codec/support/ByteBufferEncoderTests.java | 16 +++--- .../support/JacksonJsonDecoderTests.java | 15 +++--- .../support/JacksonJsonEncoderTests.java | 33 ++++++------ .../core/codec/support/Jaxb2DecoderTests.java | 54 ++++++++++--------- .../core/codec/support/Jaxb2EncoderTests.java | 33 +++++++----- .../codec/support/JsonObjectDecoderTests.java | 3 +- .../codec/support/ResourceDecoderTests.java | 18 ++++--- .../codec/support/ResourceEncoderTests.java | 20 ++++--- .../codec/support/StringDecoderTests.java | 20 ++++--- .../codec/support/StringEncoderTests.java | 18 ++++--- .../codec/support/XmlEventDecoderTests.java | 7 ++- ...AbstractDataBufferAllocatingTestCase.java} | 34 +++++++++--- .../core/io/buffer/DataBufferTests.java | 32 +---------- .../support/DataBufferTestUtilsTests.java | 35 +++--------- .../buffer/support/DataBufferUtilsTests.java | 51 +++++++----------- 19 files changed, 225 insertions(+), 218 deletions(-) rename spring-web-reactive/src/test/java/org/springframework/core/{codec/support/AbstractAllocatingTestCase.java => io/buffer/AbstractDataBufferAllocatingTestCase.java} (64%) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferDecoder.java index bea4ad9a1f..a7fa543ad2 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferDecoder.java @@ -23,11 +23,13 @@ import reactor.core.publisher.Flux; import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.support.DataBufferUtils; import org.springframework.util.MimeType; import org.springframework.util.MimeTypeUtils; /** * @author Sebastien Deleuze + * @author Arjen Poutsma */ public class ByteBufferDecoder extends AbstractDecoder { @@ -46,7 +48,13 @@ public class ByteBufferDecoder extends AbstractDecoder { @Override public Flux decode(Publisher inputStream, ResolvableType type, MimeType mimeType, Object... hints) { - return Flux.from(inputStream).map(DataBuffer::asByteBuffer); + return Flux.from(inputStream).map((dataBuffer) -> { + ByteBuffer copy = ByteBuffer.allocate(dataBuffer.readableByteCount()); + copy.put(dataBuffer.asByteBuffer()); + copy.flip(); + DataBufferUtils.release(dataBuffer); + return copy; + }); } } \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ResourceDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ResourceDecoder.java index 71b3ac8810..a9f60b4ac2 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ResourceDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ResourceDecoder.java @@ -66,6 +66,7 @@ public class ResourceDecoder extends AbstractDecoder { return Flux.from(singleBuffer.map(buffer -> { byte[] bytes = new byte[buffer.readableByteCount()]; buffer.read(bytes); + DataBufferUtils.release(buffer); return new ByteArrayResource(bytes); })); } diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java index d23da9b43d..f27ff8005e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java @@ -25,6 +25,7 @@ import reactor.core.publisher.Flux; import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.support.DataBufferUtils; import org.springframework.util.MimeType; import org.springframework.util.MimeTypeUtils; @@ -84,8 +85,9 @@ public class StringDecoder extends AbstractDecoder { inputFlux = Flux.from(inputFlux.reduce(DataBuffer::write)); } Charset charset = getCharset(mimeType); - return inputFlux.map(content -> { - CharBuffer charBuffer = charset.decode(content.asByteBuffer()); + return inputFlux.map(dataBuffer -> { + CharBuffer charBuffer = charset.decode(dataBuffer.asByteBuffer()); + DataBufferUtils.release(dataBuffer); return charBuffer.toString(); }); } diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ByteBufferDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ByteBufferDecoderTests.java index 7ef1437343..9408471bc2 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ByteBufferDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ByteBufferDecoderTests.java @@ -21,26 +21,31 @@ import java.nio.ByteBuffer; import org.junit.Test; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; +import reactor.core.test.TestSubscriber; import org.springframework.core.ResolvableType; +import org.springframework.core.io.buffer.AbstractDataBufferAllocatingTestCase; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.MediaType; -import static org.junit.Assert.*; -import reactor.core.test.TestSubscriber; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; /** * @author Sebastien Deleuze */ -public class ByteBufferDecoderTests extends AbstractAllocatingTestCase { +public class ByteBufferDecoderTests extends AbstractDataBufferAllocatingTestCase { private final ByteBufferDecoder decoder = new ByteBufferDecoder(); @Test public void canDecode() { - assertTrue(decoder.canDecode(ResolvableType.forClass(ByteBuffer.class), MediaType.TEXT_PLAIN)); - assertFalse(decoder.canDecode(ResolvableType.forClass(Integer.class), MediaType.TEXT_PLAIN)); - assertTrue(decoder.canDecode(ResolvableType.forClass(ByteBuffer.class), MediaType.APPLICATION_JSON)); + assertTrue(this.decoder.canDecode(ResolvableType.forClass(ByteBuffer.class), + MediaType.TEXT_PLAIN)); + assertFalse(this.decoder + .canDecode(ResolvableType.forClass(Integer.class), MediaType.TEXT_PLAIN)); + assertTrue(this.decoder.canDecode(ResolvableType.forClass(ByteBuffer.class), + MediaType.APPLICATION_JSON)); } @Test @@ -48,20 +53,12 @@ public class ByteBufferDecoderTests extends AbstractAllocatingTestCase { DataBuffer fooBuffer = stringBuffer("foo"); DataBuffer barBuffer = stringBuffer("bar"); Flux source = Flux.just(fooBuffer, barBuffer); - Flux output = decoder.decode(source, ResolvableType.forClassWithGenerics(Publisher.class, ByteBuffer.class), null); + Flux output = this.decoder.decode(source, + ResolvableType.forClassWithGenerics(Publisher.class, ByteBuffer.class), + null); TestSubscriber testSubscriber = new TestSubscriber<>(); - testSubscriber.bindTo(output) - .assertValuesWith(b -> assertBufferEquals(fooBuffer, b), b -> assertBufferEquals(barBuffer, b)); + testSubscriber.bindTo(output).assertNoError().assertComplete() + .assertValues(ByteBuffer.wrap("foo".getBytes()), + ByteBuffer.wrap("bar".getBytes())); } - - public void assertBufferEquals(DataBuffer expected, ByteBuffer actual) { - byte[] byteBufferBytes = new byte[actual.remaining()]; - actual.get(byteBufferBytes); - - byte[] dataBufferBytes = new byte[expected.readableByteCount()]; - expected.read(dataBufferBytes); - - assertArrayEquals(dataBufferBytes, byteBufferBytes); - } - } diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ByteBufferEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ByteBufferEncoderTests.java index 9e238c50ad..0a1b904dc9 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ByteBufferEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ByteBufferEncoderTests.java @@ -26,6 +26,7 @@ import reactor.core.publisher.Flux; import reactor.core.test.TestSubscriber; import org.springframework.core.ResolvableType; +import org.springframework.core.io.buffer.AbstractDataBufferAllocatingTestCase; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.MediaType; @@ -34,20 +35,23 @@ import static org.junit.Assert.*; /** * @author Sebastien Deleuze */ -public class ByteBufferEncoderTests extends AbstractAllocatingTestCase { +public class ByteBufferEncoderTests extends AbstractDataBufferAllocatingTestCase { private ByteBufferEncoder encoder; @Before public void createEncoder() { - encoder = new ByteBufferEncoder(); + this.encoder = new ByteBufferEncoder(); } @Test public void canEncode() { - assertTrue(encoder.canEncode(ResolvableType.forClass(ByteBuffer.class), MediaType.TEXT_PLAIN)); - assertFalse(encoder.canEncode(ResolvableType.forClass(Integer.class), MediaType.TEXT_PLAIN)); - assertTrue(encoder.canEncode(ResolvableType.forClass(ByteBuffer.class), MediaType.APPLICATION_JSON)); + assertTrue(this.encoder.canEncode(ResolvableType.forClass(ByteBuffer.class), + MediaType.TEXT_PLAIN)); + assertFalse(this.encoder + .canEncode(ResolvableType.forClass(Integer.class), MediaType.TEXT_PLAIN)); + assertTrue(this.encoder.canEncode(ResolvableType.forClass(ByteBuffer.class), + MediaType.APPLICATION_JSON)); } @Test @@ -57,7 +61,7 @@ public class ByteBufferEncoderTests extends AbstractAllocatingTestCase { Flux source = Flux.just(ByteBuffer.wrap(fooBytes), ByteBuffer.wrap(barBytes)); - Flux output = encoder.encode(source, allocator, + Flux output = this.encoder.encode(source, this.allocator, ResolvableType.forClassWithGenerics(Publisher.class, ByteBuffer.class), null); TestSubscriber testSubscriber = new TestSubscriber<>(); diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonDecoderTests.java index 8f54bfee31..e8721702d4 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonDecoderTests.java @@ -18,32 +18,35 @@ package org.springframework.core.codec.support; import org.junit.Test; import reactor.core.publisher.Flux; +import reactor.core.test.TestSubscriber; import org.springframework.core.ResolvableType; +import org.springframework.core.io.buffer.AbstractDataBufferAllocatingTestCase; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.MediaType; -import static org.junit.Assert.*; -import reactor.core.test.TestSubscriber; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; /** * @author Sebastien Deleuze */ -public class JacksonJsonDecoderTests extends AbstractAllocatingTestCase { +public class JacksonJsonDecoderTests extends AbstractDataBufferAllocatingTestCase { private final JacksonJsonDecoder decoder = new JacksonJsonDecoder(); @Test public void canDecode() { - assertTrue(decoder.canDecode(null, MediaType.APPLICATION_JSON)); - assertFalse(decoder.canDecode(null, MediaType.APPLICATION_XML)); + assertTrue(this.decoder.canDecode(null, MediaType.APPLICATION_JSON)); + assertFalse(this.decoder.canDecode(null, MediaType.APPLICATION_XML)); } @Test public void decode() { Flux source = Flux.just(stringBuffer("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}")); - Flux output = decoder.decode(source, ResolvableType.forClass(Pojo.class), null); + Flux output = + this.decoder.decode(source, ResolvableType.forClass(Pojo.class), null); TestSubscriber testSubscriber = new TestSubscriber<>(); testSubscriber.bindTo(output).assertValues(new Pojo("foofoo", "barbar")); } diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonEncoderTests.java index 0ea1c99f68..676769cbb5 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonEncoderTests.java @@ -16,13 +16,13 @@ package org.springframework.core.codec.support; -import java.nio.charset.StandardCharsets; - import org.junit.Before; import org.junit.Test; import reactor.core.publisher.Flux; import reactor.core.test.TestSubscriber; +import org.springframework.core.io.buffer.AbstractDataBufferAllocatingTestCase; +import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.MediaType; import static org.junit.Assert.assertFalse; @@ -31,33 +31,36 @@ import static org.junit.Assert.assertTrue; /** * @author Sebastien Deleuze */ -public class JacksonJsonEncoderTests extends AbstractAllocatingTestCase { +public class JacksonJsonEncoderTests extends AbstractDataBufferAllocatingTestCase { private JacksonJsonEncoder encoder; @Before public void createEncoder() { - encoder = new JacksonJsonEncoder(); + this.encoder = new JacksonJsonEncoder(); } @Test public void canWrite() { - assertTrue(encoder.canEncode(null, MediaType.APPLICATION_JSON)); - assertFalse(encoder.canEncode(null, MediaType.APPLICATION_XML)); + assertTrue(this.encoder.canEncode(null, MediaType.APPLICATION_JSON)); + assertFalse(this.encoder.canEncode(null, MediaType.APPLICATION_XML)); } @Test public void write() { Flux source = Flux.just(new Pojo("foofoo", "barbar"), new Pojo("foofoofoo", "barbarbar")); - Flux output = encoder.encode(source, allocator, null, null).map(chunk -> { - byte[] b = new byte[chunk.readableByteCount()]; - chunk.read(b); - return new String(b, StandardCharsets.UTF_8); - }); - TestSubscriber testSubscriber = new TestSubscriber<>(); - testSubscriber.bindTo(output) - .assertValues("[", "{\"foo\":\"foofoo\",\"bar\":\"barbar\"}", ",", - "{\"foo\":\"foofoofoo\",\"bar\":\"barbarbar\"}", "]"); + + Flux output = this.encoder.encode(source, this.allocator, null, null); + + TestSubscriber testSubscriber = new TestSubscriber<>(); + testSubscriber.bindTo(output). + assertComplete(). + assertNoError(). + assertValuesWith(stringConsumer("["), + stringConsumer("{\"foo\":\"foofoo\",\"bar\":\"barbar\"}"), + stringConsumer(","), + stringConsumer("{\"foo\":\"foofoofoo\",\"bar\":\"barbarbar\"}"), + stringConsumer("]")); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2DecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2DecoderTests.java index 21aa3caf09..ff5d60041e 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2DecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2DecoderTests.java @@ -31,6 +31,7 @@ import org.springframework.core.codec.support.jaxb.XmlRootElementWithNameAndName import org.springframework.core.codec.support.jaxb.XmlType; import org.springframework.core.codec.support.jaxb.XmlTypeWithName; import org.springframework.core.codec.support.jaxb.XmlTypeWithNameAndNamespace; +import org.springframework.core.io.buffer.AbstractDataBufferAllocatingTestCase; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.MediaType; @@ -39,7 +40,7 @@ import static org.junit.Assert.*; /** * @author Sebastien Deleuze */ -public class Jaxb2DecoderTests extends AbstractAllocatingTestCase { +public class Jaxb2DecoderTests extends AbstractDataBufferAllocatingTestCase { private static final String POJO_ROOT = "" + "" + @@ -67,25 +68,25 @@ public class Jaxb2DecoderTests extends AbstractAllocatingTestCase { @Test public void canDecode() { - assertTrue(decoder.canDecode(ResolvableType.forClass(Pojo.class), + assertTrue(this.decoder.canDecode(ResolvableType.forClass(Pojo.class), MediaType.APPLICATION_XML)); - assertTrue(decoder.canDecode(ResolvableType.forClass(Pojo.class), + assertTrue(this.decoder.canDecode(ResolvableType.forClass(Pojo.class), MediaType.TEXT_XML)); - assertFalse(decoder.canDecode(ResolvableType.forClass(Pojo.class), + assertFalse(this.decoder.canDecode(ResolvableType.forClass(Pojo.class), MediaType.APPLICATION_JSON)); - assertTrue(decoder.canDecode(ResolvableType.forClass(TypePojo.class), + assertTrue(this.decoder.canDecode(ResolvableType.forClass(TypePojo.class), MediaType.APPLICATION_XML)); - assertFalse(decoder.canDecode(ResolvableType.forClass(getClass()), + assertFalse(this.decoder.canDecode(ResolvableType.forClass(getClass()), MediaType.APPLICATION_XML)); } @Test public void splitOneBranches() { - Flux xmlEvents = - xmlEventDecoder.decode(Flux.just(stringBuffer(POJO_ROOT)), null, null); - Flux> result = decoder.split(xmlEvents, new QName("pojo")); + Flux xmlEvents = this.xmlEventDecoder + .decode(Flux.just(stringBuffer(POJO_ROOT)), null, null); + Flux> result = this.decoder.split(xmlEvents, new QName("pojo")); TestSubscriber> resultSubscriber = new TestSubscriber<>(); resultSubscriber.bindTo(result). @@ -108,9 +109,9 @@ public class Jaxb2DecoderTests extends AbstractAllocatingTestCase { @Test public void splitMultipleBranches() { - Flux xmlEvents = - xmlEventDecoder.decode(Flux.just(stringBuffer(POJO_CHILD)), null, null); - Flux> result = decoder.split(xmlEvents, new QName("pojo")); + Flux xmlEvents = this.xmlEventDecoder + .decode(Flux.just(stringBuffer(POJO_CHILD)), null, null); + Flux> result = this.decoder.split(xmlEvents, new QName("pojo")); TestSubscriber> resultSubscriber = new TestSubscriber<>(); resultSubscriber.bindTo(result). @@ -158,7 +159,7 @@ public class Jaxb2DecoderTests extends AbstractAllocatingTestCase { public void decodeSingleXmlRootElement() throws Exception { Flux source = Flux.just(stringBuffer(POJO_ROOT)); Flux output = - decoder.decode(source, ResolvableType.forClass(Pojo.class), null); + this.decoder.decode(source, ResolvableType.forClass(Pojo.class), null); TestSubscriber testSubscriber = new TestSubscriber<>(); @@ -173,8 +174,8 @@ public class Jaxb2DecoderTests extends AbstractAllocatingTestCase { @Test public void decodeSingleXmlTypeElement() throws Exception { Flux source = Flux.just(stringBuffer(POJO_ROOT)); - Flux output = - decoder.decode(source, ResolvableType.forClass(TypePojo.class), null); + Flux output = this.decoder + .decode(source, ResolvableType.forClass(TypePojo.class), null); TestSubscriber testSubscriber = new TestSubscriber<>(); @@ -190,7 +191,7 @@ public class Jaxb2DecoderTests extends AbstractAllocatingTestCase { public void decodeMultipleXmlRootElement() throws Exception { Flux source = Flux.just(stringBuffer(POJO_CHILD)); Flux output = - decoder.decode(source, ResolvableType.forClass(Pojo.class), null); + this.decoder.decode(source, ResolvableType.forClass(Pojo.class), null); TestSubscriber testSubscriber = new TestSubscriber<>(); @@ -205,8 +206,8 @@ public class Jaxb2DecoderTests extends AbstractAllocatingTestCase { @Test public void decodeMultipleXmlTypeElement() throws Exception { Flux source = Flux.just(stringBuffer(POJO_CHILD)); - Flux output = - decoder.decode(source, ResolvableType.forClass(TypePojo.class), null); + Flux output = this.decoder + .decode(source, ResolvableType.forClass(TypePojo.class), null); TestSubscriber testSubscriber = new TestSubscriber<>(); @@ -220,21 +221,22 @@ public class Jaxb2DecoderTests extends AbstractAllocatingTestCase { @Test public void toExpectedQName() { - assertEquals(new QName("pojo"), decoder.toQName(Pojo.class)); - assertEquals(new QName("pojo"), decoder.toQName(TypePojo.class)); + assertEquals(new QName("pojo"), this.decoder.toQName(Pojo.class)); + assertEquals(new QName("pojo"), this.decoder.toQName(TypePojo.class)); assertEquals(new QName("namespace", "name"), - decoder.toQName(XmlRootElementWithNameAndNamespace.class)); + this.decoder.toQName(XmlRootElementWithNameAndNamespace.class)); assertEquals(new QName("namespace", "name"), - decoder.toQName(XmlRootElementWithName.class)); + this.decoder.toQName(XmlRootElementWithName.class)); assertEquals(new QName("namespace", "xmlRootElement"), - decoder.toQName(XmlRootElement.class)); + this.decoder.toQName(XmlRootElement.class)); assertEquals(new QName("namespace", "name"), - decoder.toQName(XmlTypeWithNameAndNamespace.class)); + this.decoder.toQName(XmlTypeWithNameAndNamespace.class)); assertEquals(new QName("namespace", "name"), - decoder.toQName(XmlTypeWithName.class)); - assertEquals(new QName("namespace", "xmlType"), decoder.toQName(XmlType.class)); + this.decoder.toQName(XmlTypeWithName.class)); + assertEquals(new QName("namespace", "xmlType"), + this.decoder.toQName(XmlType.class)); } diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2EncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2EncoderTests.java index 21c37595e6..56d2a7b6a2 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2EncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2EncoderTests.java @@ -26,7 +26,10 @@ import reactor.core.publisher.Flux; import reactor.core.test.TestSubscriber; 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.support.DataBufferTestUtils; +import org.springframework.core.io.buffer.support.DataBufferUtils; import org.springframework.http.MediaType; import static org.custommonkey.xmlunit.XMLAssert.assertXMLEqual; @@ -38,47 +41,51 @@ import static org.junit.Assert.assertTrue; * @author Sebastien Deleuze * @author Arjen Poutsma */ -public class Jaxb2EncoderTests extends AbstractAllocatingTestCase { +public class Jaxb2EncoderTests extends AbstractDataBufferAllocatingTestCase { private Jaxb2Encoder encoder; @Before public void createEncoder() { - encoder = new Jaxb2Encoder(); + this.encoder = new Jaxb2Encoder(); } @Test public void canEncode() { - assertTrue(encoder.canEncode(ResolvableType.forClass(Pojo.class), + assertTrue(this.encoder.canEncode(ResolvableType.forClass(Pojo.class), MediaType.APPLICATION_XML)); - assertTrue(encoder.canEncode(ResolvableType.forClass(Pojo.class), + assertTrue(this.encoder.canEncode(ResolvableType.forClass(Pojo.class), MediaType.TEXT_XML)); - assertFalse(encoder.canEncode(ResolvableType.forClass(Pojo.class), + assertFalse(this.encoder.canEncode(ResolvableType.forClass(Pojo.class), MediaType.APPLICATION_JSON)); - assertTrue(encoder.canEncode( + assertTrue(this.encoder.canEncode( ResolvableType.forClass(Jaxb2DecoderTests.TypePojo.class), MediaType.APPLICATION_XML)); - assertFalse(encoder.canEncode(ResolvableType.forClass(getClass()), + assertFalse(this.encoder.canEncode(ResolvableType.forClass(getClass()), MediaType.APPLICATION_XML)); } @Test public void encode() { Flux source = Flux.just(new Pojo("foofoo", "barbar"), new Pojo("foofoofoo", "barbarbar")); - Flux output = - encoder.encode(source, allocator, ResolvableType.forClass(Pojo.class), - MediaType.APPLICATION_XML).map(chunk -> DataBufferTestUtils - .dumpString(chunk, StandardCharsets.UTF_8)); - TestSubscriber testSubscriber = new TestSubscriber<>(); - testSubscriber.bindTo(output).assertValuesWith(s -> { + Flux output = this.encoder + .encode(source, this.allocator, ResolvableType.forClass(Pojo.class), + MediaType.APPLICATION_XML); + TestSubscriber testSubscriber = new TestSubscriber<>(); + testSubscriber.bindTo(output).assertValuesWith(dataBuffer -> { try { + String s = DataBufferTestUtils + .dumpString(dataBuffer, StandardCharsets.UTF_8); assertXMLEqual("barbarfoofoo", s); } catch (SAXException | IOException e) { fail(e.getMessage()); } + finally { + DataBufferUtils.release(dataBuffer); + } }); } diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JsonObjectDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JsonObjectDecoderTests.java index 622148efad..92aef3a8fe 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JsonObjectDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JsonObjectDecoderTests.java @@ -22,12 +22,13 @@ import org.junit.Test; import reactor.core.publisher.Flux; import reactor.core.test.TestSubscriber; +import org.springframework.core.io.buffer.AbstractDataBufferAllocatingTestCase; import org.springframework.core.io.buffer.DataBuffer; /** * @author Sebastien Deleuze */ -public class JsonObjectDecoderTests extends AbstractAllocatingTestCase { +public class JsonObjectDecoderTests extends AbstractDataBufferAllocatingTestCase { @Test diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ResourceDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ResourceDecoderTests.java index e2df242364..7688a1d036 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ResourceDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ResourceDecoderTests.java @@ -26,6 +26,7 @@ import org.springframework.core.ResolvableType; import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.InputStreamResource; import org.springframework.core.io.Resource; +import org.springframework.core.io.buffer.AbstractDataBufferAllocatingTestCase; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.MediaType; import org.springframework.util.StreamUtils; @@ -35,19 +36,22 @@ import static org.junit.Assert.*; /** * @author Arjen Poutsma */ -public class ResourceDecoderTests extends AbstractAllocatingTestCase { +public class ResourceDecoderTests extends AbstractDataBufferAllocatingTestCase { private final ResourceDecoder decoder = new ResourceDecoder(); @Test public void canDecode() throws Exception { - assertTrue(decoder.canDecode(ResolvableType.forClass(InputStreamResource.class), + assertTrue( + this.decoder.canDecode(ResolvableType.forClass(InputStreamResource.class), MediaType.TEXT_PLAIN)); - assertTrue(decoder.canDecode(ResolvableType.forClass(ByteArrayResource.class), + assertTrue( + this.decoder.canDecode(ResolvableType.forClass(ByteArrayResource.class), MediaType.TEXT_PLAIN)); - assertTrue(decoder.canDecode(ResolvableType.forClass(Resource.class), + assertTrue(this.decoder.canDecode(ResolvableType.forClass(Resource.class), MediaType.TEXT_PLAIN)); - assertTrue(decoder.canDecode(ResolvableType.forClass(InputStreamResource.class), + assertTrue( + this.decoder.canDecode(ResolvableType.forClass(InputStreamResource.class), MediaType.APPLICATION_JSON)); } @@ -57,8 +61,8 @@ public class ResourceDecoderTests extends AbstractAllocatingTestCase { DataBuffer barBuffer = stringBuffer("bar"); Flux source = Flux.just(fooBuffer, barBuffer); - Flux result = - decoder.decode(source, ResolvableType.forClass(Resource.class), null); + Flux result = this.decoder + .decode(source, ResolvableType.forClass(Resource.class), null); TestSubscriber testSubscriber = new TestSubscriber<>(); testSubscriber.bindTo(result). diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ResourceEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ResourceEncoderTests.java index 45421ccdcf..c3d67320df 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ResourceEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ResourceEncoderTests.java @@ -27,6 +27,7 @@ import org.springframework.core.ResolvableType; import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.InputStreamResource; import org.springframework.core.io.Resource; +import org.springframework.core.io.buffer.AbstractDataBufferAllocatingTestCase; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.MediaType; @@ -35,19 +36,22 @@ import static org.junit.Assert.assertTrue; /** * @author Arjen Poutsma */ -public class ResourceEncoderTests extends AbstractAllocatingTestCase { +public class ResourceEncoderTests extends AbstractDataBufferAllocatingTestCase { private final ResourceEncoder encoder = new ResourceEncoder(); @Test public void canEncode() throws Exception { - assertTrue(encoder.canEncode(ResolvableType.forClass(InputStreamResource.class), + assertTrue( + this.encoder.canEncode(ResolvableType.forClass(InputStreamResource.class), MediaType.TEXT_PLAIN)); - assertTrue(encoder.canEncode(ResolvableType.forClass(ByteArrayResource.class), + assertTrue( + this.encoder.canEncode(ResolvableType.forClass(ByteArrayResource.class), MediaType.TEXT_PLAIN)); - assertTrue(encoder.canEncode(ResolvableType.forClass(Resource.class), + assertTrue(this.encoder.canEncode(ResolvableType.forClass(Resource.class), MediaType.TEXT_PLAIN)); - assertTrue(encoder.canEncode(ResolvableType.forClass(InputStreamResource.class), + assertTrue( + this.encoder.canEncode(ResolvableType.forClass(InputStreamResource.class), MediaType.APPLICATION_JSON)); } @@ -58,13 +62,13 @@ public class ResourceEncoderTests extends AbstractAllocatingTestCase { Mono source = Mono.just(resource); - Flux output = - encoder.encode(source, allocator, ResolvableType.forClass(Resource.class), + Flux output = this.encoder + .encode(source, this.allocator, ResolvableType.forClass(Resource.class), null); TestSubscriber testSubscriber = new TestSubscriber<>(); testSubscriber.bindTo(output).assertNoError().assertComplete() - .assertValues(stringBuffer(s)); + .assertValuesWith(stringConsumer(s)); } diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringDecoderTests.java index b5f83fbc04..c1d69680af 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringDecoderTests.java @@ -25,6 +25,7 @@ import reactor.core.test.TestSubscriber; import rx.Single; import org.springframework.core.ResolvableType; +import org.springframework.core.io.buffer.AbstractDataBufferAllocatingTestCase; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.MediaType; @@ -35,23 +36,28 @@ import static org.junit.Assert.*; * @author Brian Clozel * @author Mark Paluch */ -public class StringDecoderTests extends AbstractAllocatingTestCase { +public class StringDecoderTests extends AbstractDataBufferAllocatingTestCase { private StringDecoder decoder; @Before public void createEncoder() { - decoder = new StringDecoder(); + this.decoder = new StringDecoder(); } @Test public void canDecode() { - assertTrue(decoder.canDecode(ResolvableType.forClass(String.class), MediaType.TEXT_PLAIN)); - assertTrue(decoder.canDecode(ResolvableType.forClass(String.class), MediaType.TEXT_HTML)); - assertTrue(decoder.canDecode(ResolvableType.forClass(String.class), MediaType.APPLICATION_JSON)); - assertFalse(decoder.canDecode(ResolvableType.forClass(Integer.class), MediaType.TEXT_PLAIN)); - assertFalse(decoder.canDecode(ResolvableType.forClass(Pojo.class), MediaType.APPLICATION_JSON)); + assertTrue(this.decoder + .canDecode(ResolvableType.forClass(String.class), MediaType.TEXT_PLAIN)); + assertTrue(this.decoder + .canDecode(ResolvableType.forClass(String.class), MediaType.TEXT_HTML)); + assertTrue(this.decoder.canDecode(ResolvableType.forClass(String.class), + MediaType.APPLICATION_JSON)); + assertFalse(this.decoder + .canDecode(ResolvableType.forClass(Integer.class), MediaType.TEXT_PLAIN)); + assertFalse(this.decoder.canDecode(ResolvableType.forClass(Pojo.class), + MediaType.APPLICATION_JSON)); } @Test diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringEncoderTests.java index d69c284638..3742a6589a 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringEncoderTests.java @@ -26,6 +26,7 @@ import reactor.core.publisher.Flux; import reactor.core.test.TestSubscriber; import org.springframework.core.ResolvableType; +import org.springframework.core.io.buffer.AbstractDataBufferAllocatingTestCase; import org.springframework.http.MediaType; import static org.junit.Assert.assertFalse; @@ -35,26 +36,29 @@ import static org.junit.Assert.assertTrue; * @author Sebastien Deleuze */ @RunWith(Parameterized.class) -public class StringEncoderTests extends AbstractAllocatingTestCase { +public class StringEncoderTests extends AbstractDataBufferAllocatingTestCase { private StringEncoder encoder; @Before public void createEncoder() { - encoder = new StringEncoder(); + this.encoder = new StringEncoder(); } @Test public void canWrite() { - assertTrue(encoder.canEncode(ResolvableType.forClass(String.class), MediaType.TEXT_PLAIN)); - assertFalse(encoder.canEncode(ResolvableType.forClass(Integer.class), MediaType.TEXT_PLAIN)); - assertFalse(encoder.canEncode(ResolvableType.forClass(String.class), MediaType.APPLICATION_JSON)); + assertTrue(this.encoder + .canEncode(ResolvableType.forClass(String.class), MediaType.TEXT_PLAIN)); + assertFalse(this.encoder + .canEncode(ResolvableType.forClass(Integer.class), MediaType.TEXT_PLAIN)); + assertFalse(this.encoder.canEncode(ResolvableType.forClass(String.class), + MediaType.APPLICATION_JSON)); } @Test public void write() throws InterruptedException { - Flux output = - Flux.from(encoder.encode(Flux.just("foo"), allocator, null, null)) + Flux output = Flux.from( + this.encoder.encode(Flux.just("foo"), this.allocator, null, null)) .map(chunk -> { byte[] b = new byte[chunk.readableByteCount()]; chunk.read(b); diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/XmlEventDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/XmlEventDecoderTests.java index 6f41f48027..f1aec58dde 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/XmlEventDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/XmlEventDecoderTests.java @@ -22,13 +22,15 @@ import org.junit.Test; import reactor.core.publisher.Flux; import reactor.core.test.TestSubscriber; +import org.springframework.core.io.buffer.AbstractDataBufferAllocatingTestCase; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; /** * @author Arjen Poutsma */ -public class XmlEventDecoderTests extends AbstractAllocatingTestCase { +public class XmlEventDecoderTests extends AbstractDataBufferAllocatingTestCase { private static final String XML = "" + "" + @@ -41,7 +43,8 @@ public class XmlEventDecoderTests extends AbstractAllocatingTestCase { @Test public void toXMLEvents() { - Flux events = decoder.decode(Flux.just(stringBuffer(XML)), null, null); + Flux events = + this.decoder.decode(Flux.just(stringBuffer(XML)), null, null); TestSubscriber testSubscriber = new TestSubscriber<>(); testSubscriber.bindTo(events). diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/AbstractAllocatingTestCase.java b/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/AbstractDataBufferAllocatingTestCase.java similarity index 64% rename from spring-web-reactive/src/test/java/org/springframework/core/codec/support/AbstractAllocatingTestCase.java rename to spring-web-reactive/src/test/java/org/springframework/core/io/buffer/AbstractDataBufferAllocatingTestCase.java index 75710ac4d6..958bf947e0 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/AbstractAllocatingTestCase.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/AbstractDataBufferAllocatingTestCase.java @@ -14,25 +14,27 @@ * limitations under the License. */ -package org.springframework.core.codec.support; +package org.springframework.core.io.buffer; import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.function.Consumer; import io.netty.buffer.PooledByteBufAllocator; import io.netty.buffer.UnpooledByteBufAllocator; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; -import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferAllocator; -import org.springframework.core.io.buffer.DefaultDataBufferAllocator; -import org.springframework.core.io.buffer.NettyDataBufferAllocator; +import org.springframework.core.io.buffer.support.DataBufferTestUtils; +import org.springframework.core.io.buffer.support.DataBufferUtils; + +import static org.junit.Assert.assertEquals; /** * @author Arjen Poutsma */ @RunWith(Parameterized.class) -public abstract class AbstractAllocatingTestCase { +public abstract class AbstractDataBufferAllocatingTestCase { @Parameterized.Parameter public DataBufferAllocator allocator; @@ -50,10 +52,28 @@ public abstract class AbstractAllocatingTestCase { }; } + protected DataBuffer createDataBuffer(int capacity) { + return this.allocator.allocateBuffer(capacity); + } + protected DataBuffer stringBuffer(String value) { byte[] bytes = value.getBytes(StandardCharsets.UTF_8); - DataBuffer buffer = allocator.allocateBuffer(bytes.length); + DataBuffer buffer = this.allocator.allocateBuffer(bytes.length); buffer.write(bytes); return buffer; } + + protected void release(DataBuffer... buffers) { + Arrays.stream(buffers).forEach(DataBufferUtils::release); + } + + protected Consumer stringConsumer(String expected) { + return dataBuffer -> { + String value = + DataBufferTestUtils.dumpString(dataBuffer, StandardCharsets.UTF_8); + assertEquals(expected, value); + DataBufferUtils.release(dataBuffer); + }; + } + } diff --git a/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/DataBufferTests.java b/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/DataBufferTests.java index 687e485e1f..cf9806f098 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/DataBufferTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/DataBufferTests.java @@ -22,13 +22,7 @@ import java.io.OutputStream; import java.nio.ByteBuffer; import java.util.Arrays; -import io.netty.buffer.PooledByteBufAllocator; -import io.netty.buffer.UnpooledByteBufAllocator; import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; - -import org.springframework.core.io.buffer.support.DataBufferUtils; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; @@ -36,31 +30,7 @@ import static org.junit.Assert.assertEquals; /** * @author Arjen Poutsma */ -@RunWith(Parameterized.class) -public class DataBufferTests { - - @Parameterized.Parameter - public DataBufferAllocator allocator; - - @Parameterized.Parameters(name = "{0}") - public static Object[][] buffers() { - - return new Object[][]{ - {new NettyDataBufferAllocator(new UnpooledByteBufAllocator(true))}, - {new NettyDataBufferAllocator(new UnpooledByteBufAllocator(false))}, - {new NettyDataBufferAllocator(new PooledByteBufAllocator(true))}, - {new NettyDataBufferAllocator(new PooledByteBufAllocator(false))}, - {new DefaultDataBufferAllocator(true)}, - {new DefaultDataBufferAllocator(false)}}; - } - - private DataBuffer createDataBuffer(int capacity) { - return allocator.allocateBuffer(capacity); - } - - private void release(DataBuffer... buffers) { - Arrays.stream(buffers).forEach(DataBufferUtils::release); - } +public class DataBufferTests extends AbstractDataBufferAllocatingTestCase { @Test public void writeAndRead() { diff --git a/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/support/DataBufferTestUtilsTests.java b/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/support/DataBufferTestUtilsTests.java index 98267b1547..05fa82283a 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/support/DataBufferTestUtilsTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/support/DataBufferTestUtilsTests.java @@ -18,16 +18,10 @@ package org.springframework.core.io.buffer.support; import java.nio.charset.StandardCharsets; -import io.netty.buffer.PooledByteBufAllocator; -import io.netty.buffer.UnpooledByteBufAllocator; import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; +import org.springframework.core.io.buffer.AbstractDataBufferAllocatingTestCase; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferAllocator; -import org.springframework.core.io.buffer.DefaultDataBufferAllocator; -import org.springframework.core.io.buffer.NettyDataBufferAllocator; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; @@ -35,45 +29,32 @@ import static org.junit.Assert.assertEquals; /** * @author Arjen Poutsma */ -@RunWith(Parameterized.class) -public class DataBufferTestUtilsTests { - - @Parameterized.Parameter - public DataBufferAllocator allocator; - - @Parameterized.Parameters(name = "{0}") - public static Object[][] buffers() { - - return new Object[][]{ - {new NettyDataBufferAllocator(new UnpooledByteBufAllocator(true))}, - {new NettyDataBufferAllocator(new UnpooledByteBufAllocator(false))}, - {new NettyDataBufferAllocator(new PooledByteBufAllocator(true))}, - {new NettyDataBufferAllocator(new PooledByteBufAllocator(false))}, - {new DefaultDataBufferAllocator(true)}, - {new DefaultDataBufferAllocator(false)}}; - } +public class DataBufferTestUtilsTests extends AbstractDataBufferAllocatingTestCase { @Test public void dumpBytes() { - DataBuffer buffer = allocator.allocateBuffer(4); + DataBuffer buffer = this.allocator.allocateBuffer(4); byte[] source = {'a', 'b', 'c', 'd'}; buffer.write(source); byte[] result = DataBufferTestUtils.dumpBytes(buffer); assertArrayEquals(source, result); + + release(buffer); } @Test public void dumpString() { - DataBuffer buffer = allocator.allocateBuffer(4); + DataBuffer buffer = this.allocator.allocateBuffer(4); String source = "abcd"; buffer.write(source.getBytes(StandardCharsets.UTF_8)); String result = DataBufferTestUtils.dumpString(buffer, StandardCharsets.UTF_8); - assertEquals(source, result); + + release(buffer); } } \ No newline at end of file diff --git a/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/support/DataBufferUtilsTests.java b/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/support/DataBufferUtilsTests.java index 052c6a2a00..f4971bc5c0 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/support/DataBufferUtilsTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/support/DataBufferUtilsTests.java @@ -26,7 +26,7 @@ import org.junit.Test; import reactor.core.publisher.Flux; import reactor.core.test.TestSubscriber; -import org.springframework.core.codec.support.AbstractAllocatingTestCase; +import org.springframework.core.io.buffer.AbstractDataBufferAllocatingTestCase; import org.springframework.core.io.buffer.DataBuffer; import static org.junit.Assert.assertFalse; @@ -34,7 +34,7 @@ import static org.junit.Assert.assertFalse; /** * @author Arjen Poutsma */ -public class DataBufferUtilsTests extends AbstractAllocatingTestCase { +public class DataBufferUtilsTests extends AbstractDataBufferAllocatingTestCase { @Test public void readChannel() throws Exception { @@ -42,14 +42,14 @@ public class DataBufferUtilsTests extends AbstractAllocatingTestCase { .toURI(); FileChannel channel = FileChannel.open(Paths.get(uri), StandardOpenOption.READ); - Flux flux = DataBufferUtils.read(channel, allocator, 4); + Flux flux = DataBufferUtils.read(channel, this.allocator, 4); TestSubscriber testSubscriber = new TestSubscriber<>(); testSubscriber.bindTo(flux). assertNoError(). assertComplete(). - assertValues(stringBuffer("foo\n"), stringBuffer("bar\n"), - stringBuffer("baz\n"), stringBuffer("qux\n")); + assertValuesWith(stringConsumer("foo\n"), stringConsumer("bar\n"), + stringConsumer("baz\n"), stringConsumer("qux\n")); assertFalse(channel.isOpen()); } @@ -60,15 +60,15 @@ public class DataBufferUtilsTests extends AbstractAllocatingTestCase { .toURI(); FileChannel channel = FileChannel.open(Paths.get(uri), StandardOpenOption.READ); - Flux flux = DataBufferUtils.read(channel, allocator, 3); + Flux flux = DataBufferUtils.read(channel, this.allocator, 3); TestSubscriber testSubscriber = new TestSubscriber<>(); testSubscriber.bindTo(flux). assertNoError(). assertComplete(). - assertValues(stringBuffer("foo"), stringBuffer("\nba"), - stringBuffer("r\nb"), stringBuffer("az\n"), stringBuffer("qux"), - stringBuffer("\n")); + assertValuesWith(stringConsumer("foo"), stringConsumer("\nba"), + stringConsumer("r\nb"), stringConsumer("az\n"), + stringConsumer("qux"), stringConsumer("\n")); assertFalse(channel.isOpen()); } @@ -78,37 +78,22 @@ public class DataBufferUtilsTests extends AbstractAllocatingTestCase { InputStream is = DataBufferUtilsTests.class .getResourceAsStream("DataBufferUtilsTests.txt"); - Flux flux = DataBufferUtils.read(is, allocator, 4); + Flux flux = DataBufferUtils.read(is, this.allocator, 4); TestSubscriber testSubscriber = new TestSubscriber<>(); testSubscriber.bindTo(flux). assertNoError(). assertComplete(). - assertValues(stringBuffer("foo\n"), stringBuffer("bar\n"), - stringBuffer("baz\n"), stringBuffer("qux\n")); + assertValuesWith(stringConsumer("foo\n"), stringConsumer("bar\n"), + stringConsumer("baz\n"), stringConsumer("qux\n")); } - @Test - public void readUnalignedInputStream() throws Exception { - InputStream is = DataBufferUtilsTests.class - .getResourceAsStream("DataBufferUtilsTests.txt"); - - Flux flux = DataBufferUtils.read(is, allocator, 3); - - TestSubscriber testSubscriber = new TestSubscriber<>(); - testSubscriber.bindTo(flux). - assertNoError(). - assertComplete(). - assertValues(stringBuffer("foo"), stringBuffer("\nba"), - stringBuffer("r\nb"), stringBuffer("az\n"), stringBuffer("qux"), - stringBuffer("\n")); - } - - @Test public void takeUntilByteCount() { - Flux flux = - Flux.just(stringBuffer("foo"), stringBuffer("bar"), stringBuffer("baz")); + DataBuffer foo = stringBuffer("foo"); + DataBuffer bar = stringBuffer("bar"); + DataBuffer baz = stringBuffer("baz"); + Flux flux = Flux.just(foo, bar, baz); Flux result = DataBufferUtils.takeUntilByteCount(flux, 5L); @@ -116,7 +101,9 @@ public class DataBufferUtilsTests extends AbstractAllocatingTestCase { testSubscriber.bindTo(result). assertNoError(). assertComplete(). - assertValues(stringBuffer("foo"), stringBuffer("ba")); + assertValuesWith(stringConsumer("foo"), stringConsumer("ba")); + + release(bar, baz); } From cc8b2109a91d6b98016b3d644e24ff9ac1c603e3 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Thu, 28 Apr 2016 16:12:35 +0200 Subject: [PATCH 333/498] Removed DataBufferUtils.toInputStream Replaced all usages of toInputStream with non-blocking alternatives. --- .../core/codec/support/ResourceDecoder.java | 26 +-- .../core/codec/support/XmlEventDecoder.java | 33 ++-- .../io/buffer/support/DataBufferUtils.java | 21 --- .../util/CollectionUtils2.java | 60 ------- .../codec/support/XmlEventDecoderTests.java | 24 ++- .../reactive/XmlHandlerIntegrationTests.java | 170 ------------------ 6 files changed, 57 insertions(+), 277 deletions(-) delete mode 100644 spring-web-reactive/src/main/java/org/springframework/util/CollectionUtils2.java delete mode 100644 spring-web-reactive/src/test/java/org/springframework/http/server/reactive/XmlHandlerIntegrationTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ResourceDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ResourceDecoder.java index a9f60b4ac2..40ef6d0dbe 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ResourceDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ResourceDecoder.java @@ -16,7 +16,7 @@ package org.springframework.core.codec.support; -import java.io.InputStream; +import java.io.ByteArrayInputStream; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -55,20 +55,24 @@ public class ResourceDecoder extends AbstractDecoder { MimeType mimeType, Object... hints) { Class clazz = type.getRawClass(); - Flux body = Flux.from(inputStream); + Mono byteArray = Flux.from(inputStream). + reduce(DataBuffer::write). + map(dataBuffer -> { + byte[] bytes = new byte[dataBuffer.readableByteCount()]; + dataBuffer.read(bytes); + DataBufferUtils.release(dataBuffer); + return bytes; + }); + if (InputStreamResource.class.equals(clazz)) { - InputStream is = DataBufferUtils.toInputStream(body); - return Flux.just(new InputStreamResource(is)); + return Flux.from(byteArray. + map(ByteArrayInputStream::new). + map(InputStreamResource::new)); } else if (clazz.isAssignableFrom(ByteArrayResource.class)) { - Mono singleBuffer = body.reduce(DataBuffer::write); - return Flux.from(singleBuffer.map(buffer -> { - byte[] bytes = new byte[buffer.readableByteCount()]; - buffer.read(bytes); - DataBufferUtils.release(buffer); - return new ByteArrayResource(bytes); - })); + return Flux.from(byteArray. + map(ByteArrayResource::new)); } else { return Flux.error(new IllegalStateException( diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/XmlEventDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/XmlEventDecoder.java index 36a1badb28..b0174ad4c7 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/XmlEventDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/XmlEventDecoder.java @@ -16,7 +16,6 @@ package org.springframework.core.codec.support; -import java.io.InputStream; import java.util.ArrayList; import java.util.List; import java.util.function.Function; @@ -76,6 +75,8 @@ public class XmlEventDecoder extends AbstractDecoder { private static final XMLInputFactory inputFactory = XMLInputFactory.newFactory(); + boolean useAalto = true; + public XmlEventDecoder() { super(MimeTypeUtils.APPLICATION_XML, MimeTypeUtils.TEXT_XML); } @@ -83,21 +84,25 @@ public class XmlEventDecoder extends AbstractDecoder { @Override public Flux decode(Publisher inputStream, ResolvableType type, MimeType mimeType, Object... hints) { - if (aaltoPresent) { - return Flux.from(inputStream).flatMap(new AaltoDataBufferToXmlEvent()); + Flux flux = Flux.from(inputStream); + if (useAalto && aaltoPresent) { + return flux.flatMap(new AaltoDataBufferToXmlEvent()); } else { - try { - InputStream blockingStream = DataBufferUtils.toInputStream(inputStream); - - XMLEventReader eventReader = - inputFactory.createXMLEventReader(blockingStream); - - return Flux.fromIterable((Iterable) () -> eventReader); - } - catch (XMLStreamException ex) { - return Flux.error(ex); - } + Mono singleBuffer = flux.reduce(DataBuffer::write); + return singleBuffer. + map(DataBuffer::asInputStream). + flatMap(is -> { + try { + XMLEventReader eventReader = + inputFactory.createXMLEventReader(is); + return Flux + .fromIterable((Iterable) () -> eventReader); + } + catch (XMLStreamException ex) { + return Mono.error(ex); + } + }); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java index 2bb2bcdf0c..8da723e655 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java @@ -19,11 +19,9 @@ package org.springframework.core.io.buffer.support; import java.io.Closeable; import java.io.IOException; import java.io.InputStream; -import java.io.SequenceInputStream; import java.nio.ByteBuffer; import java.nio.channels.Channels; import java.nio.channels.ReadableByteChannel; -import java.util.Enumeration; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Consumer; @@ -37,7 +35,6 @@ import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferAllocator; import org.springframework.core.io.buffer.PooledDataBuffer; import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils2; /**i * Utility class for working with {@link DataBuffer}s. @@ -56,24 +53,6 @@ public abstract class DataBufferUtils { } }; - - /** - * Returns the given data buffer publisher as a blocking input stream, streaming over - * all underlying buffers when available. - * @param publisher the publisher to create the input stream for - * @return the input stream - */ - public static InputStream toInputStream(Publisher publisher) { - Iterable streams = Flux.from(publisher). - map(DataBuffer::asInputStream). - toIterable(1); - - Enumeration enumeration = - CollectionUtils2.toEnumeration(streams.iterator()); - - return new SequenceInputStream(enumeration); - } - /** * Reads the given {@code ReadableByteChannel} into a {@code Flux} of * {@code DataBuffer}s. Closes the channel when the flux is terminated. diff --git a/spring-web-reactive/src/main/java/org/springframework/util/CollectionUtils2.java b/spring-web-reactive/src/main/java/org/springframework/util/CollectionUtils2.java deleted file mode 100644 index 45f2c0983a..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/util/CollectionUtils2.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * 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.util; - -import java.util.Enumeration; -import java.util.Iterator; - -/** - * TODO: to be merged with {@link CollectionUtils} - * @author Arjen Poutsma - */ -public abstract class CollectionUtils2 { - - /** - * Adapt an iterator to an enumeration. - * @param iterator the iterator - * @return the enumeration - */ - public static Enumeration toEnumeration(Iterator iterator) { - return new IteratorEnumeration(iterator); - } - - /** - * Enumeration wrapping an Iterator. - */ - private static class IteratorEnumeration implements Enumeration { - - private final Iterator iterator; - - public IteratorEnumeration(Iterator iterator) { - this.iterator = iterator; - } - - @Override - public boolean hasMoreElements() { - return this.iterator.hasNext(); - } - - @Override - public T nextElement() { - return this.iterator.next(); - } - } - - -} diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/XmlEventDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/XmlEventDecoderTests.java index f1aec58dde..e1e0185b3a 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/XmlEventDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/XmlEventDecoderTests.java @@ -41,7 +41,7 @@ public class XmlEventDecoderTests extends AbstractDataBufferAllocatingTestCase { private XmlEventDecoder decoder = new XmlEventDecoder(); @Test - public void toXMLEvents() { + public void toXMLEventsAalto() { Flux events = this.decoder.decode(Flux.just(stringBuffer(XML)), null, null); @@ -61,6 +61,28 @@ public class XmlEventDecoderTests extends AbstractDataBufferAllocatingTestCase { e -> assertEndElement(e, "pojo")); } + @Test + public void toXMLEventsNonAalto() { + decoder.useAalto = false; + + Flux events = + this.decoder.decode(Flux.just(stringBuffer(XML)), null, null); + + TestSubscriber testSubscriber = new TestSubscriber<>(); + testSubscriber.bindTo(events). + assertNoError(). + assertComplete(). + assertValuesWith(e -> assertTrue(e.isStartDocument()), + e -> assertStartElement(e, "pojo"), + e -> assertStartElement(e, "foo"), + e -> assertCharacters(e, "foofoo"), + e -> assertEndElement(e, "foo"), + e -> assertStartElement(e, "bar"), + e -> assertCharacters(e, "barbar"), + e -> assertEndElement(e, "bar"), e -> assertEndElement(e, "pojo"), + e -> assertTrue(e.isEndDocument())); + } + private static void assertStartElement(XMLEvent event, String expectedLocalName) { assertTrue(event.isStartElement()); assertEquals(expectedLocalName, event.asStartElement().getName().getLocalPart()); diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/XmlHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/XmlHandlerIntegrationTests.java deleted file mode 100644 index 502b9f9176..0000000000 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/XmlHandlerIntegrationTests.java +++ /dev/null @@ -1,170 +0,0 @@ -/* - * 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.http.server.reactive; - -import java.io.InputStream; -import java.io.OutputStream; -import java.net.URI; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import javax.xml.bind.JAXBContext; -import javax.xml.bind.Marshaller; -import javax.xml.bind.Unmarshaller; -import javax.xml.bind.annotation.XmlRootElement; - -import org.junit.Test; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DefaultDataBufferAllocator; -import org.springframework.core.io.buffer.support.DataBufferUtils; -import org.springframework.http.MediaType; -import org.springframework.http.RequestEntity; -import org.springframework.http.ResponseEntity; -import org.springframework.http.server.reactive.boot.ReactorHttpServer; -import org.springframework.http.server.reactive.boot.RxNettyHttpServer; -import org.springframework.web.client.RestTemplate; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assume.assumeFalse; - -/** - * @author Arjen Poutsma - */ -public class XmlHandlerIntegrationTests extends AbstractHttpHandlerIntegrationTests { - - private final XmlHandler handler = new XmlHandler(); - - @Override - protected HttpHandler createHttpHandler() { - return handler; - } - - @Test - public void xml() throws Exception { - // TODO: fix Reactor and RxNetty support - assumeFalse(server instanceof ReactorHttpServer || - server instanceof RxNettyHttpServer); - - RestTemplate restTemplate = new RestTemplate(); - - Person johnDoe = new Person("John Doe"); - Person janeDoe = new Person("Jane Doe"); - - RequestEntity request = RequestEntity.post(new URI("http://localhost:" + port)).body( - johnDoe); - ResponseEntity response = restTemplate.exchange(request, Person.class); - assertEquals(janeDoe, response.getBody()); - - handler.requestComplete.await(10, TimeUnit.SECONDS); - if (handler.requestError != null) { - throw handler.requestError; - } - assertEquals(johnDoe, handler.requestPerson); - - } - - private static class XmlHandler implements HttpHandler { - - private CountDownLatch requestComplete = new CountDownLatch(1); - - private Person requestPerson; - - private Exception requestError; - - - @Override - public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { - requestError = null; - try { - JAXBContext jaxbContext = JAXBContext.newInstance(Person.class); - Marshaller marshaller = jaxbContext.createMarshaller(); - - Runnable r = () -> { - try { - InputStream bis = - DataBufferUtils.toInputStream(request.getBody()); - - Unmarshaller unmarshaller = jaxbContext.createUnmarshaller(); - requestPerson = (Person) unmarshaller.unmarshal(bis); - - } - catch (Exception ex) { - requestError = ex; - } - finally { - requestComplete.countDown(); - } - }; - - Thread t = new Thread(r); - t.start(); - - response.getHeaders().setContentType(MediaType.APPLICATION_XML); - Person janeDoe = new Person("Jane Doe"); - - DataBuffer buffer = new DefaultDataBufferAllocator().allocateBuffer(); - OutputStream bos = buffer.asOutputStream(); - marshaller.marshal(janeDoe, bos); - bos.close(); - - return response.setBody(Flux.just(buffer)); - } - catch (Exception ex) { - return Mono.error(ex); - } - } - } - - @XmlRootElement - private static class Person { - - private String name; - - public Person() { - } - - public Person(String name) { - this.name = name; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - @Override - public boolean equals(Object o) { - return name.equals(((Person) o).name); - } - - @Override - public int hashCode() { - return name.hashCode(); - } - - @Override - public String toString() { - return name; - } - } - -} From eb21f3ab0a4eb4cec6f2c5594a71f6b81ea36987 Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Thu, 28 Apr 2016 22:38:26 +0100 Subject: [PATCH 334/498] remove lift() --- .../io/buffer/support/DataBufferUtils.java | 102 ++++++++++-------- 1 file changed, 59 insertions(+), 43 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java index 8da723e655..ad50c7b3ef 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java @@ -29,6 +29,7 @@ import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import reactor.core.publisher.Flux; +import reactor.core.publisher.FluxSource; import reactor.core.subscriber.SubscriberWithContext; import org.springframework.core.io.buffer.DataBuffer; @@ -105,49 +106,7 @@ public abstract class DataBufferUtils { Assert.notNull(publisher, "'publisher' must not be null"); Assert.isTrue(maxByteCount >= 0, "'maxByteCount' must be a positive number"); - return Flux.from(publisher).lift(subscriber -> new Subscriber() { - - private Subscription subscription; - - private final AtomicLong byteCount = new AtomicLong(); - - @Override - public void onSubscribe(Subscription s) { - this.subscription = s; - subscriber.onSubscribe(s); - } - - @Override - public void onNext(DataBuffer dataBuffer) { - int delta = dataBuffer.readableByteCount(); - long currentCount = this.byteCount.addAndGet(delta); - if (currentCount > maxByteCount) { - int size = (int) (maxByteCount - currentCount + delta); - ByteBuffer byteBuffer = - (ByteBuffer) dataBuffer.asByteBuffer().limit(size); - DataBuffer partialBuffer = - dataBuffer.allocator().allocateBuffer(size); - partialBuffer.write(byteBuffer); - - subscriber.onNext(partialBuffer); - subscriber.onComplete(); - this.subscription.cancel(); - } - else { - subscriber.onNext(dataBuffer); - } - } - - @Override - public void onError(Throwable t) { - subscriber.onError(t); - } - - @Override - public void onComplete() { - subscriber.onComplete(); - } - }); + return new TakeByteUntilCount(publisher, maxByteCount); } /** @@ -162,6 +121,63 @@ public abstract class DataBufferUtils { return false; } + private static final class TakeByteUntilCount extends FluxSource { + + final long maxByteCount; + + TakeByteUntilCount(Publisher source, long maxByteCount) { + super(source); + this.maxByteCount = maxByteCount; + } + + @Override + public void subscribe(Subscriber subscriber) { + source.subscribe(new Subscriber() { + + private Subscription subscription; + + private final AtomicLong byteCount = new AtomicLong(); + + @Override + public void onSubscribe(Subscription s) { + this.subscription = s; + subscriber.onSubscribe(s); + } + + @Override + public void onNext(DataBuffer dataBuffer) { + int delta = dataBuffer.readableByteCount(); + long currentCount = this.byteCount.addAndGet(delta); + if (currentCount > maxByteCount) { + int size = (int) (maxByteCount - currentCount + delta); + ByteBuffer byteBuffer = + (ByteBuffer) dataBuffer.asByteBuffer().limit(size); + DataBuffer partialBuffer = + dataBuffer.allocator().allocateBuffer(size); + partialBuffer.write(byteBuffer); + + subscriber.onNext(partialBuffer); + subscriber.onComplete(); + this.subscription.cancel(); + } + else { + subscriber.onNext(dataBuffer); + } + } + + @Override + public void onError(Throwable t) { + subscriber.onError(t); + } + + @Override + public void onComplete() { + subscriber.onComplete(); + } + }); + } + } + private static class ReadableByteChannelConsumer implements Consumer> { From b2d9e7c28ddb7f122b402722bca94c2aabdb56df Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Mon, 2 May 2016 22:56:18 +0100 Subject: [PATCH 335/498] Split FluxProcessor factories into individual Processor, remove confusing factories --- .../web/server/adapter/DefaultServerWebExchange.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java b/spring-web-reactive/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java index 8e9807491c..295db4f880 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java @@ -19,8 +19,6 @@ import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; -import reactor.core.publisher.EmitterProcessor; -import reactor.core.publisher.FluxProcessor; import reactor.core.publisher.Mono; import org.springframework.http.server.reactive.ServerHttpRequest; @@ -89,8 +87,7 @@ public class DefaultServerWebExchange implements ServerWebExchange { if (this.sessionMono == null) { synchronized (this.createSessionLock) { if (this.sessionMono == null) { - FluxProcessor replay = EmitterProcessor.replay(1); - this.sessionMono = this.sessionManager.getSession(this).subscribeWith(replay).next(); + this.sessionMono = this.sessionManager.getSession(this).cache(); } } } From b71f143dfd622e9e3b7742e032ce982ec00a94b2 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Tue, 3 May 2016 16:33:56 +0200 Subject: [PATCH 336/498] Polishing --- .../io/buffer/support/DataBufferUtils.java | 42 ++++++++----------- 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java index ad50c7b3ef..28ce6781c0 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java @@ -16,7 +16,6 @@ package org.springframework.core.io.buffer.support; -import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; @@ -44,33 +43,16 @@ import org.springframework.util.Assert; */ public abstract class DataBufferUtils { - private static final Consumer CLOSE_CONSUMER = closeable -> { + private static final Consumer CLOSE_CONSUMER = channel -> { try { - if (closeable != null) { - closeable.close(); + if (channel != null) { + channel.close(); } } catch (IOException ignored) { } }; - /** - * Reads the given {@code ReadableByteChannel} into a {@code Flux} of - * {@code DataBuffer}s. Closes the channel when the flux is terminated. - * @param channel the channel to read from - * @param allocator the allocator to create data buffers with - * @param bufferSize the maximum size of the data buffers - * @return a flux of data buffers read from the given channel - */ - public static Flux read(ReadableByteChannel channel, - DataBufferAllocator allocator, int bufferSize) { - Assert.notNull(channel, "'channel' must not be null"); - Assert.notNull(allocator, "'allocator' must not be null"); - - return Flux.create(new ReadableByteChannelConsumer(allocator, bufferSize), - subscriber -> channel, closeConsumer()); - } - /** * Reads the given {@code InputStream} into a {@code Flux} of * {@code DataBuffer}s. Closes the stream when the flux inputStream terminated. @@ -88,9 +70,21 @@ public abstract class DataBufferUtils { return read(channel, allocator, bufferSize); } - @SuppressWarnings("unchecked") - private static Consumer closeConsumer() { - return (Consumer) CLOSE_CONSUMER; + /** + * Reads the given {@code ReadableByteChannel} into a {@code Flux} of + * {@code DataBuffer}s. Closes the channel when the flux is terminated. + * @param channel the channel to read from + * @param allocator the allocator to create data buffers with + * @param bufferSize the maximum size of the data buffers + * @return a flux of data buffers read from the given channel + */ + public static Flux read(ReadableByteChannel channel, + DataBufferAllocator allocator, int bufferSize) { + Assert.notNull(channel, "'channel' must not be null"); + Assert.notNull(allocator, "'allocator' must not be null"); + + return Flux.create(new ReadableByteChannelConsumer(allocator, bufferSize), + subscriber -> channel, CLOSE_CONSUMER); } /** From 04f47da15e4d1511e1169fa2b0e77bbf147e8f1c Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Tue, 3 May 2016 23:06:07 +0100 Subject: [PATCH 337/498] Synchronize Reactor Flux#create / Flux#generate changes --- .../codec/support/JacksonJsonEncoder.java | 4 ++-- .../io/buffer/support/DataBufferUtils.java | 20 +++++++++++-------- .../reactive/ChannelSendOperatorTests.java | 7 ++++--- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonEncoder.java index fc08cfd878..81e317b114 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonEncoder.java @@ -73,8 +73,8 @@ public class JacksonJsonEncoder extends AbstractEncoder { else { // array Mono startArray = Mono.just(allocator.wrap(START_ARRAY_BUFFER)); - Flux arraySeparators = - Flux.create(sub -> sub.onNext(allocator.wrap(SEPARATOR_BUFFER))); + Flux arraySeparators = Mono.just(allocator.wrap(SEPARATOR_BUFFER)) + .repeat(); Mono endArray = Mono.just(allocator.wrap(END_ARRAY_BUFFER)); Flux serializedObjects = diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java index 28ce6781c0..2b0293f8eb 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java @@ -22,6 +22,7 @@ import java.nio.ByteBuffer; import java.nio.channels.Channels; import java.nio.channels.ReadableByteChannel; import java.util.concurrent.atomic.AtomicLong; +import java.util.function.BiFunction; import java.util.function.Consumer; import org.reactivestreams.Publisher; @@ -29,7 +30,7 @@ import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import reactor.core.publisher.Flux; import reactor.core.publisher.FluxSource; -import reactor.core.subscriber.SubscriberWithContext; +import reactor.core.publisher.GenerateOutput; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferAllocator; @@ -83,8 +84,9 @@ public abstract class DataBufferUtils { Assert.notNull(channel, "'channel' must not be null"); Assert.notNull(allocator, "'allocator' must not be null"); - return Flux.create(new ReadableByteChannelConsumer(allocator, bufferSize), - subscriber -> channel, CLOSE_CONSUMER); + return Flux.generate(() -> channel, + new ReadableByteChannelGenerator(allocator, bufferSize), + CLOSE_CONSUMER); } /** @@ -172,24 +174,25 @@ public abstract class DataBufferUtils { } } - private static class ReadableByteChannelConsumer - implements Consumer> { + private static class ReadableByteChannelGenerator + implements BiFunction, + ReadableByteChannel> { private final DataBufferAllocator allocator; private final int chunkSize; - public ReadableByteChannelConsumer(DataBufferAllocator allocator, int chunkSize) { + public ReadableByteChannelGenerator(DataBufferAllocator allocator, int chunkSize) { this.allocator = allocator; this.chunkSize = chunkSize; } @Override - public void accept(SubscriberWithContext sub) { + public ReadableByteChannel apply(ReadableByteChannel + channel, GenerateOutput sub) { try { ByteBuffer byteBuffer = ByteBuffer.allocate(chunkSize); int read; - ReadableByteChannel channel = sub.context(); if ((read = channel.read(byteBuffer)) > 0) { byteBuffer.flip(); boolean release = true; @@ -212,6 +215,7 @@ public abstract class DataBufferUtils { catch (IOException ex) { sub.onError(ex); } + return channel; } } diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ChannelSendOperatorTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ChannelSendOperatorTests.java index 70fdc8b20f..d5ce4dc018 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ChannelSendOperatorTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ChannelSendOperatorTests.java @@ -108,13 +108,14 @@ public class ChannelSendOperatorTests { @Test public void errorAfterMultipleItems() throws Exception { IllegalStateException error = new IllegalStateException("boo"); - Flux publisher = Flux.create(subscriber -> { - int i = subscriber.context().incrementAndGet(); + Flux publisher = Flux.generate(() -> 0, (idx , subscriber) -> { + int i = ++idx; subscriber.onNext(String.valueOf(i)); if (i == 3) { subscriber.onError(error); } - }, subscriber -> new AtomicInteger()); + return i; + }); Mono completion = publisher.as(this::sendOperator); Signal signal = completion.materialize().get(); From 4e8c21e85a9fc3e248fdbda4d3beb6a571838f45 Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Wed, 4 May 2016 01:37:11 +0100 Subject: [PATCH 338/498] Sync Reactor SignalEmitter --- .../core/io/buffer/support/DataBufferUtils.java | 14 +++++++------- .../server/reactive/ChannelSendOperatorTests.java | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java index 2b0293f8eb..3c9aedd5d9 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java @@ -30,7 +30,7 @@ import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import reactor.core.publisher.Flux; import reactor.core.publisher.FluxSource; -import reactor.core.publisher.GenerateOutput; +import reactor.core.subscriber.SignalEmitter; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferAllocator; @@ -175,8 +175,8 @@ public abstract class DataBufferUtils { } private static class ReadableByteChannelGenerator - implements BiFunction, - ReadableByteChannel> { + implements BiFunction, + ReadableByteChannel> { private final DataBufferAllocator allocator; @@ -189,7 +189,7 @@ public abstract class DataBufferUtils { @Override public ReadableByteChannel apply(ReadableByteChannel - channel, GenerateOutput sub) { + channel, SignalEmitter sub) { try { ByteBuffer byteBuffer = ByteBuffer.allocate(chunkSize); int read; @@ -200,7 +200,7 @@ public abstract class DataBufferUtils { try { dataBuffer.write(byteBuffer); release = false; - sub.onNext(dataBuffer); + sub.emit(dataBuffer); } finally { if (release) { @@ -209,11 +209,11 @@ public abstract class DataBufferUtils { } } else { - sub.onComplete(); + sub.complete(); } } catch (IOException ex) { - sub.onError(ex); + sub.fail(ex); } return channel; } diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ChannelSendOperatorTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ChannelSendOperatorTests.java index d5ce4dc018..c65fe4facb 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ChannelSendOperatorTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ChannelSendOperatorTests.java @@ -110,9 +110,9 @@ public class ChannelSendOperatorTests { IllegalStateException error = new IllegalStateException("boo"); Flux publisher = Flux.generate(() -> 0, (idx , subscriber) -> { int i = ++idx; - subscriber.onNext(String.valueOf(i)); + subscriber.tryEmit(String.valueOf(i)); if (i == 3) { - subscriber.onError(error); + subscriber.fail(error); } return i; }); From 4d9126a474ca9a06ce53740db509c59cc59b3f6b Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 2 May 2016 13:54:27 -0400 Subject: [PATCH 339/498] Add support for more annotated method arguments - @PathVariable - @RequestHeader - @RequestParam - @CookieValue - @Value - @RequestAttribute - @SessionAttribute --- spring-web-reactive/build.gradle | 2 +- ...tractNamedValueMethodArgumentResolver.java | 290 ++++++++++++++++++ .../CookieValueMethodArgumentResolver.java | 93 ++++++ ...ExpressionValueMethodArgumentResolver.java | 81 +++++ ...PathVariableMapMethodArgumentResolver.java | 64 ++++ .../PathVariableMethodArgumentResolver.java | 114 +++++++ ...equestAttributeMethodArgumentResolver.java | 68 ++++ ...equestHeaderMapMethodArgumentResolver.java | 62 ++++ .../RequestHeaderMethodArgumentResolver.java | 97 ++++++ .../RequestMappingHandlerAdapter.java | 57 +++- .../RequestParamArgumentResolver.java | 52 ---- ...RequestParamMapMethodArgumentResolver.java | 69 +++++ .../RequestParamMethodArgumentResolver.java | 125 ++++++++ ...essionAttributeMethodArgumentResolver.java | 71 +++++ .../web/server/ServerErrorException.java | 65 ++++ .../method/InvocableHandlerMethodTests.java | 8 +- ...equestAttributesArgumentResolverTests.java | 177 +++++++++++ ...ookieValueMethodArgumentResolverTests.java | 132 ++++++++ ...ssionValueMethodArgumentResolverTests.java | 104 +++++++ ...ariableMapMethodArgumentResolverTests.java | 112 +++++++ ...thVariableMethodArgumentResolverTests.java | 110 +++++++ ...tAttributeMethodArgumentResolverTests.java | 163 ++++++++++ ...tHeaderMapMethodArgumentResolverTests.java | 150 +++++++++ ...uestHeaderMethodArgumentResolverTests.java | 229 ++++++++++++++ ...stParamMapMethodArgumentResolverTests.java | 129 ++++++++ ...questParamMethodArgumentResolverTests.java | 232 ++++++++++++++ ...nAttributeMethodArgumentResolverTests.java | 170 ++++++++++ 27 files changed, 2960 insertions(+), 66 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractNamedValueMethodArgumentResolver.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/CookieValueMethodArgumentResolver.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ExpressionValueMethodArgumentResolver.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/PathVariableMapMethodArgumentResolver.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/PathVariableMethodArgumentResolver.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestAttributeMethodArgumentResolver.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMapMethodArgumentResolver.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolver.java delete mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestParamArgumentResolver.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestParamMapMethodArgumentResolver.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestParamMethodArgumentResolver.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/SessionAttributeMethodArgumentResolver.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/server/ServerErrorException.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/AbstractRequestAttributesArgumentResolverTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/CookieValueMethodArgumentResolverTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ExpressionValueMethodArgumentResolverTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/PathVariableMapMethodArgumentResolverTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/PathVariableMethodArgumentResolverTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestAttributeMethodArgumentResolverTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMapMethodArgumentResolverTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolverTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestParamMapMethodArgumentResolverTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestParamMethodArgumentResolverTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SessionAttributeMethodArgumentResolverTests.java diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index 76c8db41f9..b0f5771525 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -27,7 +27,7 @@ repositories { } ext { - springVersion = '4.3.0.RC1' + springVersion = '4.3.0.BUILD-SNAPSHOT' reactorVersion = '2.5.0.BUILD-SNAPSHOT' reactorNettyVersion = '2.5.0.BUILD-SNAPSHOT' tomcatVersion = '8.0.33' diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractNamedValueMethodArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractNamedValueMethodArgumentResolver.java new file mode 100644 index 0000000000..1db3ca782a --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractNamedValueMethodArgumentResolver.java @@ -0,0 +1,290 @@ +/* + * 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.web.reactive.result.method.annotation; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import reactor.core.publisher.Mono; + +import org.springframework.beans.ConversionNotSupportedException; +import org.springframework.beans.SimpleTypeConverter; +import org.springframework.beans.TypeMismatchException; +import org.springframework.beans.factory.config.BeanExpressionContext; +import org.springframework.beans.factory.config.BeanExpressionResolver; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.ConversionService; +import org.springframework.ui.ModelMap; +import org.springframework.util.Assert; +import org.springframework.web.bind.annotation.ValueConstants; +import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver; +import org.springframework.web.server.ServerErrorException; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.ServerWebInputException; + +/** + * Abstract base class for resolving method arguments from a named value. + * Request parameters, request headers, and path variables are examples of named + * values. Each may have a name, a required flag, and a default value. + *

    Subclasses define how to do the following: + *

      + *
    • Obtain named value information for a method parameter + *
    • Resolve names into argument values + *
    • Handle missing argument values when argument values are required + *
    • Optionally handle a resolved value + *
    + *

    A default value string can contain ${...} placeholders and Spring Expression + * Language #{...} expressions. For this to work a + * {@link ConfigurableBeanFactory} must be supplied to the class constructor. + * + * @author Rossen Stoyanchev + */ +public abstract class AbstractNamedValueMethodArgumentResolver implements HandlerMethodArgumentResolver { + + private final ConfigurableBeanFactory configurableBeanFactory; + + private final BeanExpressionContext expressionContext; + + private final Map namedValueInfoCache = new ConcurrentHashMap<>(256); + + /** Instead of a WebDataBinder for now */ + private final SimpleTypeConverter typeConverter; + + + /** + * @param conversionService for type conversion (to be replaced with WebDataBinder) + * @param beanFactory a bean factory to use for resolving ${...} placeholder + * and #{...} SpEL expressions in default values, or {@code null} if default + * values are not expected to contain expressions + */ + public AbstractNamedValueMethodArgumentResolver(ConversionService conversionService, + ConfigurableBeanFactory beanFactory) { + + Assert.notNull(conversionService, "'conversionService' is required."); + this.typeConverter = new SimpleTypeConverter(); + this.typeConverter.setConversionService(conversionService); + this.configurableBeanFactory = beanFactory; + this.expressionContext = (beanFactory != null ? new BeanExpressionContext(beanFactory, null) : null); + } + + + @Override + public Mono resolveArgument(MethodParameter parameter, ModelMap model, ServerWebExchange exchange) { + NamedValueInfo namedValueInfo = getNamedValueInfo(parameter); + MethodParameter nestedParameter = parameter.nestedIfOptional(); + + Object resolvedName = resolveStringValue(namedValueInfo.name); + if (resolvedName == null) { + return Mono.error(new IllegalArgumentException( + "Specified name must not resolve to null: [" + namedValueInfo.name + "]")); + } + + return resolveName(resolvedName.toString(), nestedParameter, exchange) + .map(arg -> { + if ("".equals(arg) && namedValueInfo.defaultValue != null) { + arg = resolveStringValue(namedValueInfo.defaultValue); + } + arg = applyConversion(arg, parameter); + handleResolvedValue(arg, namedValueInfo.name, parameter, model, exchange); + return arg; + }) + .otherwiseIfEmpty(getDefaultValue(namedValueInfo, parameter, model, exchange)); + } + + /** + * Obtain the named value for the given method parameter. + */ + private NamedValueInfo getNamedValueInfo(MethodParameter parameter) { + NamedValueInfo namedValueInfo = this.namedValueInfoCache.get(parameter); + if (namedValueInfo == null) { + namedValueInfo = createNamedValueInfo(parameter); + namedValueInfo = updateNamedValueInfo(parameter, namedValueInfo); + this.namedValueInfoCache.put(parameter, namedValueInfo); + } + return namedValueInfo; + } + + /** + * Create the {@link NamedValueInfo} object for the given method parameter. + * Implementations typically retrieve the method annotation by means of + * {@link MethodParameter#getParameterAnnotation(Class)}. + * @param parameter the method parameter + * @return the named value information + */ + protected abstract NamedValueInfo createNamedValueInfo(MethodParameter parameter); + + /** + * Create a new NamedValueInfo based on the given NamedValueInfo with + * sanitized values. + */ + private NamedValueInfo updateNamedValueInfo(MethodParameter parameter, NamedValueInfo info) { + String name = info.name; + if (info.name.length() == 0) { + name = parameter.getParameterName(); + if (name == null) { + String type = parameter.getNestedParameterType().getName(); + throw new IllegalArgumentException("Name for argument type [" + type + "] not " + + "available, and parameter name information not found in class file either."); + } + } + String defaultValue = (ValueConstants.DEFAULT_NONE.equals(info.defaultValue) ? null : info.defaultValue); + return new NamedValueInfo(name, info.required, defaultValue); + } + + /** + * Resolve the given annotation-specified value, + * potentially containing placeholders and expressions. + */ + private Object resolveStringValue(String value) { + if (this.configurableBeanFactory == null) { + return value; + } + String placeholdersResolved = this.configurableBeanFactory.resolveEmbeddedValue(value); + BeanExpressionResolver exprResolver = this.configurableBeanFactory.getBeanExpressionResolver(); + if (exprResolver == null) { + return value; + } + return exprResolver.evaluate(placeholdersResolved, this.expressionContext); + } + + /** + * Resolve the given parameter type and value name into an argument value. + * @param name the name of the value being resolved + * @param parameter the method parameter to resolve to an argument value + * (pre-nested in case of a {@link java.util.Optional} declaration) + * @param exchange the current exchange + * @return the resolved argument (may be {@code null}) + */ + protected abstract Mono resolveName(String name, MethodParameter parameter, + ServerWebExchange exchange); + + private Object applyConversion(Object value, MethodParameter parameter) { + try { + value = this.typeConverter.convertIfNecessary(value, parameter.getParameterType(), parameter); + } + catch (ConversionNotSupportedException ex) { + throw new ServerErrorException("Conversion not supported.", parameter, ex); + } + catch (TypeMismatchException ex) { + throw new ServerWebInputException("Type mismatch.", parameter, ex); + } + return value; + } + + private Mono getDefaultValue(NamedValueInfo namedValueInfo, MethodParameter parameter, + ModelMap model, ServerWebExchange exchange) { + + Object value = null; + try { + if (namedValueInfo.defaultValue != null) { + value = resolveStringValue(namedValueInfo.defaultValue); + } + else if (namedValueInfo.required && !parameter.isOptional()) { + handleMissingValue(namedValueInfo.name, parameter, exchange); + } + value = handleNullValue(namedValueInfo.name, value, parameter.getNestedParameterType()); + value = applyConversion(value, parameter); + handleResolvedValue(value, namedValueInfo.name, parameter, model, exchange); + return Mono.justOrEmpty(value); + } + catch (Throwable ex) { + return Mono.error(ex); + } + } + + /** + * Invoked when a named value is required, but + * {@link #resolveName(String, MethodParameter, ServerWebExchange)} returned + * {@code null} and there is no default value. Subclasses typically throw an + * exception in this case. + * @param name the name for the value + * @param parameter the method parameter + * @param exchange the current exchange + */ + @SuppressWarnings("UnusedParameters") + protected void handleMissingValue(String name, MethodParameter parameter, ServerWebExchange exchange) { + handleMissingValue(name, parameter); + } + + /** + * Invoked when a named value is required, but + * {@link #resolveName(String, MethodParameter, ServerWebExchange)} returned + * {@code null} and there is no default value. Subclasses typically throw an + * exception in this case. + * @param name the name for the value + * @param parameter the method parameter + */ + protected void handleMissingValue(String name, MethodParameter parameter) { + String typeName = parameter.getNestedParameterType().getSimpleName(); + throw new ServerWebInputException("Missing argument '" + name + "' for method " + + "parameter of type " + typeName, parameter); + } + + /** + * A {@code null} results in a {@code false} value for {@code boolean}s or + * an exception for other primitives. + */ + private Object handleNullValue(String name, Object value, Class paramType) { + if (value == null) { + if (Boolean.TYPE.equals(paramType)) { + return Boolean.FALSE; + } + else if (paramType.isPrimitive()) { + throw new IllegalStateException("Optional " + paramType.getSimpleName() + + " parameter '" + name + "' is present but cannot be translated into a" + + " null value due to being declared as a primitive type. " + + "Consider declaring it as object wrapper for the corresponding primitive type."); + } + } + return value; + } + + /** + * Invoked after a value is resolved. + * @param arg the resolved argument value + * @param name the argument name + * @param parameter the argument parameter type + * @param model the model + * @param exchange the current exchange + */ + @SuppressWarnings("UnusedParameters") + protected void handleResolvedValue(Object arg, String name, MethodParameter parameter, + ModelMap model, ServerWebExchange exchange) { + } + + + /** + * Represents the information about a named value, including name, whether + * it's required and a default value. + */ + protected static class NamedValueInfo { + + private final String name; + + private final boolean required; + + private final String defaultValue; + + public NamedValueInfo(String name, boolean required, String defaultValue) { + this.name = name; + this.required = required; + this.defaultValue = defaultValue; + } + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/CookieValueMethodArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/CookieValueMethodArgumentResolver.java new file mode 100644 index 0000000000..48f239f9cc --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/CookieValueMethodArgumentResolver.java @@ -0,0 +1,93 @@ +/* + * 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.web.reactive.result.method.annotation; + +import reactor.core.publisher.Mono; + +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.ConversionService; +import org.springframework.http.HttpCookie; +import org.springframework.web.bind.annotation.CookieValue; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.ServerWebInputException; + +/** + * Resolve method arguments annotated with {@code @CookieValue}. + * + *

    An {@code @CookieValue} is a named value that is resolved from a cookie. + * It has a required flag and a default value to fall back on when the cookie + * does not exist. + * + * @author Rossen Stoyanchev + */ +public class CookieValueMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver { + + + /** + * @param beanFactory a bean factory to use for resolving ${...} + * placeholder and #{...} SpEL expressions in default values; + * or {@code null} if default values are not expected to contain expressions + */ + public CookieValueMethodArgumentResolver(ConversionService conversionService, + ConfigurableBeanFactory beanFactory) { + + super(conversionService, beanFactory); + } + + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(CookieValue.class); + } + + @Override + protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) { + CookieValue annotation = parameter.getParameterAnnotation(CookieValue.class); + return new CookieValueNamedValueInfo(annotation); + } + + @Override + protected Mono resolveName(String name, MethodParameter parameter, ServerWebExchange exchange) { + HttpCookie cookie = exchange.getRequest().getCookies().getFirst(name); + if (HttpCookie.class.isAssignableFrom(parameter.getNestedParameterType())) { + return Mono.justOrEmpty(cookie); + } + else if (cookie != null) { + return Mono.justOrEmpty(cookie.getValue()); + } + else { + return Mono.empty(); + } + } + + @Override + protected void handleMissingValue(String name, MethodParameter parameter) { + String type = parameter.getNestedParameterType().getSimpleName(); + String reason = "Missing cookie '" + name + "' for method parameter of type " + type; + throw new ServerWebInputException(reason, parameter); + } + + + private static class CookieValueNamedValueInfo extends NamedValueInfo { + + private CookieValueNamedValueInfo(CookieValue annotation) { + super(annotation.name(), annotation.required(), annotation.defaultValue()); + } + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ExpressionValueMethodArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ExpressionValueMethodArgumentResolver.java new file mode 100644 index 0000000000..fb6f22d659 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ExpressionValueMethodArgumentResolver.java @@ -0,0 +1,81 @@ +/* + * 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.web.reactive.result.method.annotation; + +import reactor.core.publisher.Mono; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.ConversionService; +import org.springframework.web.server.ServerWebExchange; + +/** + * Resolves method arguments annotated with {@code @Value}. + * + *

    An {@code @Value} does not have a name but gets resolved from the default + * value string, which may contain ${...} placeholder or Spring Expression + * Language #{...} expressions. + * + * @author Rossen Stoyanchev + */ +public class ExpressionValueMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver { + + + /** + * @param beanFactory a bean factory to use for resolving ${...} + * placeholder and #{...} SpEL expressions in default values; + * or {@code null} if default values are not expected to contain expressions + */ + public ExpressionValueMethodArgumentResolver(ConversionService conversionService, + ConfigurableBeanFactory beanFactory) { + + super(conversionService, beanFactory); + } + + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(Value.class); + } + + @Override + protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) { + Value annotation = parameter.getParameterAnnotation(Value.class); + return new ExpressionValueNamedValueInfo(annotation); + } + + @Override + protected Mono resolveName(String name, MethodParameter parameter, ServerWebExchange exchange) { + // No name to resolve + return Mono.empty(); + } + + @Override + protected void handleMissingValue(String name, MethodParameter parameter) { + throw new UnsupportedOperationException("@Value is never required: " + parameter.getMethod()); + } + + + private static class ExpressionValueNamedValueInfo extends NamedValueInfo { + + private ExpressionValueNamedValueInfo(Value annotation) { + super("@Value", false, annotation.value()); + } + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/PathVariableMapMethodArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/PathVariableMapMethodArgumentResolver.java new file mode 100644 index 0000000000..9488bfa69c --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/PathVariableMapMethodArgumentResolver.java @@ -0,0 +1,64 @@ +/* + * 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.web.reactive.result.method.annotation; + +import java.util.Collections; +import java.util.Map; +import java.util.Optional; + +import reactor.core.publisher.Mono; + +import org.springframework.core.MethodParameter; +import org.springframework.ui.ModelMap; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.reactive.HandlerMapping; +import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver; +import org.springframework.web.server.ServerWebExchange; + +/** + * Resolver for {@link Map} method arguments also annotated with + * {@link PathVariable @PathVariable} where the annotation does not specify a + * path variable name. The resulting {@link Map} argument is a coyp of all URI + * template name-value pairs. + * + * @author Rossen Stoyanchev + * @see PathVariableMethodArgumentResolver + */ +public class PathVariableMapMethodArgumentResolver implements HandlerMethodArgumentResolver { + + + @Override + public boolean supportsParameter(MethodParameter parameter) { + PathVariable ann = parameter.getParameterAnnotation(PathVariable.class); + return (ann != null && (Map.class.isAssignableFrom(parameter.getParameterType())) + && !StringUtils.hasText(ann.value())); + } + + /** + * Return a Map with all URI template variables or an empty map. + */ + @Override + public Mono resolveArgument(MethodParameter parameter, ModelMap model, + ServerWebExchange exchange) { + + String name = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE; + Optional value = exchange.getAttribute(name); + return (value.isPresent() ? Mono.just(value.get()) : Mono.just(Collections.emptyMap())); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/PathVariableMethodArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/PathVariableMethodArgumentResolver.java new file mode 100644 index 0000000000..fd59d53e7c --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/PathVariableMethodArgumentResolver.java @@ -0,0 +1,114 @@ +/* + * 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.web.reactive.result.method.annotation; + +import java.util.Map; +import java.util.Optional; + +import reactor.core.publisher.Mono; + +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.converter.Converter; +import org.springframework.ui.ModelMap; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.ValueConstants; +import org.springframework.web.reactive.HandlerMapping; +import org.springframework.web.server.ServerErrorException; +import org.springframework.web.server.ServerWebExchange; + +/** + * Resolves method arguments annotated with @{@link PathVariable}. + * + *

    An @{@link PathVariable} is a named value that gets resolved from a URI + * template variable. It is always required and does not have a default value + * to fall back on. See the base class + * {@link org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver} + * for more information on how named values are processed. + * + *

    If the method parameter type is {@link Map}, the name specified in the + * annotation is used to resolve the URI variable String value. The value is + * then converted to a {@link Map} via type conversion, assuming a suitable + * {@link Converter}. + * + * @author Rossen Stoyanchev + * @see PathVariableMapMethodArgumentResolver + */ +public class PathVariableMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver { + + + public PathVariableMethodArgumentResolver(ConversionService conversionService, + ConfigurableBeanFactory beanFactory) { + + super(conversionService, beanFactory); + } + + + @Override + public boolean supportsParameter(MethodParameter parameter) { + if (!parameter.hasParameterAnnotation(PathVariable.class)) { + return false; + } + if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) { + String paramName = parameter.getParameterAnnotation(PathVariable.class).value(); + return StringUtils.hasText(paramName); + } + return true; + } + + @Override + protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) { + PathVariable annotation = parameter.getParameterAnnotation(PathVariable.class); + return new PathVariableNamedValueInfo(annotation); + } + + @Override + @SuppressWarnings("unchecked") + protected Mono resolveName(String name, MethodParameter parameter, ServerWebExchange exchange) { + String attributeName = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE; + Optional optional = exchange.getAttribute(attributeName); + Object value = null; + if (optional.isPresent()) { + value = ((Map) optional.get()).get(name); + } + return Mono.justOrEmpty(value); + } + + @Override + protected void handleMissingValue(String name, MethodParameter parameter) { + throw new ServerErrorException(name, parameter); + } + + @Override + @SuppressWarnings("unchecked") + protected void handleResolvedValue(Object arg, String name, MethodParameter parameter, + ModelMap model, ServerWebExchange exchange) { + + // TODO: View.PATH_VARIABLES ? + } + + + private static class PathVariableNamedValueInfo extends NamedValueInfo { + + public PathVariableNamedValueInfo(PathVariable annotation) { + super(annotation.value(), true, ValueConstants.DEFAULT_NONE); + } + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestAttributeMethodArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestAttributeMethodArgumentResolver.java new file mode 100644 index 0000000000..f14e9fdd35 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestAttributeMethodArgumentResolver.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.web.reactive.result.method.annotation; + +import reactor.core.publisher.Mono; + +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.ConversionService; +import org.springframework.web.bind.annotation.RequestAttribute; +import org.springframework.web.bind.annotation.ValueConstants; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.ServerWebInputException; + +/** + * Resolves method arguments annotated with an @{@link RequestAttribute}. + * + * @author Rossen Stoyanchev + * @see SessionAttributeMethodArgumentResolver + */ +public class RequestAttributeMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver { + + + public RequestAttributeMethodArgumentResolver(ConversionService conversionService, + ConfigurableBeanFactory beanFactory) { + + super(conversionService, beanFactory); + } + + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(RequestAttribute.class); + } + + + @Override + protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) { + RequestAttribute annot = parameter.getParameterAnnotation(RequestAttribute.class); + return new NamedValueInfo(annot.name(), annot.required(), ValueConstants.DEFAULT_NONE); + } + + @Override + protected Mono resolveName(String name, MethodParameter parameter, ServerWebExchange exchange){ + return Mono.justOrEmpty(exchange.getAttribute(name)); + } + + @Override + protected void handleMissingValue(String name, MethodParameter parameter) { + String type = parameter.getNestedParameterType().getSimpleName(); + String reason = "Missing request attribute '" + name + "' of type " + type; + throw new ServerWebInputException(reason, parameter); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMapMethodArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMapMethodArgumentResolver.java new file mode 100644 index 0000000000..4f3820e9c2 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMapMethodArgumentResolver.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.web.reactive.result.method.annotation; + +import java.util.Map; + +import reactor.core.publisher.Mono; + +import org.springframework.core.MethodParameter; +import org.springframework.http.HttpHeaders; +import org.springframework.ui.ModelMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver; +import org.springframework.web.server.ServerWebExchange; + +/** + * Resolves {@link Map} method arguments annotated with {@code @RequestHeader}. + * For individual header values annotated with {@code @RequestHeader} see + * {@link RequestHeaderMethodArgumentResolver} instead. + * + *

    The created {@link Map} contains all request header name/value pairs. + * The method parameter type may be a {@link MultiValueMap} to receive all + * values for a header, not only the first one. + * + * @author Rossen Stoyanchev + * @see RequestHeaderMethodArgumentResolver + */ +public class RequestHeaderMapMethodArgumentResolver implements HandlerMethodArgumentResolver { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return (parameter.hasParameterAnnotation(RequestHeader.class) && + Map.class.isAssignableFrom(parameter.getParameterType())); + } + + @Override + public Mono resolveArgument(MethodParameter parameter, ModelMap model, ServerWebExchange exchange) { + HttpHeaders headers = exchange.getRequest().getHeaders(); + if (MultiValueMap.class.isAssignableFrom(parameter.getParameterType())) { + return Mono.just(headers); + } + else { + return Mono.just(headers.toSingleValueMap()); + } + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolver.java new file mode 100644 index 0000000000..4f3cd089c5 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolver.java @@ -0,0 +1,97 @@ +/* + * 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.web.reactive.result.method.annotation; + +import java.util.List; +import java.util.Map; + +import reactor.core.publisher.Mono; + +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.ConversionService; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.ServerWebInputException; + +/** + * Resolves method arguments annotated with {@code @RequestHeader} except for + * {@link Map} arguments. See {@link RequestHeaderMapMethodArgumentResolver} for + * details on {@link Map} arguments annotated with {@code @RequestHeader}. + * + *

    An {@code @RequestHeader} is a named value resolved from a request header. + * It has a required flag and a default value to fall back on when the request + * header does not exist. + * + *

    A {@link ConversionService} is invoked to apply type conversion to resolved + * request header values that don't yet match the method parameter type. + * + * @author Rossen Stoyanchev + * @see RequestHeaderMapMethodArgumentResolver + */ +public class RequestHeaderMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver { + + /** + * @param beanFactory a bean factory to use for resolving ${...} + * placeholder and #{...} SpEL expressions in default values; + * or {@code null} if default values are not expected to have expressions + */ + public RequestHeaderMethodArgumentResolver(ConversionService conversionService, + ConfigurableBeanFactory beanFactory) { + + super(conversionService, beanFactory); + } + + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return (parameter.hasParameterAnnotation(RequestHeader.class) && + !Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())); + } + + @Override + protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) { + RequestHeader annotation = parameter.getParameterAnnotation(RequestHeader.class); + return new RequestHeaderNamedValueInfo(annotation); + } + + @Override + protected Mono resolveName(String name, MethodParameter parameter, ServerWebExchange exchange) { + List headerValues = exchange.getRequest().getHeaders().get(name); + Object result = null; + if (headerValues != null) { + result = (headerValues.size() == 1 ? headerValues.get(0) : headerValues); + } + return Mono.justOrEmpty(result); + } + + @Override + protected void handleMissingValue(String name, MethodParameter parameter) { + String type = parameter.getNestedParameterType().getSimpleName(); + throw new ServerWebInputException("Missing request header '" + name + + "' for method parameter of type " + type); + } + + + private static class RequestHeaderNamedValueInfo extends NamedValueInfo { + + private RequestHeaderNamedValueInfo(RequestHeader annotation) { + super(annotation.name(), annotation.required(), annotation.defaultValue()); + } + } + +} 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 d9d2527e62..b6c0ec6848 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 @@ -28,7 +28,11 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import reactor.core.publisher.Mono; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.core.codec.support.ByteBufferDecoder; import org.springframework.core.codec.support.ByteBufferEncoder; import org.springframework.core.codec.support.JacksonJsonDecoder; @@ -57,7 +61,7 @@ import org.springframework.web.server.ServerWebExchange; /** * @author Rossen Stoyanchev */ -public class RequestMappingHandlerAdapter implements HandlerAdapter, InitializingBean { +public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactoryAware, InitializingBean { private static Log logger = LogFactory.getLog(RequestMappingHandlerAdapter.class); @@ -69,6 +73,9 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Initializin private final Map, ExceptionHandlerMethodResolver> exceptionHandlerCache = new ConcurrentHashMap<>(64); + private ConfigurableBeanFactory beanFactory; + + /** * Configure the complete list of supported argument types thus overriding @@ -94,24 +101,52 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Initializin return this.conversionService; } + /** + * A {@link ConfigurableBeanFactory} is expected for resolving expressions + * in method argument default values. + */ + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + if (beanFactory instanceof ConfigurableBeanFactory) { + this.beanFactory = (ConfigurableBeanFactory) beanFactory; + } + } + + public ConfigurableBeanFactory getBeanFactory() { + return beanFactory; + } + + @Override public void afterPropertiesSet() throws Exception { if (ObjectUtils.isEmpty(this.argumentResolvers)) { - List> messageConverters = Arrays.asList( - new CodecHttpMessageConverter(new ByteBufferEncoder(), - new ByteBufferDecoder()), - new CodecHttpMessageConverter(new StringEncoder(), - new StringDecoder()), - new CodecHttpMessageConverter(new Jaxb2Encoder(), - new Jaxb2Decoder()), + + List> converters = Arrays.asList( + new CodecHttpMessageConverter(new ByteBufferEncoder(), new ByteBufferDecoder()), + new CodecHttpMessageConverter(new StringEncoder(), new StringDecoder()), + new CodecHttpMessageConverter(new Jaxb2Encoder(), new Jaxb2Decoder()), new CodecHttpMessageConverter(new JacksonJsonEncoder(), new JacksonJsonDecoder(new JsonObjectDecoder()))); + // Annotation-based argument resolution + ConversionService cs = getConversionService(); + this.argumentResolvers.add(new RequestParamMethodArgumentResolver(cs, getBeanFactory(), false)); + this.argumentResolvers.add(new RequestParamMapMethodArgumentResolver()); + this.argumentResolvers.add(new PathVariableMethodArgumentResolver(cs, getBeanFactory())); + this.argumentResolvers.add(new PathVariableMapMethodArgumentResolver()); + this.argumentResolvers.add(new RequestBodyArgumentResolver(converters, cs)); + this.argumentResolvers.add(new RequestHeaderMethodArgumentResolver(cs, getBeanFactory())); + this.argumentResolvers.add(new RequestHeaderMapMethodArgumentResolver()); + this.argumentResolvers.add(new CookieValueMethodArgumentResolver(cs, getBeanFactory())); + this.argumentResolvers.add(new ExpressionValueMethodArgumentResolver(cs, getBeanFactory())); + this.argumentResolvers.add(new SessionAttributeMethodArgumentResolver(cs, getBeanFactory())); + this.argumentResolvers.add(new RequestAttributeMethodArgumentResolver(cs , getBeanFactory())); - this.argumentResolvers.add(new RequestParamArgumentResolver()); - this.argumentResolvers.add(new RequestBodyArgumentResolver(messageConverters, - this.conversionService)); + // Type-based argument resolution this.argumentResolvers.add(new ModelArgumentResolver()); + + // Catch-all + this.argumentResolvers.add(new RequestParamMethodArgumentResolver(cs, getBeanFactory(), true)); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestParamArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestParamArgumentResolver.java deleted file mode 100644 index b518e3f628..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestParamArgumentResolver.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2002-2015 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.web.reactive.result.method.annotation; - -import reactor.core.publisher.Mono; - -import org.springframework.core.MethodParameter; -import org.springframework.ui.ModelMap; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver; -import org.springframework.web.server.ServerWebExchange; -import org.springframework.web.util.UriComponents; -import org.springframework.web.util.UriComponentsBuilder; - - -/** - * Support {@code @RequestParam} but for query params only. - * - * @author Rossen Stoyanchev - */ -public class RequestParamArgumentResolver implements HandlerMethodArgumentResolver { - - - @Override - public boolean supportsParameter(MethodParameter parameter) { - return parameter.hasParameterAnnotation(RequestParam.class); - } - - - @Override - public Mono resolveArgument(MethodParameter param, ModelMap model, ServerWebExchange exchange) { - RequestParam annotation = param.getParameterAnnotation(RequestParam.class); - String name = (annotation.value().length() != 0 ? annotation.value() : param.getParameterName()); - UriComponents uriComponents = UriComponentsBuilder.fromUri(exchange.getRequest().getURI()).build(); - return Mono.justOrEmpty(uriComponents.getQueryParams().getFirst(name)); - } - -} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestParamMapMethodArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestParamMapMethodArgumentResolver.java new file mode 100644 index 0000000000..50df6d3c4a --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestParamMapMethodArgumentResolver.java @@ -0,0 +1,69 @@ +/* + * 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.web.reactive.result.method.annotation; + +import java.util.Map; + +import reactor.core.publisher.Mono; + +import org.springframework.core.MethodParameter; +import org.springframework.ui.ModelMap; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver; +import org.springframework.web.server.ServerWebExchange; + +/** + * Resolver for {@link Map} method arguments annotated with + * {@link RequestParam @RequestParam} where the annotation does not specify a + * request parameter name. See {@link RequestParamMethodArgumentResolver} for + * resolving {@link Map} method arguments with a request parameter name. + * + *

    The created {@link Map} contains all request parameter name-value pairs. + * If the method parameter type is {@link MultiValueMap} instead, the created + * map contains all request parameters and all there values for cases where + * request parameters have multiple values. + * + * @author Rossen Stoyanchev + * @see RequestParamMethodArgumentResolver + */ +public class RequestParamMapMethodArgumentResolver implements HandlerMethodArgumentResolver { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + RequestParam requestParam = parameter.getParameterAnnotation(RequestParam.class); + if (requestParam != null) { + if (Map.class.isAssignableFrom(parameter.getParameterType())) { + return !StringUtils.hasText(requestParam.name()); + } + } + return false; + } + + @Override + public Mono resolveArgument(MethodParameter parameter, ModelMap model, ServerWebExchange exchange) { + Class paramType = parameter.getParameterType(); + MultiValueMap queryParams = exchange.getRequest().getQueryParams(); + if (MultiValueMap.class.isAssignableFrom(paramType)) { + return Mono.just(queryParams); + } + else { + return Mono.just(queryParams.toSingleValueMap()); + } + } +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestParamMethodArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestParamMethodArgumentResolver.java new file mode 100644 index 0000000000..b55bb3c1a4 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestParamMethodArgumentResolver.java @@ -0,0 +1,125 @@ +/* + * 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.web.reactive.result.method.annotation; + +import java.util.List; +import java.util.Map; + +import reactor.core.publisher.Mono; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.converter.Converter; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ValueConstants; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.ServerWebInputException; + +/** + * Resolver for method arguments annotated with @{@link RequestParam}. + * + *

    This resolver can also be created in default resolution mode in which + * simple types (int, long, etc.) not annotated with @{@link RequestParam} are + * also treated as request parameters with the parameter name derived from the + * argument name. + * + *

    If the method parameter type is {@link Map}, the name specified in the + * annotation is used to resolve the request parameter String value. The value is + * then converted to a {@link Map} via type conversion assuming a suitable + * {@link Converter} has been registered. Or if a request parameter name is not + * specified the {@link RequestParamMapMethodArgumentResolver} is used instead + * to provide access to all request parameters in the form of a map. + * + * @author Rossen Stoyanchev + * @see RequestParamMapMethodArgumentResolver + */ +public class RequestParamMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver { + + private final boolean useDefaultResolution; + + + /** + * @param conversionService for type conversion (to be replaced with WebDataBinder) + * @param beanFactory a bean factory used for resolving ${...} placeholder + * and #{...} SpEL expressions in default values, or {@code null} if default + * values are not expected to contain expressions + * @param useDefaultResolution in default resolution mode a method argument + * that is a simple type, as defined in {@link BeanUtils#isSimpleProperty}, + * is treated as a request parameter even if it isn't annotated, the + * request parameter name is derived from the method parameter name. + */ + public RequestParamMethodArgumentResolver(ConversionService conversionService, + ConfigurableBeanFactory beanFactory, boolean useDefaultResolution) { + + super(conversionService, beanFactory); + this.useDefaultResolution = useDefaultResolution; + } + + + @Override + public boolean supportsParameter(MethodParameter parameter) { + if (parameter.hasParameterAnnotation(RequestParam.class)) { + if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) { + String paramName = parameter.getParameterAnnotation(RequestParam.class).name(); + return StringUtils.hasText(paramName); + } + else { + return true; + } + } + return (this.useDefaultResolution && BeanUtils.isSimpleProperty(parameter.getNestedParameterType())); + } + + @Override + protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) { + RequestParam ann = parameter.getParameterAnnotation(RequestParam.class); + return (ann != null ? new RequestParamNamedValueInfo(ann) : new RequestParamNamedValueInfo()); + } + + @Override + protected Mono resolveName(String name, MethodParameter parameter, ServerWebExchange exchange) { + List paramValues = exchange.getRequest().getQueryParams().get(name); + Object result = null; + if (paramValues != null) { + result = (paramValues.size() == 1 ? paramValues.get(0) : paramValues); + } + return Mono.justOrEmpty(result); + } + + @Override + protected void handleMissingValue(String name, MethodParameter parameter, ServerWebExchange exchange) { + String type = parameter.getNestedParameterType().getSimpleName(); + String reason = "Required " + type + " parameter '" + name + "' is not present"; + throw new ServerWebInputException(reason, parameter); + } + + + private static class RequestParamNamedValueInfo extends NamedValueInfo { + + public RequestParamNamedValueInfo() { + super("", false, ValueConstants.DEFAULT_NONE); + } + + public RequestParamNamedValueInfo(RequestParam annotation) { + super(annotation.name(), annotation.required(), annotation.defaultValue()); + } + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/SessionAttributeMethodArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/SessionAttributeMethodArgumentResolver.java new file mode 100644 index 0000000000..b6eded3b22 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/SessionAttributeMethodArgumentResolver.java @@ -0,0 +1,71 @@ +/* + * 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.web.reactive.result.method.annotation; + +import java.util.Optional; + +import reactor.core.publisher.Mono; + +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.ConversionService; +import org.springframework.web.bind.annotation.SessionAttribute; +import org.springframework.web.bind.annotation.ValueConstants; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.ServerWebInputException; + +/** + * Resolves method arguments annotated with an @{@link SessionAttribute}. + * + * @author Rossen Stoyanchev + * @see RequestAttributeMethodArgumentResolver + */ +public class SessionAttributeMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver { + + + public SessionAttributeMethodArgumentResolver(ConversionService conversionService, + ConfigurableBeanFactory beanFactory) { + + super(conversionService, beanFactory); + } + + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(SessionAttribute.class); + } + + + @Override + protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) { + SessionAttribute annot = parameter.getParameterAnnotation(SessionAttribute.class); + return new NamedValueInfo(annot.name(), annot.required(), ValueConstants.DEFAULT_NONE); + } + + @Override + protected Mono resolveName(String name, MethodParameter parameter, ServerWebExchange exchange){ + return exchange.getSession().map(session -> session.getAttribute(name)) + .where(Optional::isPresent).map(Optional::get); + } + + @Override + protected void handleMissingValue(String name, MethodParameter parameter) { + String type = parameter.getNestedParameterType().getSimpleName(); + String reason = "Missing session attribute '" + name + "' of type " + type; + throw new ServerWebInputException(reason, parameter); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/ServerErrorException.java b/spring-web-reactive/src/main/java/org/springframework/web/server/ServerErrorException.java new file mode 100644 index 0000000000..9289c3c459 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/ServerErrorException.java @@ -0,0 +1,65 @@ +/* + * 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.web.server; + +import java.util.Optional; + +import org.springframework.core.MethodParameter; +import org.springframework.http.HttpStatus; + +/** + * Exception for errors that fit response status 500 (bad request) for use in + * Spring Web applications. The exception provides additional fields (e.g. + * an optional {@link MethodParameter} if related to the error). + * + * @author Rossen Stoyanchev + */ +public class ServerErrorException extends ResponseStatusException { + + private final MethodParameter parameter; + + + /** + * Constructor with an explanation only. + */ + public ServerErrorException(String reason) { + this(reason, null); + } + + /** + * Constructor for a 500 error linked to a specific {@code MethodParameter}. + */ + public ServerErrorException(String reason, MethodParameter parameter) { + this(reason, parameter, null); + } + + /** + * Constructor for a 500 error with a root cause. + */ + public ServerErrorException(String reason, MethodParameter parameter, Throwable cause) { + super(HttpStatus.INTERNAL_SERVER_ERROR, reason, cause); + this.parameter = parameter; + } + + + /** + * Return the {@code MethodParameter} associated with this error, if any. + */ + public Optional getMethodParameter() { + return Optional.ofNullable(this.parameter); + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/InvocableHandlerMethodTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/InvocableHandlerMethodTests.java index 3d9ae0bbef..939c67e217 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/InvocableHandlerMethodTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/InvocableHandlerMethodTests.java @@ -28,14 +28,16 @@ import reactor.core.publisher.Mono; import reactor.core.publisher.Signal; import reactor.core.util.SignalKind; +import org.springframework.core.convert.support.GenericConversionService; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.ui.ExtendedModelMap; import org.springframework.ui.ModelMap; +import org.springframework.util.LinkedMultiValueMap; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerResult; -import org.springframework.web.reactive.result.method.annotation.RequestParamArgumentResolver; +import org.springframework.web.reactive.result.method.annotation.RequestParamMethodArgumentResolver; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange; import org.springframework.web.server.session.WebSessionManager; @@ -82,8 +84,10 @@ public class InvocableHandlerMethodTests { @Test public void resolveArgToZeroValues() throws Exception { when(this.request.getURI()).thenReturn(new URI("http://localhost:8080/path")); + when(this.request.getQueryParams()).thenReturn(new LinkedMultiValueMap<>()); InvocableHandlerMethod hm = createHandlerMethod("singleArg", String.class); - hm.setHandlerMethodArgumentResolvers(Collections.singletonList(new RequestParamArgumentResolver())); + hm.setHandlerMethodArgumentResolvers(Collections.singletonList( + new RequestParamMethodArgumentResolver(new GenericConversionService(), null, false))); Mono mono = hm.invokeForRequest(this.exchange, this.model); HandlerResult value = mono.get(); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/AbstractRequestAttributesArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/AbstractRequestAttributesArgumentResolverTests.java new file mode 100644 index 0000000000..ae1b4b9f25 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/AbstractRequestAttributesArgumentResolverTests.java @@ -0,0 +1,177 @@ +/* + * 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.web.reactive.result.method.annotation; + +import java.lang.reflect.Method; +import java.net.URI; +import java.util.Optional; + +import org.junit.Before; +import org.junit.Test; +import reactor.core.publisher.Mono; +import reactor.core.test.TestSubscriber; + +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.GenericTypeResolver; +import org.springframework.core.MethodParameter; +import org.springframework.core.annotation.SynthesizingMethodParameter; +import org.springframework.http.HttpMethod; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.util.ReflectionUtils; +import org.springframework.web.bind.annotation.RequestAttribute; +import org.springframework.web.bind.annotation.SessionAttribute; +import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.ServerWebInputException; +import org.springframework.web.server.WebSession; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.WebSessionManager; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Base class for {@code @RequestAttribute} and {@code @SessionAttribute} method + * argument resolution tests. + * + * @author Rossen Stoyanchev + */ +public abstract class AbstractRequestAttributesArgumentResolverTests { + + private HandlerMethodArgumentResolver resolver; + + private ServerWebExchange exchange; + + private Method handleMethod; + + + @Before @SuppressWarnings("ConfusingArgumentToVarargsMethod") + public void setUp() throws Exception { + this.resolver = createResolver(); + + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); + WebSessionManager sessionManager = mock(WebSessionManager.class); + this.exchange = new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); + + WebSession webSession = mock(WebSession.class); + when(sessionManager.getSession(this.exchange)).thenReturn(Mono.just(webSession)); + + this.handleMethod = ReflectionUtils.findMethod(getClass(), getHandleMethodName(), (Class[]) null); + } + + + protected abstract HandlerMethodArgumentResolver createResolver(); + + protected abstract String getHandleMethodName(); + + + @Test + public void supportsParameter() throws Exception { + assertTrue(this.resolver.supportsParameter(new MethodParameter(this.handleMethod, 0))); + assertFalse(this.resolver.supportsParameter(new MethodParameter(this.handleMethod, 4))); + } + + @Test + public void resolve() throws Exception { + MethodParameter param = initMethodParameter(0); + Mono mono = this.resolver.resolveArgument(param, null, this.exchange); + TestSubscriber subscriber = new TestSubscriber<>(); + mono.subscribeWith(subscriber); + subscriber.assertError(ServerWebInputException.class); + + Foo foo = new Foo(); + this.exchange.getAttributes().put("foo", foo); + mono = this.resolver.resolveArgument(param, null, this.exchange); + assertSame(foo, mono.get()); + } + + @Test + public void resolveWithName() throws Exception { + MethodParameter param = initMethodParameter(1); + Foo foo = new Foo(); + this.exchange.getAttributes().put("specialFoo", foo); + Mono mono = this.resolver.resolveArgument(param, null, this.exchange); + assertSame(foo, mono.get()); + } + + @Test + public void resolveNotRequired() throws Exception { + MethodParameter param = initMethodParameter(2); + Mono mono = this.resolver.resolveArgument(param, null, this.exchange); + assertNull(mono.get()); + + Foo foo = new Foo(); + this.exchange.getAttributes().put("foo", foo); + mono = this.resolver.resolveArgument(param, null, this.exchange); + assertSame(foo, mono.get()); + } + + @Test + public void resolveOptional() throws Exception { + MethodParameter param = initMethodParameter(3); + Mono mono = this.resolver.resolveArgument(param, null, this.exchange); + assertNotNull(mono.get()); + assertEquals(Optional.class, mono.get().getClass()); + assertFalse(((Optional) mono.get()).isPresent()); + + Foo foo = new Foo(); + this.exchange.getAttributes().put("foo", foo); + mono = this.resolver.resolveArgument(param, null, this.exchange); + + assertNotNull(mono.get()); + assertEquals(Optional.class, mono.get().getClass()); + Optional optional = (Optional) mono.get(); + assertTrue(optional.isPresent()); + assertSame(foo, optional.get()); + } + + + private MethodParameter initMethodParameter(int parameterIndex) { + MethodParameter param = new SynthesizingMethodParameter(this.handleMethod, parameterIndex); + param.initParameterNameDiscovery(new DefaultParameterNameDiscoverer()); + GenericTypeResolver.resolveParameterType(param, this.resolver.getClass()); + return param; + } + + + @SuppressWarnings({"unused", "OptionalUsedAsFieldOrParameterType"}) + private void handleWithRequestAttribute( + @RequestAttribute Foo foo, + @RequestAttribute("specialFoo") Foo namedFoo, + @RequestAttribute(name="foo", required = false) Foo notRequiredFoo, + @RequestAttribute(name="foo") Optional optionalFoo) { + } + + @SuppressWarnings({"unused", "OptionalUsedAsFieldOrParameterType"}) + private void handleWithSessionAttribute( + @SessionAttribute Foo foo, + @SessionAttribute("specialFoo") Foo namedFoo, + @SessionAttribute(name="foo", required = false) Foo notRequiredFoo, + @SessionAttribute(name="foo") Optional optionalFoo) { + } + + private static class Foo { + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/CookieValueMethodArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/CookieValueMethodArgumentResolverTests.java new file mode 100644 index 0000000000..e31b056e62 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/CookieValueMethodArgumentResolverTests.java @@ -0,0 +1,132 @@ +/* + * 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.web.reactive.result.method.annotation; + +import java.lang.reflect.Method; +import java.net.URI; + +import org.junit.Before; +import org.junit.Test; +import reactor.core.publisher.Mono; +import reactor.core.test.TestSubscriber; + +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.core.MethodParameter; +import org.springframework.core.annotation.SynthesizingMethodParameter; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.http.HttpCookie; +import org.springframework.http.HttpMethod; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.web.bind.annotation.CookieValue; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.ServerWebInputException; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.WebSessionManager; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +/** + * Test fixture with {@link CookieValueMethodArgumentResolver}. + * + * @author Rossen Stoyanchev + */ +public class CookieValueMethodArgumentResolverTests { + + private CookieValueMethodArgumentResolver resolver; + + private ServerWebExchange exchange; + + private MethodParameter cookieParameter; + private MethodParameter cookieStringParameter; + private MethodParameter stringParameter; + + + @Before + public void setUp() throws Exception { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.refresh(); + ConversionService cs = new DefaultConversionService(); + this.resolver = new CookieValueMethodArgumentResolver(cs, context.getBeanFactory()); + + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); + WebSessionManager sessionManager = mock(WebSessionManager.class); + this.exchange = new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); + + Method method = getClass().getMethod("params", HttpCookie.class, String.class, String.class); + this.cookieParameter = new SynthesizingMethodParameter(method, 0); + this.cookieStringParameter = new SynthesizingMethodParameter(method, 1); + this.stringParameter = new SynthesizingMethodParameter(method, 2); + } + + + @Test + public void supportsParameter() { + assertTrue(this.resolver.supportsParameter(this.cookieParameter)); + assertTrue(this.resolver.supportsParameter(this.cookieStringParameter)); + assertFalse(this.resolver.supportsParameter(this.stringParameter)); + } + + @Test + public void resolveCookieArgument() { + HttpCookie expected = new HttpCookie("name", "foo"); + this.exchange.getRequest().getCookies().add(expected.getName(), expected); + + Mono mono = this.resolver.resolveArgument(this.cookieParameter, null, this.exchange); + assertEquals(expected, mono.get()); + } + + @Test + public void resolveCookieStringArgument() { + HttpCookie cookie = new HttpCookie("name", "foo"); + this.exchange.getRequest().getCookies().add(cookie.getName(), cookie); + + Mono mono = this.resolver.resolveArgument(this.cookieStringParameter, null, this.exchange); + assertEquals("Invalid result", cookie.getValue(), mono.get()); + } + + @Test + public void resolveCookieDefaultValue() { + Mono mono = this.resolver.resolveArgument(this.cookieStringParameter, null, this.exchange); + Object result = mono.get(); + + assertTrue(result instanceof String); + assertEquals("bar", result); + } + + @Test + public void notFound() { + Mono mono = resolver.resolveArgument(this.cookieParameter, null, this.exchange); + TestSubscriber subscriber = new TestSubscriber<>(); + mono.subscribeWith(subscriber); + subscriber.assertError(ServerWebInputException.class); + } + + + @SuppressWarnings("unused") + public void params( + @CookieValue("name") HttpCookie cookie, + @CookieValue(name = "name", defaultValue = "bar") String cookieString, + String stringParam) { + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ExpressionValueMethodArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ExpressionValueMethodArgumentResolverTests.java new file mode 100644 index 0000000000..52ae4a2a11 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ExpressionValueMethodArgumentResolverTests.java @@ -0,0 +1,104 @@ +/* + * 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.web.reactive.result.method.annotation; + +import java.lang.reflect.Method; +import java.net.URI; + +import org.junit.Before; +import org.junit.Test; +import reactor.core.publisher.Mono; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.http.HttpMethod; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.WebSessionManager; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +/** + * Unit tests for {@link ExpressionValueMethodArgumentResolver}. + * + * @author Rossen Stoyanchev + */ +public class ExpressionValueMethodArgumentResolverTests { + + private ExpressionValueMethodArgumentResolver resolver; + + private ServerWebExchange exchange; + + private MethodParameter paramSystemProperty; + private MethodParameter paramNotSupported; + + + @Before + public void setUp() throws Exception { + ConversionService conversionService = new GenericConversionService(); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.refresh(); + this.resolver = new ExpressionValueMethodArgumentResolver(conversionService, context.getBeanFactory()); + + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); + WebSessionManager sessionManager = mock(WebSessionManager.class); + this.exchange = new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); + + Method method = getClass().getMethod("params", int.class, String.class); + this.paramSystemProperty = new MethodParameter(method, 0); + this.paramNotSupported = new MethodParameter(method, 1); + } + + + @Test + public void supportsParameter() throws Exception { + assertTrue(this.resolver.supportsParameter(this.paramSystemProperty)); + assertFalse(this.resolver.supportsParameter(this.paramNotSupported)); + } + + @Test + public void resolveSystemProperty() throws Exception { + System.setProperty("systemProperty", "22"); + try { + Mono mono = this.resolver.resolveArgument(this.paramSystemProperty, null, this.exchange); + Object value = mono.get(); + assertEquals(22, value); + } + finally { + System.clearProperty("systemProperty"); + } + + } + + // TODO: test with expression for ServerWebExchange + + + @SuppressWarnings("unused") + public void params(@Value("#{systemProperties.systemProperty}") int param1, + String notSupported) { + } + +} \ No newline at end of file diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/PathVariableMapMethodArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/PathVariableMapMethodArgumentResolverTests.java new file mode 100644 index 0000000000..851e77e804 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/PathVariableMapMethodArgumentResolverTests.java @@ -0,0 +1,112 @@ +/* + * 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.web.reactive.result.method.annotation; + +import java.lang.reflect.Method; +import java.net.URI; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import reactor.core.publisher.Mono; + +import org.springframework.core.MethodParameter; +import org.springframework.http.HttpMethod; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.ui.ModelMap; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.reactive.HandlerMapping; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.WebSessionManager; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +/** + * Unit tests for {@link PathVariableMapMethodArgumentResolver}. + * + * @author Rossen Stoyanchev + */ +public class PathVariableMapMethodArgumentResolverTests { + + private PathVariableMapMethodArgumentResolver resolver; + + private ServerWebExchange exchange; + + private MethodParameter paramMap; + private MethodParameter paramNamedMap; + private MethodParameter paramMapNoAnnot; + + + @Before + public void setUp() throws Exception { + this.resolver = new PathVariableMapMethodArgumentResolver(); + + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); + WebSessionManager sessionManager = mock(WebSessionManager.class); + this.exchange = new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); + + Method method = getClass().getMethod("handle", Map.class, Map.class, Map.class); + this.paramMap = new MethodParameter(method, 0); + this.paramNamedMap = new MethodParameter(method, 1); + this.paramMapNoAnnot = new MethodParameter(method, 2); + } + + @Test + public void supportsParameter() { + assertTrue(resolver.supportsParameter(paramMap)); + assertFalse(resolver.supportsParameter(paramNamedMap)); + assertFalse(resolver.supportsParameter(paramMapNoAnnot)); + } + + @Test + public void resolveArgument() throws Exception { + Map uriTemplateVars = new HashMap<>(); + uriTemplateVars.put("name1", "value1"); + uriTemplateVars.put("name2", "value2"); + this.exchange.getAttributes().put(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriTemplateVars); + + Mono mono = this.resolver.resolveArgument(this.paramMap, new ModelMap(), this.exchange); + Object result = mono.get(); + + assertEquals(uriTemplateVars, result); + } + + @Test + public void resolveArgumentNoUriVars() throws Exception { + Mono mono = this.resolver.resolveArgument(this.paramMap, new ModelMap(), this.exchange); + Object result = mono.get(); + + assertEquals(Collections.emptyMap(), result); + } + + + @SuppressWarnings("unused") + public void handle( + @PathVariable Map map, + @PathVariable(value = "name") Map namedMap, + Map mapWithoutAnnotat) { + } + +} \ No newline at end of file diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/PathVariableMethodArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/PathVariableMethodArgumentResolverTests.java new file mode 100644 index 0000000000..f79d713013 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/PathVariableMethodArgumentResolverTests.java @@ -0,0 +1,110 @@ +/* + * 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.web.reactive.result.method.annotation; + +import java.lang.reflect.Method; +import java.net.URI; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import reactor.core.publisher.Mono; +import reactor.core.test.TestSubscriber; + +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.http.HttpMethod; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.ui.ModelMap; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.reactive.HandlerMapping; +import org.springframework.web.server.ServerErrorException; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.WebSessionManager; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +/** + * Unit tests for {@link PathVariableMethodArgumentResolver}. + * + * @author Rossen Stoyanchev + */ +public class PathVariableMethodArgumentResolverTests { + + private PathVariableMethodArgumentResolver resolver; + + private ServerWebExchange exchange; + + private MethodParameter paramNamedString; + private MethodParameter paramString; + + + @Before + public void setUp() throws Exception { + ConversionService conversionService = new DefaultConversionService(); + this.resolver = new PathVariableMethodArgumentResolver(conversionService, null); + + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); + WebSessionManager sessionManager = mock(WebSessionManager.class); + this.exchange = new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); + + Method method = getClass().getMethod("handle", String.class, String.class); + this.paramNamedString = new MethodParameter(method, 0); + this.paramString = new MethodParameter(method, 1); + } + + + @Test + public void supportsParameter() { + assertTrue(this.resolver.supportsParameter(this.paramNamedString)); + assertFalse(this.resolver.supportsParameter(this.paramString)); + } + + @Test + public void resolveArgument() throws Exception { + Map uriTemplateVars = new HashMap<>(); + uriTemplateVars.put("name", "value"); + this.exchange.getAttributes().put(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriTemplateVars); + + Mono mono = this.resolver.resolveArgument(this.paramNamedString, new ModelMap(), this.exchange); + Object result = mono.get(); + + assertTrue(result instanceof String); + assertEquals("value", result); + } + + @Test + public void handleMissingValue() throws Exception { + Mono mono = this.resolver.resolveArgument(this.paramNamedString, new ModelMap(), this.exchange); + TestSubscriber subscriber = new TestSubscriber<>(); + mono.subscribeWith(subscriber); + subscriber.assertError(ServerErrorException.class); + } + + @SuppressWarnings("unused") + public void handle(@PathVariable(value = "name") String param1, String param2) { + } + +} \ No newline at end of file diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestAttributeMethodArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestAttributeMethodArgumentResolverTests.java new file mode 100644 index 0000000000..3d82d8e04e --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestAttributeMethodArgumentResolverTests.java @@ -0,0 +1,163 @@ +/* + * 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.web.reactive.result.method.annotation; + +import java.lang.reflect.Method; +import java.net.URI; +import java.util.Optional; + +import org.junit.Before; +import org.junit.Test; +import reactor.core.publisher.Mono; +import reactor.core.test.TestSubscriber; + +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.GenericTypeResolver; +import org.springframework.core.MethodParameter; +import org.springframework.core.annotation.SynthesizingMethodParameter; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.http.HttpMethod; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.util.ReflectionUtils; +import org.springframework.web.bind.annotation.RequestAttribute; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.ServerWebInputException; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.WebSessionManager; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + + +/** + * Unit tests for {@link RequestAttributeMethodArgumentResolver}. + * @author Rossen Stoyanchev + */ +public class RequestAttributeMethodArgumentResolverTests { + + private RequestAttributeMethodArgumentResolver resolver; + + private ServerWebExchange exchange; + + private Method handleMethod; + + + @Before + @SuppressWarnings("ConfusingArgumentToVarargsMethod") + public void setUp() throws Exception { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.refresh(); + ConversionService cs = new DefaultConversionService(); + this.resolver = new RequestAttributeMethodArgumentResolver(cs, context.getBeanFactory()); + + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); + WebSessionManager sessionManager = mock(WebSessionManager.class); + this.exchange = new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); + + this.handleMethod = ReflectionUtils.findMethod(getClass(), "handleWithRequestAttribute", (Class[]) null); + } + + + @Test + public void supportsParameter() throws Exception { + assertTrue(this.resolver.supportsParameter(new MethodParameter(this.handleMethod, 0))); + assertFalse(this.resolver.supportsParameter(new MethodParameter(this.handleMethod, 4))); + } + + @Test + public void resolve() throws Exception { + MethodParameter param = initMethodParameter(0); + Mono mono = this.resolver.resolveArgument(param, null, this.exchange); + TestSubscriber subscriber = new TestSubscriber<>(); + mono.subscribeWith(subscriber); + subscriber.assertError(ServerWebInputException.class); + + Foo foo = new Foo(); + this.exchange.getAttributes().put("foo", foo); + mono = this.resolver.resolveArgument(param, null, this.exchange); + assertSame(foo, mono.get()); + } + + @Test + public void resolveWithName() throws Exception { + MethodParameter param = initMethodParameter(1); + Foo foo = new Foo(); + this.exchange.getAttributes().put("specialFoo", foo); + Mono mono = this.resolver.resolveArgument(param, null, this.exchange); + assertSame(foo, mono.get()); + } + + @Test + public void resolveNotRequired() throws Exception { + MethodParameter param = initMethodParameter(2); + Mono mono = this.resolver.resolveArgument(param, null, this.exchange); + assertNull(mono.get()); + + Foo foo = new Foo(); + this.exchange.getAttributes().put("foo", foo); + mono = this.resolver.resolveArgument(param, null, this.exchange); + assertSame(foo, mono.get()); + } + + @Test + public void resolveOptional() throws Exception { + MethodParameter param = initMethodParameter(3); + Mono mono = this.resolver.resolveArgument(param, null, this.exchange); + assertNotNull(mono.get()); + assertEquals(Optional.class, mono.get().getClass()); + assertFalse(((Optional) mono.get()).isPresent()); + + Foo foo = new Foo(); + this.exchange.getAttributes().put("foo", foo); + mono = this.resolver.resolveArgument(param, null, this.exchange); + + assertNotNull(mono.get()); + assertEquals(Optional.class, mono.get().getClass()); + Optional optional = (Optional) mono.get(); + assertTrue(optional.isPresent()); + assertSame(foo, optional.get()); + } + + + private MethodParameter initMethodParameter(int parameterIndex) { + MethodParameter param = new SynthesizingMethodParameter(this.handleMethod, parameterIndex); + param.initParameterNameDiscovery(new DefaultParameterNameDiscoverer()); + GenericTypeResolver.resolveParameterType(param, this.resolver.getClass()); + return param; + } + + + @SuppressWarnings({"unused", "OptionalUsedAsFieldOrParameterType"}) + private void handleWithRequestAttribute( + @RequestAttribute Foo foo, + @RequestAttribute("specialFoo") Foo namedFoo, + @RequestAttribute(name="foo", required = false) Foo notRequiredFoo, + @RequestAttribute(name="foo") Optional optionalFoo) { + } + + private static class Foo { + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMapMethodArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMapMethodArgumentResolverTests.java new file mode 100644 index 0000000000..a2e48579af --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMapMethodArgumentResolverTests.java @@ -0,0 +1,150 @@ +/* + * 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.web.reactive.result.method.annotation; + +import java.lang.reflect.Method; +import java.net.URI; +import java.util.Collections; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import reactor.core.publisher.Mono; + +import org.springframework.core.MethodParameter; +import org.springframework.core.annotation.SynthesizingMethodParameter; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.WebSessionManager; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +/** + * Unit tests for {@link RequestHeaderMapMethodArgumentResolver}. + * + * @author Rossen Stoyanchev + */ +public class RequestHeaderMapMethodArgumentResolverTests { + + private RequestHeaderMapMethodArgumentResolver resolver; + + private MethodParameter paramMap; + private MethodParameter paramMultiValueMap; + private MethodParameter paramHttpHeaders; + private MethodParameter paramUnsupported; + + private ServerWebExchange exchange; + + + @Before + public void setUp() throws Exception { + resolver = new RequestHeaderMapMethodArgumentResolver(); + + Method method = getClass().getMethod("params", Map.class, MultiValueMap.class, HttpHeaders.class, Map.class); + paramMap = new SynthesizingMethodParameter(method, 0); + paramMultiValueMap = new SynthesizingMethodParameter(method, 1); + paramHttpHeaders = new SynthesizingMethodParameter(method, 2); + paramUnsupported = new SynthesizingMethodParameter(method, 3); + + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); + WebSessionManager sessionManager = mock(WebSessionManager.class); + this.exchange = new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); + } + + + @Test + public void supportsParameter() { + assertTrue("Map parameter not supported", resolver.supportsParameter(paramMap)); + assertTrue("MultiValueMap parameter not supported", resolver.supportsParameter(paramMultiValueMap)); + assertTrue("HttpHeaders parameter not supported", resolver.supportsParameter(paramHttpHeaders)); + assertFalse("non-@RequestParam map supported", resolver.supportsParameter(paramUnsupported)); + } + + @Test + public void resolveMapArgument() throws Exception { + String name = "foo"; + String value = "bar"; + Map expected = Collections.singletonMap(name, value); + this.exchange.getRequest().getHeaders().add(name, value); + + Mono mono = this.resolver.resolveArgument(paramMap, null, this.exchange); + Object result = mono.get(); + + assertTrue(result instanceof Map); + assertEquals("Invalid result", expected, result); + } + + @Test + public void resolveMultiValueMapArgument() throws Exception { + String name = "foo"; + String value1 = "bar"; + String value2 = "baz"; + + this.exchange.getRequest().getHeaders().add(name, value1); + this.exchange.getRequest().getHeaders().add(name, value2); + + MultiValueMap expected = new LinkedMultiValueMap<>(1); + expected.add(name, value1); + expected.add(name, value2); + + Mono mono = this.resolver.resolveArgument(paramMultiValueMap, null, this.exchange); + Object result = mono.get(); + + assertTrue(result instanceof MultiValueMap); + assertEquals("Invalid result", expected, result); + } + + @Test + public void resolveHttpHeadersArgument() throws Exception { + String name = "foo"; + String value1 = "bar"; + String value2 = "baz"; + + this.exchange.getRequest().getHeaders().add(name, value1); + this.exchange.getRequest().getHeaders().add(name, value2); + + HttpHeaders expected = new HttpHeaders(); + expected.add(name, value1); + expected.add(name, value2); + + Mono mono = this.resolver.resolveArgument(paramHttpHeaders, null, this.exchange); + Object result = mono.get(); + + assertTrue(result instanceof HttpHeaders); + assertEquals("Invalid result", expected, result); + } + + + @SuppressWarnings("unused") + public void params(@RequestHeader Map param1, + @RequestHeader MultiValueMap param2, + @RequestHeader HttpHeaders param3, + Map unsupported) { + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolverTests.java new file mode 100644 index 0000000000..d8dcfbcece --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolverTests.java @@ -0,0 +1,229 @@ +/* + * 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.web.reactive.result.method.annotation; + +import java.lang.reflect.Method; +import java.net.URI; +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.Date; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import reactor.core.publisher.Mono; +import reactor.core.test.TestSubscriber; + +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.core.MethodParameter; +import org.springframework.core.annotation.SynthesizingMethodParameter; +import org.springframework.core.convert.ConversionService; +import org.springframework.format.support.DefaultFormattingConversionService; +import org.springframework.http.HttpMethod; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.util.ReflectionUtils; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.ServerWebInputException; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.WebSessionManager; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +/** + * Unit tests for {@link RequestHeaderMethodArgumentResolver}. + * + * @author Rossen Stoyanchev + */ +public class RequestHeaderMethodArgumentResolverTests { + + private RequestHeaderMethodArgumentResolver resolver; + + private MethodParameter paramNamedDefaultValueStringHeader; + private MethodParameter paramNamedValueStringArray; + private MethodParameter paramSystemProperty; + private MethodParameter paramResolvedNameWithExpression; + private MethodParameter paramResolvedNameWithPlaceholder; + private MethodParameter paramNamedValueMap; + private MethodParameter paramDate; + private MethodParameter paramInstant; + + private ServerWebExchange exchange; + + + @Before + public void setUp() throws Exception { + ConversionService conversionService = new DefaultFormattingConversionService(); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.refresh(); + this.resolver = new RequestHeaderMethodArgumentResolver(conversionService, context.getBeanFactory()); + + @SuppressWarnings("ConfusingArgumentToVarargsMethod") + Method method = ReflectionUtils.findMethod(getClass(), "params", null); + this.paramNamedDefaultValueStringHeader = new SynthesizingMethodParameter(method, 0); + this.paramNamedValueStringArray = new SynthesizingMethodParameter(method, 1); + this.paramSystemProperty = new SynthesizingMethodParameter(method, 2); + this.paramResolvedNameWithExpression = new SynthesizingMethodParameter(method, 3); + this.paramResolvedNameWithPlaceholder = new SynthesizingMethodParameter(method, 4); + this.paramNamedValueMap = new SynthesizingMethodParameter(method, 5); + this.paramDate = new SynthesizingMethodParameter(method, 6); + this.paramInstant = new SynthesizingMethodParameter(method, 7); + + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); + WebSessionManager sessionManager = mock(WebSessionManager.class); + this.exchange = new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); + } + + + @Test + public void supportsParameter() { + assertTrue("String parameter not supported", resolver.supportsParameter(paramNamedDefaultValueStringHeader)); + assertTrue("String array parameter not supported", resolver.supportsParameter(paramNamedValueStringArray)); + assertFalse("non-@RequestParam parameter supported", resolver.supportsParameter(paramNamedValueMap)); + } + + @Test + public void resolveStringArgument() throws Exception { + String expected = "foo"; + this.exchange.getRequest().getHeaders().add("name", expected); + + Mono mono = this.resolver.resolveArgument(paramNamedDefaultValueStringHeader, null, this.exchange); + Object result = mono.get(); + assertTrue(result instanceof String); + assertEquals(expected, result); + } + + @Test + public void resolveStringArrayArgument() throws Exception { + String[] expected = new String[] {"foo", "bar"}; + this.exchange.getRequest().getHeaders().put("name", Arrays.asList(expected)); + + Mono mono = this.resolver.resolveArgument(paramNamedValueStringArray, null, this.exchange); + Object result = mono.get(); + assertTrue(result instanceof String[]); + assertArrayEquals(expected, (String[]) result); + } + + @Test + public void resolveDefaultValue() throws Exception { + Mono mono = this.resolver.resolveArgument(paramNamedDefaultValueStringHeader, null, this.exchange); + Object result = mono.get(); + assertTrue(result instanceof String); + assertEquals("bar", result); + } + + @Test + public void resolveDefaultValueFromSystemProperty() throws Exception { + System.setProperty("systemProperty", "bar"); + try { + Mono mono = this.resolver.resolveArgument(paramSystemProperty, null, this.exchange); + Object result = mono.get(); + assertTrue(result instanceof String); + assertEquals("bar", result); + } + finally { + System.clearProperty("systemProperty"); + } + } + + @Test + public void resolveNameFromSystemPropertyThroughExpression() throws Exception { + String expected = "foo"; + this.exchange.getRequest().getHeaders().add("bar", expected); + + System.setProperty("systemProperty", "bar"); + try { + Mono mono = this.resolver.resolveArgument(paramResolvedNameWithExpression, null, this.exchange); + Object result = mono.get(); + assertTrue(result instanceof String); + assertEquals(expected, result); + } + finally { + System.clearProperty("systemProperty"); + } + } + + @Test + public void resolveNameFromSystemPropertyThroughPlaceholder() throws Exception { + String expected = "foo"; + this.exchange.getRequest().getHeaders().add("bar", expected); + + System.setProperty("systemProperty", "bar"); + try { + Mono mono = this.resolver.resolveArgument(paramResolvedNameWithPlaceholder, null, this.exchange); + Object result = mono.get(); + assertTrue(result instanceof String); + assertEquals(expected, result); + } + finally { + System.clearProperty("systemProperty"); + } + } + + @Test + public void notFound() throws Exception { + Mono mono = resolver.resolveArgument(paramNamedValueStringArray, null, this.exchange); + TestSubscriber subscriber = new TestSubscriber<>(); + mono.subscribeWith(subscriber); + subscriber.assertError(ServerWebInputException.class); + } + + @Test + public void dateConversion() throws Exception { + String rfc1123val = "Thu, 21 Apr 2016 17:11:08 +0100"; + this.exchange.getRequest().getHeaders().add("name", rfc1123val); + + Mono mono = this.resolver.resolveArgument(paramDate, null, this.exchange); + Object result = mono.get(); + + assertTrue(result instanceof Date); + assertEquals(new Date(rfc1123val), result); + } + + @Test + public void instantConversion() throws Exception { + String rfc1123val = "Thu, 21 Apr 2016 17:11:08 +0100"; + this.exchange.getRequest().getHeaders().add("name", rfc1123val); + + Mono mono = this.resolver.resolveArgument(paramInstant, null, this.exchange); + Object result = mono.get(); + + assertTrue(result instanceof Instant); + assertEquals(Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(rfc1123val)), result); + } + + + @SuppressWarnings("unused") + public void params( + @RequestHeader(name = "name", defaultValue = "bar") String param1, + @RequestHeader("name") String[] param2, + @RequestHeader(name = "name", defaultValue="#{systemProperties.systemProperty}") String param3, + @RequestHeader("#{systemProperties.systemProperty}") String param4, + @RequestHeader("${systemProperty}") String param5, + @RequestHeader("name") Map unsupported, + @RequestHeader("name") Date dateParam, + @RequestHeader("name") Instant instantParam) { + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestParamMapMethodArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestParamMapMethodArgumentResolverTests.java new file mode 100644 index 0000000000..c33daa06ef --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestParamMapMethodArgumentResolverTests.java @@ -0,0 +1,129 @@ +/* + * 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.web.reactive.result.method.annotation; + +import java.lang.reflect.Method; +import java.net.URI; +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import reactor.core.publisher.Mono; + +import org.springframework.core.MethodParameter; +import org.springframework.core.annotation.SynthesizingMethodParameter; +import org.springframework.http.HttpMethod; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.WebSessionManager; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +/** + * Unit tests for {@link RequestParamMapMethodArgumentResolver}. + * + * @author Rossen Stoyanchev + */ +public class RequestParamMapMethodArgumentResolverTests { + + private RequestParamMapMethodArgumentResolver resolver; + + private ServerWebExchange exchange; + + private MethodParameter paramMap; + private MethodParameter paramMultiValueMap; + private MethodParameter paramNamedMap; + private MethodParameter paramMapWithoutAnnot; + + + + @Before + public void setUp() throws Exception { + this.resolver = new RequestParamMapMethodArgumentResolver(); + + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); + WebSessionManager sessionManager = mock(WebSessionManager.class); + this.exchange = new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); + + Method method = getClass().getMethod("params", Map.class, MultiValueMap.class, Map.class, Map.class); + this.paramMap = new SynthesizingMethodParameter(method, 0); + this.paramMultiValueMap = new SynthesizingMethodParameter(method, 1); + this.paramNamedMap = new SynthesizingMethodParameter(method, 2); + this.paramMapWithoutAnnot = new SynthesizingMethodParameter(method, 3); + } + + + @Test + public void supportsParameter() { + assertTrue(this.resolver.supportsParameter(this.paramMap)); + assertTrue(this.resolver.supportsParameter(this.paramMultiValueMap)); + assertFalse(this.resolver.supportsParameter(this.paramNamedMap)); + assertFalse(this.resolver.supportsParameter(this.paramMapWithoutAnnot)); + } + + @Test + public void resolveMapArgument() throws Exception { + String name = "foo"; + String value = "bar"; + this.exchange.getRequest().getQueryParams().set(name, value); + Map expected = Collections.singletonMap(name, value); + + Mono mono = resolver.resolveArgument(paramMap, null, exchange); + Object result = mono.get(); + + assertTrue(result instanceof Map); + assertEquals(expected, result); + } + + @Test + public void resolveMultiValueMapArgument() throws Exception { + String name = "foo"; + String value1 = "bar"; + String value2 = "baz"; + this.exchange.getRequest().getQueryParams().put(name, Arrays.asList(value1, value2)); + + MultiValueMap expected = new LinkedMultiValueMap<>(1); + expected.add(name, value1); + expected.add(name, value2); + + Mono mono = this.resolver.resolveArgument(this.paramMultiValueMap, null, this.exchange); + Object result = mono.get(); + + assertTrue(result instanceof MultiValueMap); + assertEquals(expected, result); + } + + + @SuppressWarnings("unused") + public void params(@RequestParam Map param1, + @RequestParam MultiValueMap param2, + @RequestParam("name") Map param3, + Map param4) { + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestParamMethodArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestParamMethodArgumentResolverTests.java new file mode 100644 index 0000000000..4e6979dfcc --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestParamMethodArgumentResolverTests.java @@ -0,0 +1,232 @@ +/* + * 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.web.reactive.result.method.annotation; + +import java.lang.reflect.Method; +import java.net.URI; +import java.util.Arrays; +import java.util.Map; +import java.util.Optional; + +import org.junit.Before; +import org.junit.Test; +import reactor.core.publisher.Mono; +import reactor.core.test.TestSubscriber; + +import org.springframework.core.LocalVariableTableParameterNameDiscoverer; +import org.springframework.core.MethodParameter; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.core.annotation.SynthesizingMethodParameter; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.http.HttpMethod; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.util.ReflectionUtils; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.ServerWebInputException; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.WebSessionManager; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +/** + * Unit tests for {@link RequestParamMethodArgumentResolver}. + * + * @author Rossen Stoyanchev + */ +public class RequestParamMethodArgumentResolverTests { + + private RequestParamMethodArgumentResolver resolver; + + private ServerWebExchange exchange; + + private MethodParameter paramNamedDefaultValueString; + private MethodParameter paramNamedStringArray; + private MethodParameter paramNamedMap; + private MethodParameter paramMap; + private MethodParameter paramStringNotAnnot; + private MethodParameter paramRequired; + private MethodParameter paramNotRequired; + private MethodParameter paramOptional; + + + @Before @SuppressWarnings("ConfusingArgumentToVarargsMethod") + public void setUp() throws Exception { + ConversionService conversionService = new DefaultConversionService(); + this.resolver = new RequestParamMethodArgumentResolver(conversionService, null, true); + + ParameterNameDiscoverer paramNameDiscoverer = new LocalVariableTableParameterNameDiscoverer(); + Method method = ReflectionUtils.findMethod(getClass(), "handle", (Class[]) null); + + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); + WebSessionManager sessionManager = mock(WebSessionManager.class); + this.exchange = new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); + + this.paramNamedDefaultValueString = new SynthesizingMethodParameter(method, 0); + this.paramNamedStringArray = new SynthesizingMethodParameter(method, 1); + this.paramNamedMap = new SynthesizingMethodParameter(method, 2); + this.paramMap = new SynthesizingMethodParameter(method, 3); + this.paramStringNotAnnot = new SynthesizingMethodParameter(method, 4); + this.paramStringNotAnnot.initParameterNameDiscovery(paramNameDiscoverer); + this.paramRequired = new SynthesizingMethodParameter(method, 5); + this.paramNotRequired = new SynthesizingMethodParameter(method, 6); + this.paramOptional = new SynthesizingMethodParameter(method, 7); + } + + + @Test + public void supportsParameter() { + this.resolver = new RequestParamMethodArgumentResolver(new GenericConversionService(), null, true); + assertTrue(this.resolver.supportsParameter(this.paramNamedDefaultValueString)); + assertTrue(this.resolver.supportsParameter(this.paramNamedStringArray)); + assertTrue(this.resolver.supportsParameter(this.paramNamedMap)); + assertFalse(this.resolver.supportsParameter(this.paramMap)); + assertTrue(this.resolver.supportsParameter(this.paramStringNotAnnot)); + assertTrue(this.resolver.supportsParameter(this.paramRequired)); + assertTrue(this.resolver.supportsParameter(this.paramNotRequired)); + assertTrue(this.resolver.supportsParameter(this.paramOptional)); + + this.resolver = new RequestParamMethodArgumentResolver(new GenericConversionService(), null, false); + assertFalse(this.resolver.supportsParameter(this.paramStringNotAnnot)); + } + + @Test + public void resolveString() throws Exception { + String expected = "foo"; + this.exchange.getRequest().getQueryParams().set("name", expected); + + Mono mono = this.resolver.resolveArgument(this.paramNamedDefaultValueString, null, this.exchange); + Object result = mono.get(); + + assertTrue(result instanceof String); + assertEquals("Invalid result", expected, result); + } + + @Test + public void resolveStringArray() throws Exception { + String[] expected = {"foo", "bar"}; + this.exchange.getRequest().getQueryParams().put("name", Arrays.asList(expected)); + + Mono mono = this.resolver.resolveArgument(this.paramNamedStringArray, null, this.exchange); + Object result = mono.get(); + + assertTrue(result instanceof String[]); + assertArrayEquals(expected, (String[]) result); + } + + @Test + public void resolveDefaultValue() throws Exception { + Mono mono = this.resolver.resolveArgument(paramNamedDefaultValueString, null, this.exchange); + Object result = mono.get(); + + assertTrue(result instanceof String); + assertEquals("Invalid result", "bar", result); + } + + @Test + public void missingRequestParam() throws Exception { + Mono mono = this.resolver.resolveArgument(paramNamedStringArray, null, this.exchange); + TestSubscriber subscriber = new TestSubscriber<>(); + mono.subscribeWith(subscriber); + subscriber.assertError(ServerWebInputException.class); + } + + @Test + public void resolveSimpleTypeParam() throws Exception { + this.exchange.getRequest().getQueryParams().set("stringNotAnnot", "plainValue"); + Mono mono = this.resolver.resolveArgument(paramStringNotAnnot, null, this.exchange); + Object result = mono.get(); + + assertTrue(result instanceof String); + assertEquals("plainValue", result); + } + + @Test // SPR-8561 + public void resolveSimpleTypeParamToNull() throws Exception { + Mono mono = this.resolver.resolveArgument(paramStringNotAnnot, null, this.exchange); + Object result = mono.get(); + + assertNull(result); + } + + @Test // SPR-10180 + public void resolveEmptyValueToDefault() throws Exception { + this.exchange.getRequest().getQueryParams().set("name", ""); + Mono mono = this.resolver.resolveArgument(paramNamedDefaultValueString, null, this.exchange); + Object result = mono.get(); + + assertEquals("bar", result); + } + + @Test + public void resolveEmptyValueWithoutDefault() throws Exception { + this.exchange.getRequest().getQueryParams().set("stringNotAnnot", ""); + Mono mono = this.resolver.resolveArgument(paramStringNotAnnot, null, this.exchange); + Object result = mono.get(); + + assertEquals("", result); + } + + @Test + public void resolveEmptyValueRequiredWithoutDefault() throws Exception { + this.exchange.getRequest().getQueryParams().set("name", ""); + Mono mono = this.resolver.resolveArgument(paramRequired, null, this.exchange); + Object result = mono.get(); + + assertEquals("", result); + } + + @Test + public void resolveOptionalParamValue() throws Exception { + Mono mono = this.resolver.resolveArgument(paramOptional, null, this.exchange); + Object result = mono.get(); + + assertEquals(Optional.empty(), result); + + this.exchange.getRequest().getQueryParams().set("name", "123"); + mono = resolver.resolveArgument(paramOptional, null, this.exchange); + result = mono.get(); + + assertEquals(Optional.class, result.getClass()); + Optional value = (Optional) result; + assertTrue(value.isPresent()); + assertEquals(123, value.get()); + } + + + @SuppressWarnings({"unused", "OptionalUsedAsFieldOrParameterType"}) + public void handle( + @RequestParam(name = "name", defaultValue = "bar") String param1, + @RequestParam("name") String[] param2, + @RequestParam("name") Map param3, + @RequestParam Map param4, + String stringNotAnnot, + @RequestParam("name") String paramRequired, + @RequestParam(name = "name", required = false) String paramNotRequired, + @RequestParam("name") Optional paramOptional) { + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SessionAttributeMethodArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SessionAttributeMethodArgumentResolverTests.java new file mode 100644 index 0000000000..1bdbc9ce4a --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SessionAttributeMethodArgumentResolverTests.java @@ -0,0 +1,170 @@ +/* + * 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.web.reactive.result.method.annotation; + +import java.lang.reflect.Method; +import java.net.URI; +import java.util.Optional; + +import org.junit.Before; +import org.junit.Test; +import reactor.core.publisher.Mono; +import reactor.core.test.TestSubscriber; + +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.GenericTypeResolver; +import org.springframework.core.MethodParameter; +import org.springframework.core.annotation.SynthesizingMethodParameter; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.http.HttpMethod; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.util.ReflectionUtils; +import org.springframework.web.bind.annotation.SessionAttribute; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.ServerWebInputException; +import org.springframework.web.server.WebSession; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.WebSessionManager; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Unit tests for {@link SessionAttributeMethodArgumentResolver}. + * @author Rossen Stoyanchev + */ +public class SessionAttributeMethodArgumentResolverTests { + + private SessionAttributeMethodArgumentResolver resolver; + + private ServerWebExchange exchange; + + private WebSession session; + + private Method handleMethod; + + + @Before + @SuppressWarnings("ConfusingArgumentToVarargsMethod") + public void setUp() throws Exception { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.refresh(); + ConversionService cs = new DefaultConversionService(); + this.resolver = new SessionAttributeMethodArgumentResolver(cs, context.getBeanFactory()); + + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); + WebSessionManager sessionManager = mock(WebSessionManager.class); + this.exchange = new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); + + this.session = mock(WebSession.class); + when(sessionManager.getSession(this.exchange)).thenReturn(Mono.just(this.session)); + when(this.session.getAttribute(any())).thenReturn(Optional.empty()); + + this.handleMethod = ReflectionUtils.findMethod(getClass(), "handleWithSessionAttribute", (Class[]) null); + } + + @Test + public void supportsParameter() throws Exception { + assertTrue(this.resolver.supportsParameter(new MethodParameter(this.handleMethod, 0))); + assertFalse(this.resolver.supportsParameter(new MethodParameter(this.handleMethod, 4))); + } + + @Test + public void resolve() throws Exception { + MethodParameter param = initMethodParameter(0); + Mono mono = this.resolver.resolveArgument(param, null, this.exchange); + TestSubscriber subscriber = new TestSubscriber<>(); + mono.subscribeWith(subscriber); + subscriber.assertError(ServerWebInputException.class); + + Foo foo = new Foo(); + when(this.session.getAttribute("foo")).thenReturn(Optional.of(foo)); + mono = this.resolver.resolveArgument(param, null, this.exchange); + assertSame(foo, mono.get()); + } + + @Test + public void resolveWithName() throws Exception { + MethodParameter param = initMethodParameter(1); + Foo foo = new Foo(); + when(this.session.getAttribute("specialFoo")).thenReturn(Optional.of(foo)); + Mono mono = this.resolver.resolveArgument(param, null, this.exchange); + assertSame(foo, mono.get()); + } + + @Test + public void resolveNotRequired() throws Exception { + MethodParameter param = initMethodParameter(2); + Mono mono = this.resolver.resolveArgument(param, null, this.exchange); + assertNull(mono.get()); + + Foo foo = new Foo(); + when(this.session.getAttribute("foo")).thenReturn(Optional.of(foo)); + mono = this.resolver.resolveArgument(param, null, this.exchange); + assertSame(foo, mono.get()); + } + + @Test + public void resolveOptional() throws Exception { + MethodParameter param = initMethodParameter(3); + Mono mono = this.resolver.resolveArgument(param, null, this.exchange); + assertNotNull(mono.get()); + assertEquals(Optional.class, mono.get().getClass()); + assertFalse(((Optional) mono.get()).isPresent()); + + Foo foo = new Foo(); + when(this.session.getAttribute("foo")).thenReturn(Optional.of(foo)); + mono = this.resolver.resolveArgument(param, null, this.exchange); + + assertNotNull(mono.get()); + assertEquals(Optional.class, mono.get().getClass()); + Optional optional = (Optional) mono.get(); + assertTrue(optional.isPresent()); + assertSame(foo, optional.get()); + } + + + private MethodParameter initMethodParameter(int parameterIndex) { + MethodParameter param = new SynthesizingMethodParameter(this.handleMethod, parameterIndex); + param.initParameterNameDiscovery(new DefaultParameterNameDiscoverer()); + GenericTypeResolver.resolveParameterType(param, this.resolver.getClass()); + return param; + } + + + @SuppressWarnings({"unused", "OptionalUsedAsFieldOrParameterType"}) + private void handleWithSessionAttribute( + @SessionAttribute Foo foo, + @SessionAttribute("specialFoo") Foo namedFoo, + @SessionAttribute(name="foo", required = false) Foo notRequiredFoo, + @SessionAttribute(name="foo") Optional optionalFoo) { + } + + private static class Foo { + } + +} From aadbf1d8a6c0c34112322d2de32be20cda6fd6e7 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 5 May 2016 20:53:12 -0400 Subject: [PATCH 340/498] Order ViewResolver's --- .../view/ViewResolverResultHandler.java | 2 ++ .../view/ViewResolverResultHandlerTests.java | 32 ++++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/ViewResolverResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/ViewResolverResultHandler.java index 317e23e2a5..bdf89c54c2 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/ViewResolverResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/ViewResolverResultHandler.java @@ -25,6 +25,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.core.Ordered; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.convert.ConversionService; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.util.Assert; @@ -59,6 +60,7 @@ public class ViewResolverResultHandler implements HandlerResultHandler, Ordered Assert.notEmpty(resolvers, "At least one ViewResolver is required."); Assert.notNull(service, "'conversionService' is required."); this.viewResolvers.addAll(resolvers); + AnnotationAwareOrderComparator.sort(this.viewResolvers); this.conversionService = service; } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/view/ViewResolverResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/view/ViewResolverResultHandlerTests.java index 6dd15aa541..8d6c2f38e3 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/view/ViewResolverResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/view/ViewResolverResultHandlerTests.java @@ -35,6 +35,7 @@ import reactor.core.publisher.Mono; import reactor.core.test.TestSubscriber; import rx.Single; +import org.springframework.core.Ordered; import org.springframework.core.ResolvableType; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter; @@ -190,6 +191,23 @@ public class ViewResolverResultHandlerTests { assertThat(ex.getMessage(), endsWith("neither returned a view name nor a View object"))); } + @Test + public void ordered() throws Exception { + TestViewResolver resolver1 = new TestViewResolver(); + TestViewResolver resolver2 = new TestViewResolver(); + List resolvers = Arrays.asList(resolver1, resolver2); + + resolver1.setOrder(2); + resolver2.setOrder(1); + + ViewResolverResultHandler resultHandler = + new ViewResolverResultHandler(resolvers, this.conversionService); + + assertEquals(Arrays.asList(resolver2, resolver1), resultHandler.getViewResolvers()); + } + + + private TestSubscriber handle(HandlerResultHandler handler, Object value, ResolvableType type) { HandlerResult result = new HandlerResult(new Object(), value, type, this.model); Mono mono = handler.handleResult(this.exchange, result); @@ -210,10 +228,21 @@ public class ViewResolverResultHandlerTests { } - private static class TestViewResolver implements ViewResolver { + private static class TestViewResolver implements ViewResolver, Ordered { private final Map views = new HashMap<>(); + private int order = Ordered.LOWEST_PRECEDENCE; + + + public void setOrder(int order) { + this.order = order; + } + + @Override + public int getOrder() { + return this.order; + } public TestViewResolver addView(TestView view) { this.views.put(view.getName(), view); @@ -225,6 +254,7 @@ public class ViewResolverResultHandlerTests { View view = this.views.get(viewName); return Mono.justOrEmpty(view); } + } public static final class TestView implements View { From 59217243ee572a38635de9756485750ccd1ffd36 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 5 May 2016 21:16:21 -0400 Subject: [PATCH 341/498] Add viewNames for UrlBasedViewResolver --- .../reactive/view/UrlBasedViewResolver.java | 41 +++++++++++ .../view/UrlBasedViewResolverTests.java | 72 +++++++++++++++++++ 2 files changed, 113 insertions(+) create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/view/UrlBasedViewResolverTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/UrlBasedViewResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/UrlBasedViewResolver.java index 5449493789..e6db4c6f37 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/UrlBasedViewResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/UrlBasedViewResolver.java @@ -21,6 +21,7 @@ import reactor.core.publisher.Mono; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.InitializingBean; +import org.springframework.util.PatternMatchUtils; import org.springframework.web.reactive.View; @@ -61,6 +62,8 @@ public class UrlBasedViewResolver extends ViewResolverSupport implements Initial private String suffix = ""; + private String[] viewNames; + /** * Set the view class to instantiate through {@link #createUrlBasedView(String)}. @@ -120,6 +123,25 @@ public class UrlBasedViewResolver extends ViewResolverSupport implements Initial return this.suffix; } + /** + * Set the view names (or name patterns) that can be handled by this + * {@link org.springframework.web.reactive.ViewResolver}. View names can + * contain simple wildcards such that 'my*', '*Report' and '*Repo*' will + * all match the view name 'myReport'. + * @see #canHandle + */ + public void setViewNames(String... viewNames) { + this.viewNames = viewNames; + } + + /** + * Return the view names (or name patterns) that can be handled by this + * {@link org.springframework.web.reactive.ViewResolver}. + */ + protected String[] getViewNames() { + return this.viewNames; + } + @Override public void afterPropertiesSet() throws Exception { @@ -131,6 +153,9 @@ public class UrlBasedViewResolver extends ViewResolverSupport implements Initial @Override public Mono resolveViewName(String viewName, Locale locale) { + if (!canHandle(viewName, locale)) { + return Mono.empty(); + } AbstractUrlBasedView urlBasedView = createUrlBasedView(viewName); View view = applyLifecycleMethods(viewName, urlBasedView); try { @@ -141,6 +166,22 @@ public class UrlBasedViewResolver extends ViewResolverSupport implements Initial } } + /** + * Indicates whether or not this + * {@link org.springframework.web.reactive.ViewResolver} can handle the + * supplied view name. If not, an empty result is returned. The default + * implementation checks against the configured {@link #setViewNames + * view names}. + * @param viewName the name of the view to retrieve + * @param locale the Locale to retrieve the view for + * @return whether this resolver applies to the specified view + * @see org.springframework.util.PatternMatchUtils#simpleMatch(String, String) + */ + protected boolean canHandle(String viewName, Locale locale) { + String[] viewNames = getViewNames(); + return (viewNames == null || PatternMatchUtils.simpleMatch(viewNames, viewName)); + } + /** * Creates a new View instance of the specified view class and configures it. * Does not perform any lookup for pre-defined View instances. diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/view/UrlBasedViewResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/view/UrlBasedViewResolverTests.java new file mode 100644 index 0000000000..1595dde4c2 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/view/UrlBasedViewResolverTests.java @@ -0,0 +1,72 @@ +/* + * 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.web.reactive.view; + +import java.util.Locale; +import java.util.Map; + +import org.junit.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.context.support.StaticApplicationContext; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.web.reactive.View; +import org.springframework.web.server.ServerWebExchange; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +/** + * Unit tests for {@link UrlBasedViewResolver}. + * + * @author Rossen Stoyanchev + */ +public class UrlBasedViewResolverTests { + + + @Test + public void viewNames() throws Exception { + StaticApplicationContext context = new StaticApplicationContext(); + context.refresh(); + + UrlBasedViewResolver resolver = new UrlBasedViewResolver(); + resolver.setViewClass(TestView.class); + resolver.setViewNames("my*"); + resolver.setApplicationContext(context); + + Mono mono = resolver.resolveViewName("my-view", Locale.US); + assertNotNull(mono.get()); + + mono = resolver.resolveViewName("not-my-view", Locale.US); + assertNull(mono.get()); + } + + + private static class TestView extends AbstractUrlBasedView { + + @Override + public boolean checkResourceExists(Locale locale) throws Exception { + return true; + } + + @Override + protected Flux renderInternal(Map attributes, ServerWebExchange exchange) { + return Flux.empty(); + } + } + +} From f0d28f4b09015f7f61adb4c0f8963d4d41243cdf Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Fri, 6 May 2016 23:51:47 +0100 Subject: [PATCH 342/498] fixme : force retain() on incoming bytebuf content --- .../http/server/reactive/ReactorServerHttpRequest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java index ba52096395..da38767324 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java @@ -17,14 +17,12 @@ package org.springframework.http.server.reactive; import java.net.URI; import java.net.URISyntaxException; -import java.nio.ByteBuffer; import io.netty.handler.codec.http.cookie.Cookie; import reactor.core.publisher.Flux; import reactor.io.netty.http.HttpChannel; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferAllocator; import org.springframework.core.io.buffer.NettyDataBufferAllocator; import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; @@ -90,7 +88,9 @@ public class ReactorServerHttpRequest extends AbstractServerHttpRequest { @Override public Flux getBody() { - return this.channel.receive().map(allocator::wrap); + return this.channel.receive() + .retain() //FIXME Rogue reference holding + .map(allocator::wrap); } } From efa5b876fe0a733d41660fe5cb4f8b37d9123a42 Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Thu, 12 May 2016 09:36:28 +0100 Subject: [PATCH 343/498] fix emit --- .../springframework/core/io/buffer/support/DataBufferUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java index 3c9aedd5d9..85b475e107 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java @@ -200,7 +200,7 @@ public abstract class DataBufferUtils { try { dataBuffer.write(byteBuffer); release = false; - sub.emit(dataBuffer); + sub.next(dataBuffer); } finally { if (release) { From a10826824145df8bd6d0bf4c7ee3ac5eba08bee0 Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Thu, 12 May 2016 09:36:28 +0100 Subject: [PATCH 344/498] fix emit --- .../http/server/reactive/ChannelSendOperatorTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ChannelSendOperatorTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ChannelSendOperatorTests.java index c65fe4facb..7db972ebd2 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ChannelSendOperatorTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ChannelSendOperatorTests.java @@ -110,7 +110,7 @@ public class ChannelSendOperatorTests { IllegalStateException error = new IllegalStateException("boo"); Flux publisher = Flux.generate(() -> 0, (idx , subscriber) -> { int i = ++idx; - subscriber.tryEmit(String.valueOf(i)); + subscriber.next(String.valueOf(i)); if (i == 3) { subscriber.fail(error); } From 5cbc29e61e4137b418cdc2e6353c1b19bd434b5b Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Tue, 17 May 2016 13:24:36 +0200 Subject: [PATCH 345/498] Upgrade to Tomcat 8.5.2 --- spring-web-reactive/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index b0f5771525..1077080587 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -30,7 +30,7 @@ ext { springVersion = '4.3.0.BUILD-SNAPSHOT' reactorVersion = '2.5.0.BUILD-SNAPSHOT' reactorNettyVersion = '2.5.0.BUILD-SNAPSHOT' - tomcatVersion = '8.0.33' + tomcatVersion = '8.5.2' jettyVersion = '9.3.8.v20160314' nettyVersion = '4.1.0.CR6' jacksonVersion = '2.7.3' From 662e093fbb0d60ffa31fef8dcc3f6ac64ab9d10b Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 24 May 2016 15:32:18 -0400 Subject: [PATCH 346/498] Temporarily fix RxJava at 1.1.1 to avoid hanging tests Issue #103 --- spring-web-reactive/build.gradle | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index 1077080587..744ff47e7a 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -30,6 +30,7 @@ ext { springVersion = '4.3.0.BUILD-SNAPSHOT' reactorVersion = '2.5.0.BUILD-SNAPSHOT' reactorNettyVersion = '2.5.0.BUILD-SNAPSHOT' + rxJavaVersion = '1.1.1' // See issue #103 tomcatVersion = '8.5.2' jettyVersion = '9.3.8.v20160314' nettyVersion = '4.1.0.CR6' @@ -96,8 +97,10 @@ dependencies { compile "commons-logging:commons-logging:1.2" optional "org.springframework:spring-context-support:${springVersion}" // for FreeMarker - optional 'io.reactivex:rxjava:1.1.0' - optional "io.reactivex:rxnetty-http:0.5.2-SNAPSHOT" + optional 'io.reactivex:rxjava:${rxJavaVersion}' + optional ("io.reactivex:rxnetty-http:0.5.2-SNAPSHOT") { + exclude group: 'io.reactivex', module: 'rxjava' + } optional "com.fasterxml.jackson.core:jackson-annotations:${jacksonVersion}" optional "com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}" optional "io.projectreactor:reactor-netty:${reactorNettyVersion}" From da3b21b020b4a5a733196b54ca1ae218f27fe741 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 24 May 2016 15:41:51 -0400 Subject: [PATCH 347/498] Fix typo in build script --- spring-web-reactive/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index 744ff47e7a..b6811b7eff 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -97,7 +97,7 @@ dependencies { compile "commons-logging:commons-logging:1.2" optional "org.springframework:spring-context-support:${springVersion}" // for FreeMarker - optional 'io.reactivex:rxjava:${rxJavaVersion}' + optional "io.reactivex:rxjava:${rxJavaVersion}" optional ("io.reactivex:rxnetty-http:0.5.2-SNAPSHOT") { exclude group: 'io.reactivex', module: 'rxjava' } From 91d063899bd816cc57815ac0b2317b3ab674868e Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 25 May 2016 17:34:15 -0400 Subject: [PATCH 348/498] Polish ResponseBody result handling --- .../reactive/result/SimpleResultHandler.java | 4 +- .../annotation/ResponseBodyResultHandler.java | 138 +++++++++--------- .../view/ViewResolverResultHandler.java | 8 +- .../ResponseBodyResultHandlerTests.java | 32 +++- 4 files changed, 98 insertions(+), 84 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/SimpleResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/SimpleResultHandler.java index 447147e7c3..b32a4d0f76 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/SimpleResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/SimpleResultHandler.java @@ -42,10 +42,10 @@ import org.springframework.web.server.ServerWebExchange; */ public class SimpleResultHandler implements Ordered, HandlerResultHandler { - private int order = Ordered.LOWEST_PRECEDENCE; - private ConversionService conversionService; + private int order = Ordered.LOWEST_PRECEDENCE; + public SimpleResultHandler() { } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java index 5e5c61a4e9..cea0cfc029 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java @@ -19,10 +19,8 @@ package org.springframework.web.reactive.result.method.annotation; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; -import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -40,7 +38,6 @@ import org.springframework.http.converter.reactive.HttpMessageConverter; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.util.Assert; -import org.springframework.util.MimeType; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerResult; @@ -50,6 +47,10 @@ import org.springframework.web.server.ServerWebExchange; /** + * {@code HandlerResultHandler} that handles return values from methods annotated + * with {@code @ResponseBody} writing to the body of the request or response with + * an {@link HttpMessageConverter}. + * * @author Rossen Stoyanchev * @author Stephane Maldini * @author Sebastien Deleuze @@ -57,47 +58,48 @@ import org.springframework.web.server.ServerWebExchange; */ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered { - private static final MediaType MEDIA_TYPE_APPLICATION = new MediaType("application"); + private static final MediaType MEDIA_TYPE_APPLICATION_ALL = new MediaType("application"); private final List> messageConverters; private final ConversionService conversionService; - private final List allMediaTypes; + private final List supportedMediaTypes; - private final Map, List> mediaTypesByEncoder; + private int order = 0; - private int order = 0; // TODO: should be MAX_VALUE + /** + * Constructor with message converters and conversion service. + * @param messageConverters converters for writing the response body with + * @param conversionService for converting to Flux and Mono from other reactive types + */ public ResponseBodyResultHandler(List> messageConverters, - ConversionService service) { + ConversionService conversionService) { + Assert.notEmpty(messageConverters, "At least one message converter is required."); - Assert.notNull(service, "'conversionService' is required."); + Assert.notNull(conversionService, "'conversionService' is required."); this.messageConverters = messageConverters; - this.conversionService = service; - this.allMediaTypes = getAllMediaTypes(messageConverters); - this.mediaTypesByEncoder = getMediaTypesByConverter(messageConverters); + this.conversionService = conversionService; + this.supportedMediaTypes = initSupportedMediaTypes(messageConverters); } - private static List getAllMediaTypes( - List> messageConverters) { + private static List initSupportedMediaTypes(List> converters) { Set set = new LinkedHashSet<>(); - messageConverters.forEach( - converter -> set.addAll(converter.getWritableMediaTypes())); + converters.forEach(converter -> set.addAll(converter.getWritableMediaTypes())); List result = new ArrayList<>(set); MediaType.sortBySpecificity(result); return Collections.unmodifiableList(result); } - private static Map, List> getMediaTypesByConverter( - List> converters) { - Map, List> result = - new HashMap<>(converters.size()); - converters.forEach(converter -> result - .put(converter, converter.getWritableMediaTypes())); - return Collections.unmodifiableMap(result); - } + /** + * Set the order for this result handler relative to others. + *

    By default this is set to 0 and is generally save to be ahead of other + * result handlers since it only gets involved if the method (or class) is + * annotated with {@code @ResponseBody}. + * @param order the order + */ public void setOrder(int order) { this.order = order; } @@ -144,27 +146,23 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered elementType = returnType; } - List compatibleMediaTypes = - getCompatibleMediaTypes(exchange.getRequest(), elementType); + ServerHttpRequest request = exchange.getRequest(); + List compatibleMediaTypes = getCompatibleMediaTypes(request, elementType); if (compatibleMediaTypes.isEmpty()) { - return Mono.error(new NotAcceptableStatusException( - getProducibleMediaTypes(elementType))); + List supported = getProducibleMediaTypes(elementType); + return Mono.error(new NotAcceptableStatusException(supported)); } - Optional selectedMediaType = selectBestMediaType(compatibleMediaTypes); - - if (selectedMediaType.isPresent()) { - HttpMessageConverter converter = - resolveEncoder(elementType, selectedMediaType.get()); + MediaType bestMediaType = selectBestMediaType(compatibleMediaTypes); + if (bestMediaType != null) { + HttpMessageConverter converter = resolveEncoder(elementType, bestMediaType); if (converter != null) { ServerHttpResponse response = exchange.getResponse(); - return converter.write((Publisher) publisher, elementType, - selectedMediaType.get(), - response); + return converter.write((Publisher) publisher, elementType, bestMediaType, response); } } - return Mono.error(new NotAcceptableStatusException(this.allMediaTypes)); + return Mono.error(new NotAcceptableStatusException(this.supportedMediaTypes)); } private List getCompatibleMediaTypes(ServerHttpRequest request, @@ -174,11 +172,12 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered List producibleMediaTypes = getProducibleMediaTypes(elementType); Set compatibleMediaTypes = new LinkedHashSet<>(); - for (MediaType acceptableMediaType : acceptableMediaTypes) { - compatibleMediaTypes.addAll(producibleMediaTypes.stream(). - filter(acceptableMediaType::isCompatibleWith). - map(producibleType -> getMostSpecificMediaType(acceptableMediaType, - producibleType)).collect(Collectors.toList())); + for (MediaType acceptable : acceptableMediaTypes) { + for (MediaType producible : producibleMediaTypes) { + if (acceptable.isCompatibleWith(producible)) { + compatibleMediaTypes.add(getMostSpecificMediaType(acceptable, producible)); + } + } } List result = new ArrayList<>(compatibleMediaTypes); @@ -191,44 +190,37 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered return (mediaTypes.isEmpty() ? Collections.singletonList(MediaType.ALL) : mediaTypes); } - private Optional selectBestMediaType( - List compatibleMediaTypes) { + private List getProducibleMediaTypes(ResolvableType type) { + return this.messageConverters.stream() + .filter(converter -> converter.canWrite(type, null)) + .flatMap(converter -> converter.getWritableMediaTypes().stream()) + .collect(Collectors.collectingAndThen(Collectors.toList(), result -> { + if (result.isEmpty()) { + result.add(MediaType.ALL); + } + return result; + })); + } + + private MediaType getMostSpecificMediaType(MediaType acceptable, MediaType producible) { + producible = producible.copyQualityValue(acceptable); + Comparator comparator = MediaType.SPECIFICITY_COMPARATOR; + return (comparator.compare(acceptable, producible) <= 0 ? acceptable : producible); + } + + private MediaType selectBestMediaType(List compatibleMediaTypes) { for (MediaType mediaType : compatibleMediaTypes) { if (mediaType.isConcrete()) { - return Optional.of(mediaType); + return mediaType; } - else if (mediaType.equals(MediaType.ALL) || - mediaType.equals(MEDIA_TYPE_APPLICATION)) { - return Optional.of(MediaType.APPLICATION_OCTET_STREAM); + else if (mediaType.equals(MediaType.ALL) || mediaType.equals(MEDIA_TYPE_APPLICATION_ALL)) { + return MediaType.APPLICATION_OCTET_STREAM; } } - return Optional.empty(); + return null; } - private List getProducibleMediaTypes(ResolvableType type) { - List result = this.messageConverters.stream() - .filter(converter -> converter.canWrite(type, null)) - .flatMap(encoder -> this.mediaTypesByEncoder.get(encoder).stream()) - .collect(Collectors.toList()); - if (result.isEmpty()) { - result.add(MediaType.ALL); - } - - return result; - } - - /** - * Return the more specific of the acceptable and the producible media types - * with the q-value of the former. - */ - private MediaType getMostSpecificMediaType(MediaType acceptType, MediaType produceType) { - produceType = produceType.copyQualityValue(acceptType); - Comparator comparator = MediaType.SPECIFICITY_COMPARATOR; - return (comparator.compare(acceptType, produceType) <= 0 ? acceptType : produceType); - } - - private HttpMessageConverter resolveEncoder(ResolvableType type, - MediaType mediaType) { + private HttpMessageConverter resolveEncoder(ResolvableType type, MediaType mediaType) { for (HttpMessageConverter converter : this.messageConverters) { if (converter.canWrite(type, mediaType)) { return converter; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/ViewResolverResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/ViewResolverResultHandler.java index bdf89c54c2..d4a22dce13 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/ViewResolverResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/ViewResolverResultHandler.java @@ -53,7 +53,7 @@ public class ViewResolverResultHandler implements HandlerResultHandler, Ordered private final ConversionService conversionService; - private int order = Integer.MAX_VALUE; + private int order = Ordered.LOWEST_PRECEDENCE; public ViewResolverResultHandler(List resolvers, ConversionService service) { @@ -87,17 +87,17 @@ public class ViewResolverResultHandler implements HandlerResultHandler, Ordered @Override public boolean supports(HandlerResult result) { Class clazz = result.getReturnValueType().getRawClass(); - if (isViewNameOrViewReference(clazz)) { + if (isStringOrViewReference(clazz)) { return true; } if (this.conversionService.canConvert(clazz, Mono.class)) { clazz = result.getReturnValueType().getGeneric(0).getRawClass(); - return isViewNameOrViewReference(clazz); + return isStringOrViewReference(clazz); } return false; } - private boolean isViewNameOrViewReference(Class clazz) { + private boolean isStringOrViewReference(Class clazz) { return (CharSequence.class.isAssignableFrom(clazz) || View.class.isAssignableFrom(clazz)); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java index 38106e7465..c439417d1f 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java @@ -16,37 +16,44 @@ package org.springframework.web.reactive.result.method.annotation; -import java.util.Collections; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; import org.junit.Test; import org.reactivestreams.Publisher; import org.springframework.core.ResolvableType; +import org.springframework.core.codec.Encoder; import org.springframework.core.codec.support.StringEncoder; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.http.converter.reactive.CodecHttpMessageConverter; +import org.springframework.http.converter.reactive.HttpMessageConverter; import org.springframework.ui.ExtendedModelMap; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerResult; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; + /** + * Unit tests for {@link ResponseBodyResultHandler}. + * * @author Sebastien Deleuze + * @author Rossen Stoyanchev */ public class ResponseBodyResultHandlerTests { @Test public void supports() throws NoSuchMethodException { - ResponseBodyResultHandler handler = new ResponseBodyResultHandler(Collections.singletonList( - new CodecHttpMessageConverter(new StringEncoder(), null)), - new DefaultConversionService()); + ResponseBodyResultHandler handler = createResultHandler(new StringEncoder()); TestController controller = new TestController(); - HandlerMethod hm = new HandlerMethod(controller,TestController.class.getMethod("notAnnotated")); + HandlerMethod hm = new HandlerMethod(controller, TestController.class.getMethod("notAnnotated")); ResolvableType type = ResolvableType.forMethodParameter(hm.getReturnType()); assertFalse(handler.supports(new HandlerResult(hm, null, type, new ExtendedModelMap()))); @@ -59,6 +66,21 @@ public class ResponseBodyResultHandlerTests { assertTrue(handler.supports(new HandlerResult(hm, null, type, new ExtendedModelMap()))); } + @Test + public void defaultOrder() throws Exception { + ResponseBodyResultHandler handler = createResultHandler(new StringEncoder()); + assertEquals(0, handler.getOrder()); + } + + + private ResponseBodyResultHandler createResultHandler(Encoder... encoders) { + List> converters = Arrays.stream(encoders) + .map(encoder -> new CodecHttpMessageConverter<>(encoder, null)) + .collect(Collectors.toList()); + return new ResponseBodyResultHandler(converters, new DefaultConversionService()); + } + + @SuppressWarnings("unused") private static class TestController { From 2292e46b04c65d4f3f8193b0eab249a8058e59a8 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 25 May 2016 17:35:58 -0400 Subject: [PATCH 349/498] Update empty return value ResponseBody handling When a null is returned from an @ResponseBody method, rather than returning Mono.empty() immediately, convert it to Mono.empty() and apply the same processing. Currently that doesn't make a practical difference but it's more accurate to do it this way. Eventually it may mean the possibility to turn empty values into something through an extension point as we do with ResponseBodyAdvice in Spring MVC today. --- .../annotation/ResponseBodyResultHandler.java | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java index cea0cfc029..fd4ce3c8e3 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java @@ -126,23 +126,25 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered @SuppressWarnings("unchecked") public Mono handleResult(ServerWebExchange exchange, HandlerResult result) { - Optional value = result.getReturnValue(); - if (!value.isPresent()) { - return Mono.empty(); - } - Publisher publisher; ResolvableType elementType; ResolvableType returnType = result.getReturnValueType(); + if (this.conversionService.canConvert(returnType.getRawClass(), Publisher.class)) { - publisher = this.conversionService.convert(value.get(), Publisher.class); + Optional optionalValue = result.getReturnValue(); + if (optionalValue.isPresent()) { + publisher = this.conversionService.convert(optionalValue.get(), Publisher.class); + } + else { + publisher = Mono.empty(); + } elementType = returnType.getGeneric(0); if (Void.class.equals(elementType.getRawClass())) { return Mono.from((Publisher)publisher); } } else { - publisher = Mono.just(value.get()); + publisher = Mono.justOrEmpty(result.getReturnValue()); elementType = returnType; } From 37404d081e807c1f4926590a78f1d4da266fb5c3 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 26 May 2016 10:52:19 -0400 Subject: [PATCH 350/498] Use ContentTypeResolver for content negotiation --- .../annotation/ResponseBodyResultHandler.java | 44 +++++++++++++---- .../RequestMappingIntegrationTests.java | 13 ++--- .../ResponseBodyResultHandlerTests.java | 49 +++++++++++++++++-- 3 files changed, 84 insertions(+), 22 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java index fd4ce3c8e3..976b6f193d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java @@ -42,6 +42,8 @@ import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.HandlerResultHandler; +import org.springframework.web.reactive.accept.ContentTypeResolver; +import org.springframework.web.reactive.accept.HeaderContentTypeResolver; import org.springframework.web.server.NotAcceptableStatusException; import org.springframework.web.server.ServerWebExchange; @@ -64,23 +66,44 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered private final ConversionService conversionService; + private final ContentTypeResolver contentTypeResolver; + private final List supportedMediaTypes; private int order = 0; /** - * Constructor with message converters and conversion service. + * Constructor with message converters and a {@code ConversionService} only + * and creating a {@link HeaderContentTypeResolver}, i.e. using Accept header + * to determine the requested content type. + * + * @param converters converters for writing the response body with + * @param conversionService for converting to Flux and Mono from other reactive types + */ + public ResponseBodyResultHandler(List> converters, + ConversionService conversionService) { + + this(converters, conversionService, new HeaderContentTypeResolver()); + } + + /** + * Constructor with message converters, a {@code ConversionService}, and a + * {@code ContentTypeResolver}. + * * @param messageConverters converters for writing the response body with * @param conversionService for converting to Flux and Mono from other reactive types */ public ResponseBodyResultHandler(List> messageConverters, - ConversionService conversionService) { + ConversionService conversionService, ContentTypeResolver contentTypeResolver) { Assert.notEmpty(messageConverters, "At least one message converter is required."); Assert.notNull(conversionService, "'conversionService' is required."); + Assert.notNull(contentTypeResolver, "'contentTypeResolver' is required."); + this.messageConverters = messageConverters; this.conversionService = conversionService; + this.contentTypeResolver = contentTypeResolver; this.supportedMediaTypes = initSupportedMediaTypes(messageConverters); } @@ -149,10 +172,13 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered } ServerHttpRequest request = exchange.getRequest(); - List compatibleMediaTypes = getCompatibleMediaTypes(request, elementType); + List compatibleMediaTypes = getCompatibleMediaTypes(exchange, elementType); if (compatibleMediaTypes.isEmpty()) { - List supported = getProducibleMediaTypes(elementType); - return Mono.error(new NotAcceptableStatusException(supported)); + if (result.getReturnValue().isPresent()) { + List mediaTypes = getProducibleMediaTypes(elementType); + return Mono.error(new NotAcceptableStatusException(mediaTypes)); + } + return Mono.empty(); } MediaType bestMediaType = selectBestMediaType(compatibleMediaTypes); @@ -167,10 +193,10 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered return Mono.error(new NotAcceptableStatusException(this.supportedMediaTypes)); } - private List getCompatibleMediaTypes(ServerHttpRequest request, + private List getCompatibleMediaTypes(ServerWebExchange exchange, ResolvableType elementType) { - List acceptableMediaTypes = getAcceptableMediaTypes(request); + List acceptableMediaTypes = getAcceptableMediaTypes(exchange); List producibleMediaTypes = getProducibleMediaTypes(elementType); Set compatibleMediaTypes = new LinkedHashSet<>(); @@ -187,8 +213,8 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered return result; } - private List getAcceptableMediaTypes(ServerHttpRequest request) { - List mediaTypes = request.getHeaders().getAccept(); + private List getAcceptableMediaTypes(ServerWebExchange exchange) { + List mediaTypes = this.contentTypeResolver.resolveMediaTypes(exchange); return (mediaTypes.isEmpty() ? Collections.singletonList(MediaType.ALL) : mediaTypes); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java index 594f2ede8c..5075cdbed7 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java @@ -406,14 +406,11 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @Bean public ResponseBodyResultHandler responseBodyResultHandler() { - List> converters = - Arrays.asList(new ResourceHttpMessageConverter(), - new CodecHttpMessageConverter( - new ByteBufferEncoder(), new ByteBufferDecoder()), - new CodecHttpMessageConverter(new StringEncoder(), - new StringDecoder()), - new CodecHttpMessageConverter( - new JacksonJsonEncoder(), new JacksonJsonDecoder())); + List> converters = Arrays.asList( + new ResourceHttpMessageConverter(), + new CodecHttpMessageConverter<>(new ByteBufferEncoder(), new ByteBufferDecoder()), + new CodecHttpMessageConverter<>(new StringEncoder(), new StringDecoder()), + new CodecHttpMessageConverter<>(new JacksonJsonEncoder(), new JacksonJsonDecoder())); ResponseBodyResultHandler resultHandler = new ResponseBodyResultHandler(converters, conversionService()); resultHandler.setOrder(1); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java index c439417d1f..c30e36d075 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java @@ -16,6 +16,8 @@ package org.springframework.web.reactive.result.method.annotation; +import java.net.URI; +import java.net.URISyntaxException; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; @@ -25,23 +27,36 @@ import org.reactivestreams.Publisher; import org.springframework.core.ResolvableType; import org.springframework.core.codec.Encoder; +import org.springframework.core.codec.support.JacksonJsonEncoder; import org.springframework.core.codec.support.StringEncoder; import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; import org.springframework.http.converter.reactive.CodecHttpMessageConverter; import org.springframework.http.converter.reactive.HttpMessageConverter; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.ui.ExtendedModelMap; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerResult; +import org.springframework.web.reactive.HandlerResultHandler; +import org.springframework.web.reactive.accept.ContentTypeResolver; +import org.springframework.web.reactive.accept.FixedContentTypeResolver; +import org.springframework.web.reactive.accept.HeaderContentTypeResolver; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.WebSessionManager; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; /** * Unit tests for {@link ResponseBodyResultHandler}. - * * @author Sebastien Deleuze * @author Rossen Stoyanchev */ @@ -50,7 +65,7 @@ public class ResponseBodyResultHandlerTests { @Test public void supports() throws NoSuchMethodException { - ResponseBodyResultHandler handler = createResultHandler(new StringEncoder()); + ResponseBodyResultHandler handler = createHandler(new StringEncoder()); TestController controller = new TestController(); HandlerMethod hm = new HandlerMethod(controller, TestController.class.getMethod("notAnnotated")); @@ -68,18 +83,42 @@ public class ResponseBodyResultHandlerTests { @Test public void defaultOrder() throws Exception { - ResponseBodyResultHandler handler = createResultHandler(new StringEncoder()); + ResponseBodyResultHandler handler = createHandler(new StringEncoder()); assertEquals(0, handler.getOrder()); } + @Test + public void contentTypeResolver() throws Exception { + MediaType contentType = MediaType.APPLICATION_JSON_UTF8; + ContentTypeResolver resolver = new FixedContentTypeResolver(contentType); + HandlerResultHandler handler = createHandler(resolver, new StringEncoder(), new JacksonJsonEncoder()); + + ServerWebExchange exchange = createExchange("/foo"); + HandlerResult result = new HandlerResult(new Object(), "fooValue", ResolvableType.forClass(String.class)); + handler.handleResult(exchange, result).get(); + + assertEquals(contentType, exchange.getResponse().getHeaders().getContentType()); + } + + + private ResponseBodyResultHandler createHandler(Encoder... encoders) { + return createHandler(new HeaderContentTypeResolver(), encoders); + } + + private ResponseBodyResultHandler createHandler(ContentTypeResolver resolver, + Encoder... encoders) { - private ResponseBodyResultHandler createResultHandler(Encoder... encoders) { List> converters = Arrays.stream(encoders) .map(encoder -> new CodecHttpMessageConverter<>(encoder, null)) .collect(Collectors.toList()); - return new ResponseBodyResultHandler(converters, new DefaultConversionService()); + return new ResponseBodyResultHandler(converters, new DefaultConversionService(), resolver); } + private ServerWebExchange createExchange(String path) throws URISyntaxException { + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI(path)); + WebSessionManager sessionManager = mock(WebSessionManager.class); + return new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); + } @SuppressWarnings("unused") From a017e2848a792038b10eec635ab10e4809a2883b Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 26 May 2016 11:05:48 -0400 Subject: [PATCH 351/498] Rename ContentTypeResolver to RequestedContentTypeResolver --- .../AbstractMappingContentTypeResolver.java | 2 +- .../accept/CompositeContentTypeResolver.java | 20 +++++------ .../accept/FixedContentTypeResolver.java | 4 +-- .../accept/HeaderContentTypeResolver.java | 4 +-- .../accept/MappingContentTypeResolver.java | 5 ++- .../accept/ParameterContentTypeResolver.java | 2 +- .../PathExtensionContentTypeResolver.java | 2 +- ...java => RequestedContentTypeResolver.java} | 6 ++-- ... RequestedContentTypeResolverBuilder.java} | 34 +++++++++---------- .../condition/ProducesRequestCondition.java | 12 +++---- .../result/method/RequestMappingInfo.java | 12 +++---- .../RequestMappingHandlerMapping.java | 18 +++++----- .../annotation/ResponseBodyResultHandler.java | 8 ++--- ...positeContentTypeResolverBuilderTests.java | 20 +++++------ .../ResponseBodyResultHandlerTests.java | 6 ++-- 15 files changed, 78 insertions(+), 77 deletions(-) rename spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/{ContentTypeResolver.java => RequestedContentTypeResolver.java} (84%) rename spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/{CompositeContentTypeResolverBuilder.java => RequestedContentTypeResolverBuilder.java} (86%) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/AbstractMappingContentTypeResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/AbstractMappingContentTypeResolver.java index 37f9f77d4a..a1f993f9ca 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/AbstractMappingContentTypeResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/AbstractMappingContentTypeResolver.java @@ -81,7 +81,7 @@ public abstract class AbstractMappingContentTypeResolver implements MappingConte } - // ContentTypeResolver implementation + // RequestedContentTypeResolver implementation @Override public List resolveMediaTypes(ServerWebExchange exchange) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/CompositeContentTypeResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/CompositeContentTypeResolver.java index 68df906508..a60a914512 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/CompositeContentTypeResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/CompositeContentTypeResolver.java @@ -27,7 +27,7 @@ import org.springframework.web.server.NotAcceptableStatusException; import org.springframework.web.server.ServerWebExchange; /** - * A {@link ContentTypeResolver} that contains and delegates to a list of other + * A {@link RequestedContentTypeResolver} that contains and delegates to a list of other * resolvers. * *

    Also an implementation of {@link MappingContentTypeResolver} that delegates @@ -38,10 +38,10 @@ import org.springframework.web.server.ServerWebExchange; */ public class CompositeContentTypeResolver implements MappingContentTypeResolver { - private final List resolvers = new ArrayList<>(); + private final List resolvers = new ArrayList<>(); - public CompositeContentTypeResolver(List resolvers) { + public CompositeContentTypeResolver(List resolvers) { Assert.notEmpty(resolvers, "At least one resolver is expected."); this.resolvers.addAll(resolvers); } @@ -50,18 +50,18 @@ public class CompositeContentTypeResolver implements MappingContentTypeResolver /** * Return a read-only list of the configured resolvers. */ - public List getResolvers() { + public List getResolvers() { return Collections.unmodifiableList(this.resolvers); } /** - * Return the first {@link ContentTypeResolver} of the given type. + * Return the first {@link RequestedContentTypeResolver} of the given type. * @param resolverType the resolver type * @return the first matching resolver or {@code null}. */ @SuppressWarnings("unchecked") - public T findResolver(Class resolverType) { - for (ContentTypeResolver resolver : this.resolvers) { + public T findResolver(Class resolverType) { + for (RequestedContentTypeResolver resolver : this.resolvers) { if (resolverType.isInstance(resolver)) { return (T) resolver; } @@ -72,7 +72,7 @@ public class CompositeContentTypeResolver implements MappingContentTypeResolver @Override public List resolveMediaTypes(ServerWebExchange exchange) throws NotAcceptableStatusException { - for (ContentTypeResolver resolver : this.resolvers) { + for (RequestedContentTypeResolver resolver : this.resolvers) { List mediaTypes = resolver.resolveMediaTypes(exchange); if (mediaTypes.isEmpty() || (mediaTypes.size() == 1 && mediaTypes.contains(MediaType.ALL))) { continue; @@ -85,7 +85,7 @@ public class CompositeContentTypeResolver implements MappingContentTypeResolver @Override public Set getKeysFor(MediaType mediaType) { Set result = new LinkedHashSet<>(); - for (ContentTypeResolver resolver : this.resolvers) { + for (RequestedContentTypeResolver resolver : this.resolvers) { if (resolver instanceof MappingContentTypeResolver) result.addAll(((MappingContentTypeResolver) resolver).getKeysFor(mediaType)); } @@ -95,7 +95,7 @@ public class CompositeContentTypeResolver implements MappingContentTypeResolver @Override public Set getKeys() { Set result = new LinkedHashSet<>(); - for (ContentTypeResolver resolver : this.resolvers) { + for (RequestedContentTypeResolver resolver : this.resolvers) { if (resolver instanceof MappingContentTypeResolver) result.addAll(((MappingContentTypeResolver) resolver).getKeys()); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/FixedContentTypeResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/FixedContentTypeResolver.java index a6c73bc0d1..5f145945a2 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/FixedContentTypeResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/FixedContentTypeResolver.java @@ -23,11 +23,11 @@ import org.springframework.http.MediaType; import org.springframework.web.server.ServerWebExchange; /** - * A {@link ContentTypeResolver} that resolves to a fixed list of media types. + * A {@link RequestedContentTypeResolver} that resolves to a fixed list of media types. * * @author Rossen Stoyanchev */ -public class FixedContentTypeResolver implements ContentTypeResolver { +public class FixedContentTypeResolver implements RequestedContentTypeResolver { private final List mediaTypes; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/HeaderContentTypeResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/HeaderContentTypeResolver.java index 4ef573a93c..f6c2f04865 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/HeaderContentTypeResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/HeaderContentTypeResolver.java @@ -23,11 +23,11 @@ import org.springframework.web.server.NotAcceptableStatusException; import org.springframework.web.server.ServerWebExchange; /** - * A {@link ContentTypeResolver} that checks the 'Accept' request header. + * A {@link RequestedContentTypeResolver} that checks the 'Accept' request header. * * @author Rossen Stoyanchev */ -public class HeaderContentTypeResolver implements ContentTypeResolver { +public class HeaderContentTypeResolver implements RequestedContentTypeResolver { @Override public List resolveMediaTypes(ServerWebExchange exchange) throws NotAcceptableStatusException { diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/MappingContentTypeResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/MappingContentTypeResolver.java index 645beed077..64bd272b1c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/MappingContentTypeResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/MappingContentTypeResolver.java @@ -16,18 +16,17 @@ package org.springframework.web.reactive.accept; -import java.util.List; import java.util.Set; import org.springframework.http.MediaType; /** - * An extension of {@link ContentTypeResolver} that maintains a mapping between + * An extension of {@link RequestedContentTypeResolver} that maintains a mapping between * keys (e.g. file extension, query parameter) and media types. * * @author Rossen Stoyanchev */ -public interface MappingContentTypeResolver extends ContentTypeResolver { +public interface MappingContentTypeResolver extends RequestedContentTypeResolver { /** * Resolve the given media type to a list of path extensions. diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/ParameterContentTypeResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/ParameterContentTypeResolver.java index 70641c3b45..63971b7e83 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/ParameterContentTypeResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/ParameterContentTypeResolver.java @@ -27,7 +27,7 @@ import org.springframework.web.server.NotAcceptableStatusException; import org.springframework.web.server.ServerWebExchange; /** - * A {@link ContentTypeResolver} that extracts the media type lookup key from a + * A {@link RequestedContentTypeResolver} that extracts the media type lookup key from a * known query parameter named "format" by default. *s * @author Rossen Stoyanchev diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/PathExtensionContentTypeResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/PathExtensionContentTypeResolver.java index 2326797943..d9f07e2503 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/PathExtensionContentTypeResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/PathExtensionContentTypeResolver.java @@ -32,7 +32,7 @@ import org.springframework.web.server.ServerWebExchange; import org.springframework.web.util.WebUtils; /** - * A {@link ContentTypeResolver} that extracts the file extension from the + * A {@link RequestedContentTypeResolver} that extracts the file extension from the * request path and uses that as the media type lookup key. * *

    If the file extension is not found in the explicit registrations provided diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/ContentTypeResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/RequestedContentTypeResolver.java similarity index 84% rename from spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/ContentTypeResolver.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/RequestedContentTypeResolver.java index e359cdeed5..c2476f1b11 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/ContentTypeResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/RequestedContentTypeResolver.java @@ -22,10 +22,11 @@ import org.springframework.web.server.NotAcceptableStatusException; import org.springframework.web.server.ServerWebExchange; /** + * Strategy for resolving the requested media types for a {@code ServerWebExchange}. * * @author Rossen Stoyanchev */ -public interface ContentTypeResolver { +public interface RequestedContentTypeResolver { /** * Resolve the given request to a list of requested media types. The returned @@ -36,6 +37,7 @@ public interface ContentTypeResolver { * * @throws NotAcceptableStatusException if the requested media types is invalid */ - List resolveMediaTypes(ServerWebExchange exchange) throws NotAcceptableStatusException; + List resolveMediaTypes(ServerWebExchange exchange) + throws NotAcceptableStatusException; } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/CompositeContentTypeResolverBuilder.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/RequestedContentTypeResolverBuilder.java similarity index 86% rename from spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/CompositeContentTypeResolverBuilder.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/RequestedContentTypeResolverBuilder.java index f545c08911..d11cbf64e8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/CompositeContentTypeResolverBuilder.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/accept/RequestedContentTypeResolverBuilder.java @@ -28,7 +28,7 @@ import org.springframework.util.CollectionUtils; /** * Factory to create a {@link CompositeContentTypeResolver} and configure it with - * one or more {@link ContentTypeResolver} instances with build style methods. + * one or more {@link RequestedContentTypeResolver} instances with build style methods. * The following table shows methods, resulting strategy instances, and if in * use by default: * @@ -60,7 +60,7 @@ import org.springframework.util.CollectionUtils; * * * {@link #defaultContentTypeResolver} - * {@link ContentTypeResolver} + * {@link RequestedContentTypeResolver} * Not set * * @@ -79,7 +79,7 @@ import org.springframework.util.CollectionUtils; * * @author Rossen Stoyanchev */ -public class CompositeContentTypeResolverBuilder { +public class RequestedContentTypeResolverBuilder { private boolean favorPathExtension = true; @@ -95,7 +95,7 @@ public class CompositeContentTypeResolverBuilder { private String parameterName = "format"; - private ContentTypeResolver contentTypeResolver; + private RequestedContentTypeResolver contentTypeResolver; /** @@ -105,7 +105,7 @@ public class CompositeContentTypeResolverBuilder { * for {@code /hotels.pdf} will be interpreted as a request for * {@code "application/pdf"} regardless of the 'Accept' header. */ - public CompositeContentTypeResolverBuilder favorPathExtension(boolean favorPathExtension) { + public RequestedContentTypeResolverBuilder favorPathExtension(boolean favorPathExtension) { this.favorPathExtension = favorPathExtension; return this; } @@ -121,7 +121,7 @@ public class CompositeContentTypeResolverBuilder { * resolve path extensions. To change this behavior see {@link #useJaf}. * @param mediaTypes media type mappings */ - public CompositeContentTypeResolverBuilder mediaTypes(Map mediaTypes) { + public RequestedContentTypeResolverBuilder mediaTypes(Map mediaTypes) { if (!CollectionUtils.isEmpty(mediaTypes)) { for (Map.Entry entry : mediaTypes.entrySet()) { String extension = entry.getKey().toLowerCase(Locale.ENGLISH); @@ -134,7 +134,7 @@ public class CompositeContentTypeResolverBuilder { /** * Alternative to {@link #mediaTypes} to add a single mapping. */ - public CompositeContentTypeResolverBuilder mediaType(String key, MediaType mediaType) { + public RequestedContentTypeResolverBuilder mediaType(String key, MediaType mediaType) { this.mediaTypes.put(key, mediaType); return this; } @@ -146,7 +146,7 @@ public class CompositeContentTypeResolverBuilder { * there is no match. *

    By default this is set to {@code true}. */ - public CompositeContentTypeResolverBuilder ignoreUnknownPathExtensions(boolean ignore) { + public RequestedContentTypeResolverBuilder ignoreUnknownPathExtensions(boolean ignore) { this.ignoreUnknownPathExtensions = ignore; return this; } @@ -158,7 +158,7 @@ public class CompositeContentTypeResolverBuilder { *

    By default this is not set in which case * {@code PathExtensionContentNegotiationStrategy} will use JAF if available. */ - public CompositeContentTypeResolverBuilder useJaf(boolean useJaf) { + public RequestedContentTypeResolverBuilder useJaf(boolean useJaf) { this.useJaf = useJaf; return this; } @@ -170,7 +170,7 @@ public class CompositeContentTypeResolverBuilder { *

    By default this is set to {@code false}. * @see #parameterName */ - public CompositeContentTypeResolverBuilder favorParameter(boolean favorParameter) { + public RequestedContentTypeResolverBuilder favorParameter(boolean favorParameter) { this.favorParameter = favorParameter; return this; } @@ -179,7 +179,7 @@ public class CompositeContentTypeResolverBuilder { * Set the query parameter name to use when {@link #favorParameter} is on. *

    The default parameter name is {@code "format"}. */ - public CompositeContentTypeResolverBuilder parameterName(String parameterName) { + public RequestedContentTypeResolverBuilder parameterName(String parameterName) { Assert.notNull(parameterName, "parameterName is required"); this.parameterName = parameterName; return this; @@ -189,7 +189,7 @@ public class CompositeContentTypeResolverBuilder { * Whether to disable checking the 'Accept' request header. *

    By default this value is set to {@code false}. */ - public CompositeContentTypeResolverBuilder ignoreAcceptHeader(boolean ignoreAcceptHeader) { + public RequestedContentTypeResolverBuilder ignoreAcceptHeader(boolean ignoreAcceptHeader) { this.ignoreAcceptHeader = ignoreAcceptHeader; return this; } @@ -199,25 +199,25 @@ public class CompositeContentTypeResolverBuilder { *

    By default this is not set. * @see #defaultContentTypeResolver */ - public CompositeContentTypeResolverBuilder defaultContentType(MediaType contentType) { + public RequestedContentTypeResolverBuilder defaultContentType(MediaType contentType) { this.contentTypeResolver = new FixedContentTypeResolver(contentType); return this; } /** - * Set a custom {@link ContentTypeResolver} to use to determine + * Set a custom {@link RequestedContentTypeResolver} to use to determine * the content type to use when no content type is requested. *

    By default this is not set. * @see #defaultContentType */ - public CompositeContentTypeResolverBuilder defaultContentTypeResolver(ContentTypeResolver resolver) { + public RequestedContentTypeResolverBuilder defaultContentTypeResolver(RequestedContentTypeResolver resolver) { this.contentTypeResolver = resolver; return this; } - public CompositeContentTypeResolver build() { - List resolvers = new ArrayList<>(); + public RequestedContentTypeResolver build() { + List resolvers = new ArrayList<>(); if (this.favorPathExtension) { PathExtensionContentTypeResolver resolver = new PathExtensionContentTypeResolver(this.mediaTypes); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/ProducesRequestCondition.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/ProducesRequestCondition.java index d0c701188b..a962687b73 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/ProducesRequestCondition.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/ProducesRequestCondition.java @@ -27,8 +27,8 @@ import java.util.Set; import org.springframework.http.MediaType; import org.springframework.web.accept.ContentNegotiationManager; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.reactive.accept.CompositeContentTypeResolverBuilder; -import org.springframework.web.reactive.accept.ContentTypeResolver; +import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder; +import org.springframework.web.reactive.accept.RequestedContentTypeResolver; import org.springframework.web.reactive.accept.HeaderContentTypeResolver; import org.springframework.web.server.NotAcceptableStatusException; import org.springframework.web.server.ServerWebExchange; @@ -52,7 +52,7 @@ public final class ProducesRequestCondition extends AbstractRequestCondition expressions; - private final ContentTypeResolver contentTypeResolver; + private final RequestedContentTypeResolver contentTypeResolver; /** @@ -83,7 +83,7 @@ public final class ProducesRequestCondition extends AbstractRequestCondition(parseExpressions(produces, headers)); Collections.sort(this.expressions); this.contentTypeResolver = (resolver != null ? resolver : new HeaderContentTypeResolver()); @@ -93,12 +93,12 @@ public final class ProducesRequestCondition extends AbstractRequestCondition expressions, - ContentTypeResolver resolver) { + RequestedContentTypeResolver resolver) { this.expressions = new ArrayList<>(expressions); Collections.sort(this.expressions); this.contentTypeResolver = (resolver != null ? - resolver : new CompositeContentTypeResolverBuilder().build()); + resolver : new RequestedContentTypeResolverBuilder().build()); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfo.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfo.java index bebceeb3e2..0d09461d20 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfo.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfo.java @@ -21,7 +21,7 @@ import java.util.Set; import org.springframework.util.PathMatcher; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.reactive.accept.ContentTypeResolver; +import org.springframework.web.reactive.accept.RequestedContentTypeResolver; import org.springframework.web.reactive.accept.MappingContentTypeResolver; import org.springframework.web.reactive.result.condition.ConsumesRequestCondition; import org.springframework.web.reactive.result.condition.HeadersRequestCondition; @@ -468,7 +468,7 @@ public final class RequestMappingInfo implements RequestCondition getFileExtensions() { - ContentTypeResolver resolver = getContentTypeResolver(); + RequestedContentTypeResolver resolver = getContentTypeResolver(); if (useRegisteredSuffixPatternMatch() && resolver != null) { if (resolver instanceof MappingContentTypeResolver) { return ((MappingContentTypeResolver) resolver).getKeys(); @@ -590,11 +590,11 @@ public final class RequestMappingInfo implements RequestConditionBy default this is not set. */ - public void setContentTypeResolver(ContentTypeResolver resolver) { + public void setContentTypeResolver(RequestedContentTypeResolver resolver) { this.contentTypeResolver = resolver; } - public ContentTypeResolver getContentTypeResolver() { + public RequestedContentTypeResolver getContentTypeResolver() { return this.contentTypeResolver; } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java index c27350e707..879f34cdc4 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java @@ -26,8 +26,8 @@ import org.springframework.stereotype.Controller; import org.springframework.util.Assert; import org.springframework.util.StringValueResolver; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.reactive.accept.CompositeContentTypeResolverBuilder; -import org.springframework.web.reactive.accept.ContentTypeResolver; +import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder; +import org.springframework.web.reactive.accept.RequestedContentTypeResolver; import org.springframework.web.reactive.result.condition.RequestCondition; import org.springframework.web.reactive.result.method.RequestMappingInfo; import org.springframework.web.reactive.result.method.RequestMappingInfoHandlerMapping; @@ -48,7 +48,7 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi private boolean useTrailingSlashMatch = true; - private ContentTypeResolver contentTypeResolver = new CompositeContentTypeResolverBuilder().build(); + private RequestedContentTypeResolver contentTypeResolver = new RequestedContentTypeResolverBuilder().build(); private StringValueResolver embeddedValueResolver; @@ -70,7 +70,7 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi /** * Whether suffix pattern matching should work only against path extensions - * explicitly registered with the configured {@link ContentTypeResolver}. This + * explicitly registered with the configured {@link RequestedContentTypeResolver}. This * is generally recommended to reduce ambiguity and to avoid issues such as * when a "." appears in the path for other reasons. *

    By default this is set to "true". @@ -90,11 +90,11 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi } /** - * Set the {@link ContentTypeResolver} to use to determine requested media types. + * Set the {@link RequestedContentTypeResolver} to use to determine requested media types. * If not set, the default constructor is used. */ - public void setContentTypeResolver(ContentTypeResolver contentTypeResolver) { - Assert.notNull(contentTypeResolver, "'ContentTypeResolver' must not be null"); + public void setContentTypeResolver(RequestedContentTypeResolver contentTypeResolver) { + Assert.notNull(contentTypeResolver, "'contentTypeResolver' must not be null"); this.contentTypeResolver = contentTypeResolver; } @@ -139,9 +139,9 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi } /** - * Return the configured {@link ContentTypeResolver}. + * Return the configured {@link RequestedContentTypeResolver}. */ - public ContentTypeResolver getContentTypeResolver() { + public RequestedContentTypeResolver getContentTypeResolver() { return this.contentTypeResolver; } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java index 976b6f193d..9299bdb0f1 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java @@ -42,7 +42,7 @@ import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.HandlerResultHandler; -import org.springframework.web.reactive.accept.ContentTypeResolver; +import org.springframework.web.reactive.accept.RequestedContentTypeResolver; import org.springframework.web.reactive.accept.HeaderContentTypeResolver; import org.springframework.web.server.NotAcceptableStatusException; import org.springframework.web.server.ServerWebExchange; @@ -66,7 +66,7 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered private final ConversionService conversionService; - private final ContentTypeResolver contentTypeResolver; + private final RequestedContentTypeResolver contentTypeResolver; private final List supportedMediaTypes; @@ -89,13 +89,13 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered /** * Constructor with message converters, a {@code ConversionService}, and a - * {@code ContentTypeResolver}. + * {@code RequestedContentTypeResolver}. * * @param messageConverters converters for writing the response body with * @param conversionService for converting to Flux and Mono from other reactive types */ public ResponseBodyResultHandler(List> messageConverters, - ConversionService conversionService, ContentTypeResolver contentTypeResolver) { + ConversionService conversionService, RequestedContentTypeResolver contentTypeResolver) { Assert.notEmpty(messageConverters, "At least one message converter is required."); Assert.notNull(conversionService, "'conversionService' is required."); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/accept/CompositeContentTypeResolverBuilderTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/accept/CompositeContentTypeResolverBuilderTests.java index 6295b997a2..9575082149 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/accept/CompositeContentTypeResolverBuilderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/accept/CompositeContentTypeResolverBuilderTests.java @@ -35,7 +35,7 @@ import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.mock; /** - * Unit tests for {@link CompositeContentTypeResolverBuilder}. + * Unit tests for {@link RequestedContentTypeResolverBuilder}. * * @author Rossen Stoyanchev */ @@ -43,7 +43,7 @@ public class CompositeContentTypeResolverBuilderTests { @Test public void defaultSettings() throws Exception { - CompositeContentTypeResolver resolver = new CompositeContentTypeResolverBuilder().build(); + RequestedContentTypeResolver resolver = new RequestedContentTypeResolverBuilder().build(); ServerWebExchange exchange = createExchange("/flower.gif"); @@ -70,7 +70,7 @@ public class CompositeContentTypeResolverBuilderTests { @Test public void favorPath() throws Exception { - CompositeContentTypeResolver resolver = new CompositeContentTypeResolverBuilder() + RequestedContentTypeResolver resolver = new RequestedContentTypeResolverBuilder() .favorPathExtension(true) .mediaType("foo", new MediaType("application", "foo")) .mediaType("bar", new MediaType("application", "bar")) @@ -90,7 +90,7 @@ public class CompositeContentTypeResolverBuilderTests { @Test public void favorPathWithJafTurnedOff() throws Exception { - CompositeContentTypeResolver resolver = new CompositeContentTypeResolverBuilder() + RequestedContentTypeResolver resolver = new RequestedContentTypeResolverBuilder() .favorPathExtension(true) .useJaf(false) .build(); @@ -104,7 +104,7 @@ public class CompositeContentTypeResolverBuilderTests { @Test(expected = NotAcceptableStatusException.class) // SPR-10170 public void favorPathWithIgnoreUnknownPathExtensionTurnedOff() throws Exception { - CompositeContentTypeResolver resolver = new CompositeContentTypeResolverBuilder() + RequestedContentTypeResolver resolver = new RequestedContentTypeResolverBuilder() .favorPathExtension(true) .ignoreUnknownPathExtensions(false) .build(); @@ -117,7 +117,7 @@ public class CompositeContentTypeResolverBuilderTests { @Test public void favorParameter() throws Exception { - CompositeContentTypeResolver resolver = new CompositeContentTypeResolverBuilder() + RequestedContentTypeResolver resolver = new RequestedContentTypeResolverBuilder() .favorParameter(true) .mediaType("json", MediaType.APPLICATION_JSON) .build(); @@ -131,7 +131,7 @@ public class CompositeContentTypeResolverBuilderTests { @Test(expected = NotAcceptableStatusException.class) // SPR-10170 public void favorParameterWithUnknownMediaType() throws Exception { - CompositeContentTypeResolver resolver = new CompositeContentTypeResolverBuilder() + RequestedContentTypeResolver resolver = new RequestedContentTypeResolverBuilder() .favorParameter(true) .build(); @@ -143,7 +143,7 @@ public class CompositeContentTypeResolverBuilderTests { @Test public void ignoreAcceptHeader() throws Exception { - CompositeContentTypeResolver resolver = new CompositeContentTypeResolverBuilder() + RequestedContentTypeResolver resolver = new RequestedContentTypeResolverBuilder() .ignoreAcceptHeader(true) .build(); @@ -155,7 +155,7 @@ public class CompositeContentTypeResolverBuilderTests { @Test // SPR-10513 public void setDefaultContentType() throws Exception { - CompositeContentTypeResolver resolver = new CompositeContentTypeResolverBuilder() + RequestedContentTypeResolver resolver = new RequestedContentTypeResolverBuilder() .defaultContentType(MediaType.APPLICATION_JSON) .build(); @@ -172,7 +172,7 @@ public class CompositeContentTypeResolverBuilderTests { @Test // SPR-12286 public void setDefaultContentTypeWithStrategy() throws Exception { - CompositeContentTypeResolver resolver = new CompositeContentTypeResolverBuilder() + RequestedContentTypeResolver resolver = new RequestedContentTypeResolverBuilder() .defaultContentTypeResolver(new FixedContentTypeResolver(MediaType.APPLICATION_JSON)) .build(); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java index c30e36d075..208bc45ed5 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java @@ -42,7 +42,7 @@ import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.HandlerResultHandler; -import org.springframework.web.reactive.accept.ContentTypeResolver; +import org.springframework.web.reactive.accept.RequestedContentTypeResolver; import org.springframework.web.reactive.accept.FixedContentTypeResolver; import org.springframework.web.reactive.accept.HeaderContentTypeResolver; import org.springframework.web.server.ServerWebExchange; @@ -90,7 +90,7 @@ public class ResponseBodyResultHandlerTests { @Test public void contentTypeResolver() throws Exception { MediaType contentType = MediaType.APPLICATION_JSON_UTF8; - ContentTypeResolver resolver = new FixedContentTypeResolver(contentType); + RequestedContentTypeResolver resolver = new FixedContentTypeResolver(contentType); HandlerResultHandler handler = createHandler(resolver, new StringEncoder(), new JacksonJsonEncoder()); ServerWebExchange exchange = createExchange("/foo"); @@ -105,7 +105,7 @@ public class ResponseBodyResultHandlerTests { return createHandler(new HeaderContentTypeResolver(), encoders); } - private ResponseBodyResultHandler createHandler(ContentTypeResolver resolver, + private ResponseBodyResultHandler createHandler(RequestedContentTypeResolver resolver, Encoder... encoders) { List> converters = Arrays.stream(encoders) From 2263954ad7d760c145eeaf4b02c2e9a68f118ad1 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 26 May 2016 13:34:08 -0400 Subject: [PATCH 352/498] Respect produces condition in @ResponseBody algorithm --- .../annotation/ResponseBodyResultHandler.java | 35 ++++++++++--------- .../ResponseBodyResultHandlerTests.java | 21 ++++++++++- 2 files changed, 38 insertions(+), 18 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java index 9299bdb0f1..c5d71f19fe 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java @@ -35,15 +35,15 @@ import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.convert.ConversionService; import org.springframework.http.MediaType; import org.springframework.http.converter.reactive.HttpMessageConverter; -import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.util.Assert; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.method.HandlerMethod; +import org.springframework.web.reactive.HandlerMapping; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.HandlerResultHandler; -import org.springframework.web.reactive.accept.RequestedContentTypeResolver; import org.springframework.web.reactive.accept.HeaderContentTypeResolver; +import org.springframework.web.reactive.accept.RequestedContentTypeResolver; import org.springframework.web.server.NotAcceptableStatusException; import org.springframework.web.server.ServerWebExchange; @@ -171,11 +171,10 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered elementType = returnType; } - ServerHttpRequest request = exchange.getRequest(); List compatibleMediaTypes = getCompatibleMediaTypes(exchange, elementType); if (compatibleMediaTypes.isEmpty()) { if (result.getReturnValue().isPresent()) { - List mediaTypes = getProducibleMediaTypes(elementType); + List mediaTypes = getProducibleMediaTypes(exchange, elementType); return Mono.error(new NotAcceptableStatusException(mediaTypes)); } return Mono.empty(); @@ -197,13 +196,13 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered ResolvableType elementType) { List acceptableMediaTypes = getAcceptableMediaTypes(exchange); - List producibleMediaTypes = getProducibleMediaTypes(elementType); + List producibleMediaTypes = getProducibleMediaTypes(exchange, elementType); Set compatibleMediaTypes = new LinkedHashSet<>(); for (MediaType acceptable : acceptableMediaTypes) { for (MediaType producible : producibleMediaTypes) { if (acceptable.isCompatibleWith(producible)) { - compatibleMediaTypes.add(getMostSpecificMediaType(acceptable, producible)); + compatibleMediaTypes.add(selectMoreSpecificMediaType(acceptable, producible)); } } } @@ -218,19 +217,21 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered return (mediaTypes.isEmpty() ? Collections.singletonList(MediaType.ALL) : mediaTypes); } - private List getProducibleMediaTypes(ResolvableType type) { - return this.messageConverters.stream() - .filter(converter -> converter.canWrite(type, null)) - .flatMap(converter -> converter.getWritableMediaTypes().stream()) - .collect(Collectors.collectingAndThen(Collectors.toList(), result -> { - if (result.isEmpty()) { - result.add(MediaType.ALL); - } - return result; - })); + private List getProducibleMediaTypes(ServerWebExchange exchange, ResolvableType type) { + Optional optional = exchange.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE); + if (optional.isPresent()) { + Set mediaTypes = (Set) optional.get(); + return new ArrayList<>(mediaTypes); + } + else { + return this.messageConverters.stream() + .filter(converter -> converter.canWrite(type, null)) + .flatMap(converter -> converter.getWritableMediaTypes().stream()) + .collect(Collectors.toList()); + } } - private MediaType getMostSpecificMediaType(MediaType acceptable, MediaType producible) { + private MediaType selectMoreSpecificMediaType(MediaType acceptable, MediaType producible) { producible = producible.copyQualityValue(acceptable); Comparator comparator = MediaType.SPECIFICITY_COMPARATOR; return (comparator.compare(acceptable, producible) <= 0 ? acceptable : producible); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java index 208bc45ed5..15043cb1f8 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java @@ -19,9 +19,13 @@ package org.springframework.web.reactive.result.method.annotation; import java.net.URI; import java.net.URISyntaxException; import java.util.Arrays; +import java.util.Collections; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; +import javax.print.attribute.standard.Media; + import org.junit.Test; import org.reactivestreams.Publisher; @@ -40,6 +44,7 @@ import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.ui.ExtendedModelMap; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.method.HandlerMethod; +import org.springframework.web.reactive.HandlerMapping; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.HandlerResultHandler; import org.springframework.web.reactive.accept.RequestedContentTypeResolver; @@ -88,7 +93,7 @@ public class ResponseBodyResultHandlerTests { } @Test - public void contentTypeResolver() throws Exception { + public void usesContentTypeResolver() throws Exception { MediaType contentType = MediaType.APPLICATION_JSON_UTF8; RequestedContentTypeResolver resolver = new FixedContentTypeResolver(contentType); HandlerResultHandler handler = createHandler(resolver, new StringEncoder(), new JacksonJsonEncoder()); @@ -100,6 +105,20 @@ public class ResponseBodyResultHandlerTests { assertEquals(contentType, exchange.getResponse().getHeaders().getContentType()); } + @Test + public void detectsProducibleMediaTypesAttribute() throws Exception { + ServerWebExchange exchange = createExchange("/foo"); + Set mediaTypes = Collections.singleton(MediaType.APPLICATION_JSON); + exchange.getAttributes().put(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, mediaTypes); + + HandlerResultHandler handler = createHandler(new StringEncoder(), new JacksonJsonEncoder()); + + HandlerResult result = new HandlerResult(new Object(), "fooValue", ResolvableType.forClass(String.class)); + handler.handleResult(exchange, result).get(); + + assertEquals(MediaType.APPLICATION_JSON, exchange.getResponse().getHeaders().getContentType()); + } + private ResponseBodyResultHandler createHandler(Encoder... encoders) { return createHandler(new HeaderContentTypeResolver(), encoders); From 318b15c321963ad772ae9e4762c25b205047a463 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 26 May 2016 13:39:16 -0400 Subject: [PATCH 353/498] Polish --- .../annotation/ResponseBodyResultHandler.java | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java index c5d71f19fe..5cc5fa3960 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java @@ -182,10 +182,11 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered MediaType bestMediaType = selectBestMediaType(compatibleMediaTypes); if (bestMediaType != null) { - HttpMessageConverter converter = resolveEncoder(elementType, bestMediaType); - if (converter != null) { - ServerHttpResponse response = exchange.getResponse(); - return converter.write((Publisher) publisher, elementType, bestMediaType, response); + for (HttpMessageConverter converter : this.messageConverters) { + if (converter.canWrite(elementType, bestMediaType)) { + ServerHttpResponse response = exchange.getResponse(); + return converter.write((Publisher) publisher, elementType, bestMediaType, response); + } } } @@ -249,13 +250,4 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered return null; } - private HttpMessageConverter resolveEncoder(ResolvableType type, MediaType mediaType) { - for (HttpMessageConverter converter : this.messageConverters) { - if (converter.canWrite(type, mediaType)) { - return converter; - } - } - return null; - } - } From 4d04bd5690c07c6f6ac739344755583532a39c1d Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 26 May 2016 15:22:03 -0400 Subject: [PATCH 354/498] Consolidate view resolution under result.view While View and ViewResolver play the same role as in spring-webmvc they are now abstracted behind the HandlerResultHandler abstraction so that top-level contracts don't reference them and the DispatcherHandler is also unaware of their existence. Furthermore view resolution and response body handling which are now at the same level of abstraction (each is a HandlerResultHandler) will also share code for content negotiation, so it makes sense for them to be side by side. This commit moves the reactive.view package to reactive.result.view with the View and ViewResolver contracts (previously in the top-level reactive package) also moving there. --- .../web/reactive/result/package-info.java | 4 ++- .../view/AbstractUrlBasedView.java | 2 +- .../{ => result}/view/AbstractView.java | 4 +-- .../view/UrlBasedViewResolver.java | 28 ++++++++----------- .../web/reactive/{ => result/view}/View.java | 3 +- .../{ => result/view}/ViewResolver.java | 6 ++-- .../view/ViewResolverResultHandler.java | 7 ++--- .../view/ViewResolverSupport.java | 5 ++-- .../view/freemarker/FreeMarkerConfig.java | 2 +- .../view/freemarker/FreeMarkerConfigurer.java | 2 +- .../view/freemarker/FreeMarkerView.java | 4 +-- .../freemarker/FreeMarkerViewResolver.java | 4 +-- .../{ => result}/view/package-info.java | 2 +- .../RequestMappingIntegrationTests.java | 14 ++++++---- .../view/UrlBasedViewResolverTests.java | 3 +- .../view/ViewResolverResultHandlerTests.java | 9 +++--- .../view/freemarker/FreeMarkerViewTests.java | 6 +--- .../{ => result}/view/freemarker/test.ftl | 0 18 files changed, 49 insertions(+), 56 deletions(-) rename spring-web-reactive/src/main/java/org/springframework/web/reactive/{ => result}/view/AbstractUrlBasedView.java (97%) rename spring-web-reactive/src/main/java/org/springframework/web/reactive/{ => result}/view/AbstractView.java (97%) rename spring-web-reactive/src/main/java/org/springframework/web/reactive/{ => result}/view/UrlBasedViewResolver.java (86%) rename spring-web-reactive/src/main/java/org/springframework/web/reactive/{ => result/view}/View.java (95%) rename spring-web-reactive/src/main/java/org/springframework/web/reactive/{ => result/view}/ViewResolver.java (76%) rename spring-web-reactive/src/main/java/org/springframework/web/reactive/{ => result}/view/ViewResolverResultHandler.java (95%) rename spring-web-reactive/src/main/java/org/springframework/web/reactive/{ => result}/view/ViewResolverSupport.java (94%) rename spring-web-reactive/src/main/java/org/springframework/web/reactive/{ => result}/view/freemarker/FreeMarkerConfig.java (95%) rename spring-web-reactive/src/main/java/org/springframework/web/reactive/{ => result}/view/freemarker/FreeMarkerConfigurer.java (98%) rename spring-web-reactive/src/main/java/org/springframework/web/reactive/{ => result}/view/freemarker/FreeMarkerView.java (98%) rename spring-web-reactive/src/main/java/org/springframework/web/reactive/{ => result}/view/freemarker/FreeMarkerViewResolver.java (91%) rename spring-web-reactive/src/main/java/org/springframework/web/reactive/{ => result}/view/package-info.java (54%) rename spring-web-reactive/src/test/java/org/springframework/web/reactive/{ => result}/view/UrlBasedViewResolverTests.java (95%) rename spring-web-reactive/src/test/java/org/springframework/web/reactive/{ => result}/view/ViewResolverResultHandlerTests.java (97%) rename spring-web-reactive/src/test/java/org/springframework/web/reactive/{ => result}/view/freemarker/FreeMarkerViewTests.java (95%) rename spring-web-reactive/src/test/resources/org/springframework/web/reactive/{ => result}/view/freemarker/test.ftl (100%) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/package-info.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/package-info.java index 3c4228f0fd..289c322a51 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/package-info.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/package-info.java @@ -1,4 +1,6 @@ /** - * Provides various controller styles for request handling. + * Support for various programming model styles including the invocation of + * different types of handlers (e.g. annotated controllers, simple WebHandler, + * etc) as well as result handling (@ResponseBody, view resolution, etc). */ package org.springframework.web.reactive.result; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/AbstractUrlBasedView.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/AbstractUrlBasedView.java similarity index 97% rename from spring-web-reactive/src/main/java/org/springframework/web/reactive/view/AbstractUrlBasedView.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/AbstractUrlBasedView.java index 50ffac0b3c..6c582bebfd 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/AbstractUrlBasedView.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/AbstractUrlBasedView.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.web.reactive.view; +package org.springframework.web.reactive.result.view; import java.util.Locale; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/AbstractView.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/AbstractView.java similarity index 97% rename from spring-web-reactive/src/main/java/org/springframework/web/reactive/view/AbstractView.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/AbstractView.java index 436bb63fe7..b26fdd0d28 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/AbstractView.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/AbstractView.java @@ -13,10 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.web.reactive.view; +package org.springframework.web.reactive.result.view; import java.util.ArrayList; -import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -35,7 +34,6 @@ import org.springframework.http.MediaType; import org.springframework.ui.ModelMap; import org.springframework.util.Assert; import org.springframework.web.reactive.HandlerResult; -import org.springframework.web.reactive.View; import org.springframework.web.server.ServerWebExchange; /** diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/UrlBasedViewResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/UrlBasedViewResolver.java similarity index 86% rename from spring-web-reactive/src/main/java/org/springframework/web/reactive/view/UrlBasedViewResolver.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/UrlBasedViewResolver.java index e6db4c6f37..651205f385 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/UrlBasedViewResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/UrlBasedViewResolver.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.web.reactive.view; +package org.springframework.web.reactive.result.view; import java.util.Locale; @@ -22,19 +22,17 @@ import reactor.core.publisher.Mono; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.InitializingBean; import org.springframework.util.PatternMatchUtils; -import org.springframework.web.reactive.View; /** - * A {@link org.springframework.web.reactive.ViewResolver ViewResolver} that - * allow direct resolution of symbolic view names to URLs without explicit - * mapping definition. This is useful if symbolic names match the names of view - * resources in a straightforward manner (i.e. the symbolic name is the unique - * part of the resource's filename), without the need for a dedicated mapping - * to be defined for each view. + * A {@link ViewResolver} that allow direct resolution of symbolic view names + * to URLs without explicit mapping definition. This is useful if symbolic names + * match the names of view resources in a straightforward manner (i.e. the + * symbolic name is the unique part of the resource's filename), without the need + * for a dedicated mapping to be defined for each view. * *

    Supports {@link AbstractUrlBasedView} subclasses like - * {@link org.springframework.web.reactive.view.freemarker.FreeMarkerView}. + * {@link org.springframework.web.reactive.result.view.freemarker.FreeMarkerView}. * The view class for all views generated by this resolver can be specified * via the "viewClass" property. * @@ -54,7 +52,7 @@ import org.springframework.web.reactive.View; * a symbolic view name to different resources depending on the current locale. * * @author Rossen Stoyanchev */ -public class UrlBasedViewResolver extends ViewResolverSupport implements InitializingBean { +public class UrlBasedViewResolver extends ViewResolverSupport implements ViewResolver, InitializingBean { private Class viewClass; @@ -125,9 +123,8 @@ public class UrlBasedViewResolver extends ViewResolverSupport implements Initial /** * Set the view names (or name patterns) that can be handled by this - * {@link org.springframework.web.reactive.ViewResolver}. View names can - * contain simple wildcards such that 'my*', '*Report' and '*Repo*' will - * all match the view name 'myReport'. + * {@link ViewResolver}. View names can contain simple wildcards such that + * 'my*', '*Report' and '*Repo*' will all match the view name 'myReport'. * @see #canHandle */ public void setViewNames(String... viewNames) { @@ -136,7 +133,7 @@ public class UrlBasedViewResolver extends ViewResolverSupport implements Initial /** * Return the view names (or name patterns) that can be handled by this - * {@link org.springframework.web.reactive.ViewResolver}. + * {@link ViewResolver}. */ protected String[] getViewNames() { return this.viewNames; @@ -167,8 +164,7 @@ public class UrlBasedViewResolver extends ViewResolverSupport implements Initial } /** - * Indicates whether or not this - * {@link org.springframework.web.reactive.ViewResolver} can handle the + * Indicates whether or not this {@link ViewResolver} can handle the * supplied view name. If not, an empty result is returned. The default * implementation checks against the configured {@link #setViewNames * view names}. diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/View.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/View.java similarity index 95% rename from spring-web-reactive/src/main/java/org/springframework/web/reactive/View.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/View.java index 91fb8c3dd0..b9b82f6faa 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/View.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/View.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.web.reactive; +package org.springframework.web.reactive.result.view; import java.util.List; import java.util.Optional; @@ -22,6 +22,7 @@ import reactor.core.publisher.Flux; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.MediaType; +import org.springframework.web.reactive.HandlerResult; import org.springframework.web.server.ServerWebExchange; /** diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/ViewResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolver.java similarity index 76% rename from spring-web-reactive/src/main/java/org/springframework/web/reactive/ViewResolver.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolver.java index 942f050193..4ba8e38669 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/ViewResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolver.java @@ -1,4 +1,4 @@ -package org.springframework.web.reactive; +package org.springframework.web.reactive.result.view; import java.util.Locale; @@ -10,11 +10,11 @@ import reactor.core.publisher.Mono; * *

    The process of view resolution is driven through a ViewResolver-based * {@code HandlerResultHandler} implementation called - * {@link org.springframework.web.reactive.view.ViewResolverResultHandler + * {@link org.springframework.web.reactive.result.view.ViewResolverResultHandler * ViewResolverResultHandler}. * * @author Rossen Stoyanchev - * @see org.springframework.web.reactive.view.ViewResolverResultHandler + * @see org.springframework.web.reactive.result.view.ViewResolverResultHandler */ public interface ViewResolver { diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/ViewResolverResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolverResultHandler.java similarity index 95% rename from spring-web-reactive/src/main/java/org/springframework/web/reactive/view/ViewResolverResultHandler.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolverResultHandler.java index d4a22dce13..1ea8a1b5c1 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/ViewResolverResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolverResultHandler.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.web.reactive.view; +package org.springframework.web.reactive.result.view; import java.util.ArrayList; import java.util.Collections; @@ -31,8 +31,6 @@ import org.springframework.core.io.buffer.DataBuffer; import org.springframework.util.Assert; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.HandlerResultHandler; -import org.springframework.web.reactive.View; -import org.springframework.web.reactive.ViewResolver; import org.springframework.web.server.ServerWebExchange; @@ -81,8 +79,7 @@ public class ViewResolverResultHandler implements HandlerResultHandler, Ordered return this.order; } - - // TODO: @ModelAttribute return value, declared Object return value (either String or View) + // TODO: Add support for model-related return value (Model, ModelAndView, @ModelAttribute) @Override public boolean supports(HandlerResult result) { diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/ViewResolverSupport.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolverSupport.java similarity index 94% rename from spring-web-reactive/src/main/java/org/springframework/web/reactive/view/ViewResolverSupport.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolverSupport.java index 7bafe058ec..fb4d2a0f72 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/ViewResolverSupport.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolverSupport.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.web.reactive.view; +package org.springframework.web.reactive.result.view; import java.util.ArrayList; import java.util.List; @@ -25,7 +25,6 @@ import org.springframework.core.io.buffer.DataBufferAllocator; import org.springframework.core.io.buffer.DefaultDataBufferAllocator; import org.springframework.http.MediaType; import org.springframework.util.Assert; -import org.springframework.web.reactive.ViewResolver; /** * Base class for {@code ViewResolver} implementations with shared properties. @@ -33,7 +32,7 @@ import org.springframework.web.reactive.ViewResolver; * @author Rossen Stoyanchev * @since 4.3 */ -public abstract class ViewResolverSupport implements ViewResolver, ApplicationContextAware, Ordered { +public abstract class ViewResolverSupport implements ApplicationContextAware, Ordered { public static final MediaType DEFAULT_CONTENT_TYPE = MediaType.parseMediaType("text/html;charset=UTF-8"); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/freemarker/FreeMarkerConfig.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerConfig.java similarity index 95% rename from spring-web-reactive/src/main/java/org/springframework/web/reactive/view/freemarker/FreeMarkerConfig.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerConfig.java index 6502f3e182..79653256ba 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/freemarker/FreeMarkerConfig.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerConfig.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.web.reactive.view.freemarker; +package org.springframework.web.reactive.result.view.freemarker; import freemarker.template.Configuration; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/freemarker/FreeMarkerConfigurer.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerConfigurer.java similarity index 98% rename from spring-web-reactive/src/main/java/org/springframework/web/reactive/view/freemarker/FreeMarkerConfigurer.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerConfigurer.java index 5dc3901ea5..b812910282 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/freemarker/FreeMarkerConfigurer.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerConfigurer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.web.reactive.view.freemarker; +package org.springframework.web.reactive.result.view.freemarker; import java.io.IOException; import java.util.List; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/freemarker/FreeMarkerView.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerView.java similarity index 98% rename from spring-web-reactive/src/main/java/org/springframework/web/reactive/view/freemarker/FreeMarkerView.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerView.java index 9efd05265b..4ed672ece3 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/freemarker/FreeMarkerView.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerView.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.web.reactive.view.freemarker; +package org.springframework.web.reactive.result.view.freemarker; import java.io.FileNotFoundException; import java.io.IOException; @@ -36,7 +36,7 @@ import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.context.ApplicationContextException; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.web.reactive.view.AbstractUrlBasedView; +import org.springframework.web.reactive.result.view.AbstractUrlBasedView; import org.springframework.web.server.ServerWebExchange; /** diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/freemarker/FreeMarkerViewResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerViewResolver.java similarity index 91% rename from spring-web-reactive/src/main/java/org/springframework/web/reactive/view/freemarker/FreeMarkerViewResolver.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerViewResolver.java index 631d6fbcd7..d8f9d8a97d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/freemarker/FreeMarkerViewResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerViewResolver.java @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.web.reactive.view.freemarker; +package org.springframework.web.reactive.result.view.freemarker; -import org.springframework.web.reactive.view.UrlBasedViewResolver; +import org.springframework.web.reactive.result.view.UrlBasedViewResolver; /** * A {@code ViewResolver} for resolving {@link FreeMarkerView} instances, i.e. diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/package-info.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/package-info.java similarity index 54% rename from spring-web-reactive/src/main/java/org/springframework/web/reactive/view/package-info.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/package-info.java index 602c2ea992..c789bb2632 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/view/package-info.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/package-info.java @@ -1,4 +1,4 @@ /** * Support for result handling through view resolution. */ -package org.springframework.web.reactive.view; +package org.springframework.web.reactive.result.view; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java index 5075cdbed7..2be8ca1eb1 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java @@ -75,16 +75,20 @@ import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; import org.springframework.web.reactive.DispatcherHandler; -import org.springframework.web.reactive.ViewResolver; import org.springframework.web.reactive.result.SimpleResultHandler; -import org.springframework.web.reactive.view.ViewResolverResultHandler; -import org.springframework.web.reactive.view.freemarker.FreeMarkerConfigurer; -import org.springframework.web.reactive.view.freemarker.FreeMarkerViewResolver; +import org.springframework.web.reactive.result.view.ViewResolver; +import org.springframework.web.reactive.result.view.ViewResolverResultHandler; +import org.springframework.web.reactive.result.view.freemarker.FreeMarkerConfigurer; +import org.springframework.web.reactive.result.view.freemarker.FreeMarkerViewResolver; import org.springframework.web.server.adapter.WebHttpHandlerBuilder; -import static org.junit.Assert.*; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + /** + * Integration tests with {@code @RequestMapping} methods. * @author Rossen Stoyanchev * @author Sebastien Deleuze * @author Stephane Maldini diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/view/UrlBasedViewResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/UrlBasedViewResolverTests.java similarity index 95% rename from spring-web-reactive/src/test/java/org/springframework/web/reactive/view/UrlBasedViewResolverTests.java rename to spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/UrlBasedViewResolverTests.java index 1595dde4c2..c913427e6e 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/view/UrlBasedViewResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/UrlBasedViewResolverTests.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.web.reactive.view; +package org.springframework.web.reactive.result.view; import java.util.Locale; import java.util.Map; @@ -24,7 +24,6 @@ import reactor.core.publisher.Mono; import org.springframework.context.support.StaticApplicationContext; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.web.reactive.View; import org.springframework.web.server.ServerWebExchange; import static org.junit.Assert.assertNotNull; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/view/ViewResolverResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolverResultHandlerTests.java similarity index 97% rename from spring-web-reactive/src/test/java/org/springframework/web/reactive/view/ViewResolverResultHandlerTests.java rename to spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolverResultHandlerTests.java index 8d6c2f38e3..1208a8e1ae 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/view/ViewResolverResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolverResultHandlerTests.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.web.reactive.view; +package org.springframework.web.reactive.result.view; import java.lang.reflect.Method; import java.net.URI; @@ -50,15 +50,16 @@ import org.springframework.ui.ExtendedModelMap; import org.springframework.ui.ModelMap; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.HandlerResultHandler; -import org.springframework.web.reactive.View; -import org.springframework.web.reactive.ViewResolver; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange; import org.springframework.web.server.session.DefaultWebSessionManager; import org.springframework.web.server.session.WebSessionManager; import static org.hamcrest.CoreMatchers.endsWith; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/view/freemarker/FreeMarkerViewTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerViewTests.java similarity index 95% rename from spring-web-reactive/src/test/java/org/springframework/web/reactive/view/freemarker/FreeMarkerViewTests.java rename to spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerViewTests.java index 7123553d47..7d705a0ac0 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/view/freemarker/FreeMarkerViewTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerViewTests.java @@ -13,12 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.web.reactive.view.freemarker; +package org.springframework.web.reactive.result.view.freemarker; import java.net.URI; import java.nio.ByteBuffer; import java.nio.charset.Charset; -import java.util.HashMap; import java.util.Locale; import java.util.Optional; @@ -39,17 +38,14 @@ import org.springframework.http.server.reactive.MockServerHttpRequest; import org.springframework.http.server.reactive.MockServerHttpResponse; import org.springframework.ui.ExtendedModelMap; import org.springframework.ui.ModelMap; -import org.springframework.web.context.WebApplicationContext; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange; import org.springframework.web.server.session.DefaultWebSessionManager; import org.springframework.web.server.session.WebSessionManager; -import static org.hamcrest.CoreMatchers.containsString; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; /** diff --git a/spring-web-reactive/src/test/resources/org/springframework/web/reactive/view/freemarker/test.ftl b/spring-web-reactive/src/test/resources/org/springframework/web/reactive/result/view/freemarker/test.ftl similarity index 100% rename from spring-web-reactive/src/test/resources/org/springframework/web/reactive/view/freemarker/test.ftl rename to spring-web-reactive/src/test/resources/org/springframework/web/reactive/result/view/freemarker/test.ftl From bc782902d7e6b6abeb8041d620f0a7b7657496b5 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 26 May 2016 16:00:08 -0400 Subject: [PATCH 355/498] Remove java.util.Optional from View contract --- .../web/reactive/result/view/AbstractView.java | 6 +++--- .../org/springframework/web/reactive/result/view/View.java | 2 +- .../web/reactive/result/view/ViewResolverResultHandler.java | 4 ++-- .../result/view/ViewResolverResultHandlerTests.java | 4 +--- .../result/view/freemarker/FreeMarkerViewTests.java | 2 +- 5 files changed, 8 insertions(+), 10 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/AbstractView.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/AbstractView.java index b26fdd0d28..5593926007 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/AbstractView.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/AbstractView.java @@ -114,15 +114,15 @@ public abstract class AbstractView implements View, ApplicationContextAware { * @return */ @Override - public Flux render(HandlerResult result, Optional contentType, + public Flux render(HandlerResult result, MediaType contentType, ServerWebExchange exchange) { if (logger.isTraceEnabled()) { logger.trace("Rendering view with model " + result.getModel()); } - if (contentType.isPresent()) { - exchange.getResponse().getHeaders().setContentType(contentType.get()); + if (contentType != null) { + exchange.getResponse().getHeaders().setContentType(contentType); } Map mergedModel = getModelAttributes(result, exchange); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/View.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/View.java index b9b82f6faa..cfc9866ba5 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/View.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/View.java @@ -55,6 +55,6 @@ public interface View { * @param exchange the current exchange * @return the output stream */ - Flux render(HandlerResult result, Optional contentType, ServerWebExchange exchange); + Flux render(HandlerResult result, MediaType contentType, ServerWebExchange exchange); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolverResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolverResultHandler.java index 1ea8a1b5c1..4b3ed3639a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolverResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolverResultHandler.java @@ -121,7 +121,7 @@ public class ViewResolverResultHandler implements HandlerResultHandler, Ordered return returnValueMono.then(returnValue -> { if (returnValue instanceof View) { - Flux body = ((View) returnValue).render(result, Optional.empty(), exchange); + Flux body = ((View) returnValue).render(result, null, exchange); return exchange.getResponse().setBody(body); } else if (returnValue instanceof CharSequence) { @@ -131,7 +131,7 @@ public class ViewResolverResultHandler implements HandlerResultHandler, Ordered .concatMap(resolver -> resolver.resolveViewName(viewName, locale)) .next() .then(view -> { - Flux body = view.render(result, Optional.empty(), exchange); + Flux body = view.render(result, null, exchange); return exchange.getResponse().setBody(body); }); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolverResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolverResultHandlerTests.java index 1208a8e1ae..23c6a1eaf8 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolverResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolverResultHandlerTests.java @@ -277,9 +277,7 @@ public class ViewResolverResultHandlerTests { } @Override - public Flux render(HandlerResult result, Optional contentType, - ServerWebExchange exchange) { - + public Flux render(HandlerResult result, MediaType mediaType, ServerWebExchange exchange) { String value = this.name + ": " + result.getModel().toString(); assertNotNull(value); return Flux.just(asDataBuffer(value)); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerViewTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerViewTests.java index 7d705a0ac0..bb28907238 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerViewTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerViewTests.java @@ -127,7 +127,7 @@ public class FreeMarkerViewTests { ModelMap model = new ExtendedModelMap(); model.addAttribute("hello", "hi FreeMarker"); HandlerResult result = new HandlerResult(new Object(), "", ResolvableType.NONE, model); - Flux flux = view.render(result, Optional.empty(), this.exchange); + Flux flux = view.render(result, null, this.exchange); TestSubscriber subscriber = new TestSubscriber<>(); subscriber.bindTo(flux).assertValuesWith(dataBuffer -> From 91a5759cf5125954541336a615b9796daaf89626 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 26 May 2016 16:14:18 -0400 Subject: [PATCH 356/498] Polish result handlers default order --- .../web/reactive/result/SimpleResultHandler.java | 7 +++++++ .../result/view/ViewResolverResultHandler.java | 10 +++++++++- .../annotation/RequestMappingIntegrationTests.java | 13 +++---------- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/SimpleResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/SimpleResultHandler.java index b32a4d0f76..1b9c7b1d61 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/SimpleResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/SimpleResultHandler.java @@ -56,6 +56,13 @@ public class SimpleResultHandler implements Ordered, HandlerResultHandler { } + /** + * Set the order for this result handler relative to others. + *

    By default this is set to {@link Ordered#LOWEST_PRECEDENCE} and is + * generally safe to use late in the order since it looks specifically for + * {@code void} or async return types parameterized by {@code void}. + * @param order the order + */ public void setOrder(int order) { this.order = order; } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolverResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolverResultHandler.java index 4b3ed3639a..c858401b23 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolverResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolverResultHandler.java @@ -70,6 +70,14 @@ public class ViewResolverResultHandler implements HandlerResultHandler, Ordered return Collections.unmodifiableList(this.viewResolvers); } + /** + * Set the order for this result handler relative to others. + *

    By default this is set to {@link Ordered#LOWEST_PRECEDENCE} and + * generally needs to be used late in the order since it interprets any + * String return value as a view name while others may interpret the same + * otherwise based on annotations (e.g. for {@code @ResponseBody}). + * @param order the order + */ public void setOrder(int order) { this.order = order; } @@ -79,7 +87,7 @@ public class ViewResolverResultHandler implements HandlerResultHandler, Ordered return this.order; } - // TODO: Add support for model-related return value (Model, ModelAndView, @ModelAttribute) + // TODO: Support for Model, ModelAndView, @ModelAttribute, Object with no method annotations @Override public boolean supports(HandlerResult result) { diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java index 2be8ca1eb1..0e38a43b6b 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java @@ -415,25 +415,18 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati new CodecHttpMessageConverter<>(new ByteBufferEncoder(), new ByteBufferDecoder()), new CodecHttpMessageConverter<>(new StringEncoder(), new StringDecoder()), new CodecHttpMessageConverter<>(new JacksonJsonEncoder(), new JacksonJsonDecoder())); - ResponseBodyResultHandler resultHandler = - new ResponseBodyResultHandler(converters, conversionService()); - resultHandler.setOrder(1); - return resultHandler; + return new ResponseBodyResultHandler(converters, conversionService()); } @Bean public SimpleResultHandler simpleHandlerResultHandler() { - SimpleResultHandler resultHandler = new SimpleResultHandler(conversionService()); - resultHandler.setOrder(2); - return resultHandler; + return new SimpleResultHandler(conversionService()); } @Bean public ViewResolverResultHandler viewResolverResultHandler() { List resolvers = Collections.singletonList(freeMarkerViewResolver()); - ViewResolverResultHandler resultHandler = new ViewResolverResultHandler(resolvers, conversionService()); - resultHandler.setOrder(3); - return resultHandler; + return new ViewResolverResultHandler(resolvers, conversionService()); } @Bean From eb9fe235fe1d4731e1f0e0a0aa6b97270724b061 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 26 May 2016 17:34:52 -0400 Subject: [PATCH 357/498] Properly handle Mono.empty() for view resolution This commit ensures correct handling for Mono.empty() return value where the declared return type is Mono or Mono. --- .../view/ViewResolverResultHandler.java | 61 +++++++++++++------ .../view/ViewResolverResultHandlerTests.java | 28 ++++++++- 2 files changed, 67 insertions(+), 22 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolverResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolverResultHandler.java index c858401b23..51cfc1d5de 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolverResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolverResultHandler.java @@ -25,6 +25,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.core.Ordered; +import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.convert.ConversionService; import org.springframework.core.io.buffer.DataBuffer; @@ -109,25 +110,29 @@ public class ViewResolverResultHandler implements HandlerResultHandler, Ordered @Override public Mono handleResult(ServerWebExchange exchange, HandlerResult result) { - Mono returnValueMono; - if (this.conversionService.canConvert(result.getReturnValueType().getRawClass(), Mono.class)) { - returnValueMono = this.conversionService.convert(result.getReturnValue().get(), Mono.class); - } - else if (result.getReturnValue().isPresent()) { - returnValueMono = Mono.just(result.getReturnValue().get()); - } - else { - Optional viewName = getDefaultViewName(result, exchange); - if (viewName.isPresent()) { - returnValueMono = Mono.just(viewName.get()); + Mono mono; + ResolvableType elementType; + ResolvableType returnType = result.getReturnValueType(); + + if (this.conversionService.canConvert(returnType.getRawClass(), Mono.class)) { + Optional optionalValue = result.getReturnValue(); + if (optionalValue.isPresent()) { + Mono convertedMono = this.conversionService.convert(optionalValue.get(), Mono.class); + mono = convertedMono.map(o -> o); } else { - returnValueMono = Mono.error(new IllegalStateException("Handler [" + result.getHandler() + "] " + - "neither returned a view name nor a View object")); + mono = Mono.empty(); } + elementType = returnType.getGeneric(0); + } + else { + mono = Mono.justOrEmpty(result.getReturnValue()); + elementType = returnType; } - return returnValueMono.then(returnValue -> { + mono = mono.otherwiseIfEmpty(handleMissingReturnValue(exchange, result, elementType)); + + return mono.then(returnValue -> { if (returnValue instanceof View) { Flux body = ((View) returnValue).render(result, null, exchange); return exchange.getResponse().setBody(body); @@ -144,15 +149,33 @@ public class ViewResolverResultHandler implements HandlerResultHandler, Ordered }); } else { - // Should not happen - return Mono.error(new IllegalStateException( - "Unexpected return value: " + returnValue.getClass())); + // Eventually for model-related return values (should not happen now) + return Mono.error(new IllegalStateException("Unexpected return value")); } }); } - protected Optional getDefaultViewName(HandlerResult result, ServerWebExchange exchange) { - return Optional.empty(); + private Mono handleMissingReturnValue(ServerWebExchange exchange, HandlerResult result, + ResolvableType elementType) { + + if (isStringOrViewReference(elementType.getRawClass())) { + String defaultViewName = getDefaultViewName(exchange, result); + if (defaultViewName != null) { + return Mono.just(defaultViewName); + } + else { + return Mono.error(new IllegalStateException("Handler [" + result.getHandler() + "] " + + "neither returned a view name nor a View object")); + } + } + else { + // Eventually for model-related return values (should not happen now) + return Mono.error(new IllegalStateException("Unexpected return value type")); + } + } + + protected String getDefaultViewName(ServerWebExchange exchange, HandlerResult result) { + return null; } } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolverResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolverResultHandlerTests.java index 23c6a1eaf8..1da4de85a9 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolverResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolverResultHandlerTests.java @@ -26,7 +26,6 @@ import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.Optional; import org.junit.Before; import org.junit.Test; @@ -128,7 +127,7 @@ public class ViewResolverResultHandlerTests { TestView view = new TestView("account"); List resolvers = Collections.singletonList(mock(ViewResolver.class)); ViewResolverResultHandler handler = new ViewResolverResultHandler(resolvers, this.conversionService); - handle(handler, Mono.just(view), ResolvableType.forClass(Mono.class)); + handle(handler, Mono.just(view), methodReturnType("handleMonoView")); new TestSubscriber().bindTo(this.response.getBody()) .assertValuesWith(buf -> assertEquals("account: {id=123}", asString(buf))); @@ -153,7 +152,7 @@ public class ViewResolverResultHandlerTests { TestViewResolver resolver = new TestViewResolver().addView(view); List resolvers = Collections.singletonList(resolver); ViewResolverResultHandler handler = new ViewResolverResultHandler(resolvers, this.conversionService); - handle(handler, Mono.just("account"), ResolvableType.forClass(Mono.class)); + handle(handler, Mono.just("account"), methodReturnType("handleMonoString")); new TestSubscriber().bindTo(this.response.getBody()) .assertValuesWith(buf -> assertEquals("account: {id=123}", asString(buf))); @@ -192,6 +191,24 @@ public class ViewResolverResultHandlerTests { assertThat(ex.getMessage(), endsWith("neither returned a view name nor a View object"))); } + @Test + public void viewNameMonoEmpty() throws Exception { + TestView view = new TestView("account"); + TestViewResolver resolver = new TestViewResolver().addView(view); + List resolvers = Collections.singletonList(resolver); + HandlerResultHandler handler = new ViewResolverResultHandler(resolvers, this.conversionService) { + @Override + protected String getDefaultViewName(ServerWebExchange exchange, HandlerResult result) { + return "account"; + } + }; + handle(handler, Mono.empty(), methodReturnType("handleMonoString")); + + new TestSubscriber().bindTo(this.response.getBody()) + .assertValuesWith(buf -> assertEquals("account: {id=123}", asString(buf))); + } + + @Test public void ordered() throws Exception { TestViewResolver resolver1 = new TestViewResolver(); @@ -216,6 +233,11 @@ public class ViewResolverResultHandlerTests { return subscriber.bindTo(mono).await(Duration.ofSeconds(1)); } + private ResolvableType methodReturnType(String methodName, Class... args) throws NoSuchMethodException { + Method method = TestController.class.getDeclaredMethod(methodName, args); + return ResolvableType.forMethodReturnType(method); + } + private static DataBuffer asDataBuffer(String value) { ByteBuffer byteBuffer = ByteBuffer.wrap(value.getBytes(UTF_8)); return new DefaultDataBufferAllocator().wrap(byteBuffer); From 8b99c51969ab8f2354685956fb5724fbc4de72d0 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 26 May 2016 17:37:39 -0400 Subject: [PATCH 358/498] Fix compiler warnings --- .../annotation/RequestHeaderMethodArgumentResolverTests.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolverTests.java index d8dcfbcece..14f93ef363 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolverTests.java @@ -80,7 +80,7 @@ public class RequestHeaderMethodArgumentResolverTests { this.resolver = new RequestHeaderMethodArgumentResolver(conversionService, context.getBeanFactory()); @SuppressWarnings("ConfusingArgumentToVarargsMethod") - Method method = ReflectionUtils.findMethod(getClass(), "params", null); + Method method = ReflectionUtils.findMethod(getClass(), "params", (Class[]) null); this.paramNamedDefaultValueStringHeader = new SynthesizingMethodParameter(method, 0); this.paramNamedValueStringArray = new SynthesizingMethodParameter(method, 1); this.paramSystemProperty = new SynthesizingMethodParameter(method, 2); @@ -190,6 +190,7 @@ public class RequestHeaderMethodArgumentResolverTests { } @Test + @SuppressWarnings("deprecation") public void dateConversion() throws Exception { String rfc1123val = "Thu, 21 Apr 2016 17:11:08 +0100"; this.exchange.getRequest().getHeaders().add("name", rfc1123val); From 793638e610fa01cfffa53e1edcab56c98b2d5751 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 26 May 2016 17:48:35 -0400 Subject: [PATCH 359/498] Rename ViewResolverResultHandler to ViewResolution~ --- .../annotation/ResponseBodyResultHandler.java | 3 ++- ....java => ViewResolutionResultHandler.java} | 21 +++++++++------ .../reactive/result/view/ViewResolver.java | 6 ++--- .../RequestMappingIntegrationTests.java | 6 ++--- ... => ViewResolutionResultHandlerTests.java} | 26 +++++++++---------- 5 files changed, 34 insertions(+), 28 deletions(-) rename spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/{ViewResolverResultHandler.java => ViewResolutionResultHandler.java} (87%) rename spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/{ViewResolverResultHandlerTests.java => ViewResolutionResultHandlerTests.java} (90%) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java index 5cc5fa3960..8a64f6c86c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java @@ -92,7 +92,8 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered * {@code RequestedContentTypeResolver}. * * @param messageConverters converters for writing the response body with - * @param conversionService for converting to Flux and Mono from other reactive types + * @param conversionService for converting other reactive types (e.g. + * rx.Observable, rx.Single, etc.) to Flux or Mono */ public ResponseBodyResultHandler(List> messageConverters, ConversionService conversionService, RequestedContentTypeResolver contentTypeResolver) { diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolverResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java similarity index 87% rename from spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolverResultHandler.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java index 51cfc1d5de..93f11c57dc 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolverResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java @@ -36,17 +36,17 @@ import org.springframework.web.server.ServerWebExchange; /** - * {@code HandlerResultHandler} that resolves a String return value from a - * handler to a {@link View} which is then used to render the response. - * A handler may also return a {@code View} instance and/or async variants that - * provide a String view name or a {@code View}. + * {@code HandlerResultHandler} that performs view resolution by resolving a + * {@link View} instance first and then rendering the response with it. + * If the return value is a String, the configured {@link ViewResolver}s will + * be consulted to resolve that to a {@link View} instance. * - *

    This result handler should be ordered after others that may also interpret - * a String return value for example in combination with {@code @ResponseBody}. + *

    This result handler should be ordered late relative to other result + * handlers. See {@link #setOrder(int)} for more details. * * @author Rossen Stoyanchev */ -public class ViewResolverResultHandler implements HandlerResultHandler, Ordered { +public class ViewResolutionResultHandler implements HandlerResultHandler, Ordered { private final List viewResolvers = new ArrayList<>(4); @@ -55,7 +55,12 @@ public class ViewResolverResultHandler implements HandlerResultHandler, Ordered private int order = Ordered.LOWEST_PRECEDENCE; - public ViewResolverResultHandler(List resolvers, ConversionService service) { + /** + * Constructor with {@code ViewResolver}s tand a {@code ConversionService}. + * @param resolvers the resolver to use + * @param service for converting other reactive types (e.g. rx.Single) to Mono + */ + public ViewResolutionResultHandler(List resolvers, ConversionService service) { Assert.notEmpty(resolvers, "At least one ViewResolver is required."); Assert.notNull(service, "'conversionService' is required."); this.viewResolvers.addAll(resolvers); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolver.java index 4ba8e38669..f275f2bf42 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolver.java @@ -10,11 +10,11 @@ import reactor.core.publisher.Mono; * *

    The process of view resolution is driven through a ViewResolver-based * {@code HandlerResultHandler} implementation called - * {@link org.springframework.web.reactive.result.view.ViewResolverResultHandler - * ViewResolverResultHandler}. + * {@link ViewResolutionResultHandler + * ViewResolutionResultHandler}. * * @author Rossen Stoyanchev - * @see org.springframework.web.reactive.result.view.ViewResolverResultHandler + * @see ViewResolutionResultHandler */ public interface ViewResolver { diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java index 0e38a43b6b..c866052e9d 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java @@ -76,8 +76,8 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; import org.springframework.web.reactive.DispatcherHandler; import org.springframework.web.reactive.result.SimpleResultHandler; +import org.springframework.web.reactive.result.view.ViewResolutionResultHandler; import org.springframework.web.reactive.result.view.ViewResolver; -import org.springframework.web.reactive.result.view.ViewResolverResultHandler; import org.springframework.web.reactive.result.view.freemarker.FreeMarkerConfigurer; import org.springframework.web.reactive.result.view.freemarker.FreeMarkerViewResolver; import org.springframework.web.server.adapter.WebHttpHandlerBuilder; @@ -424,9 +424,9 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati } @Bean - public ViewResolverResultHandler viewResolverResultHandler() { + public ViewResolutionResultHandler viewResolverResultHandler() { List resolvers = Collections.singletonList(freeMarkerViewResolver()); - return new ViewResolverResultHandler(resolvers, conversionService()); + return new ViewResolutionResultHandler(resolvers, conversionService()); } @Bean diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolverResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java similarity index 90% rename from spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolverResultHandlerTests.java rename to spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java index 1da4de85a9..ce8373647f 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolverResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java @@ -63,10 +63,10 @@ import static org.mockito.Mockito.mock; /** - * Unit tests for {@link ViewResolverResultHandler}. + * Unit tests for {@link ViewResolutionResultHandler}. * @author Rossen Stoyanchev */ -public class ViewResolverResultHandlerTests { +public class ViewResolutionResultHandlerTests { private static final Charset UTF_8 = Charset.forName("UTF-8"); @@ -107,7 +107,7 @@ public class ViewResolverResultHandlerTests { ResolvableType returnType = ResolvableType.forMethodParameter(method, -1); HandlerResult result = new HandlerResult(new Object(), returnValue, returnType, this.model); List resolvers = Collections.singletonList(mock(ViewResolver.class)); - ViewResolverResultHandler handler = new ViewResolverResultHandler(resolvers, this.conversionService); + ViewResolutionResultHandler handler = new ViewResolutionResultHandler(resolvers, this.conversionService); assertTrue(handler.supports(result)); } @@ -115,7 +115,7 @@ public class ViewResolverResultHandlerTests { public void viewReference() throws Exception { TestView view = new TestView("account"); List resolvers = Collections.singletonList(mock(ViewResolver.class)); - ViewResolverResultHandler handler = new ViewResolverResultHandler(resolvers, this.conversionService); + ViewResolutionResultHandler handler = new ViewResolutionResultHandler(resolvers, this.conversionService); handle(handler, view, ResolvableType.forClass(View.class)); new TestSubscriber().bindTo(this.response.getBody()) @@ -126,7 +126,7 @@ public class ViewResolverResultHandlerTests { public void viewReferenceMono() throws Exception { TestView view = new TestView("account"); List resolvers = Collections.singletonList(mock(ViewResolver.class)); - ViewResolverResultHandler handler = new ViewResolverResultHandler(resolvers, this.conversionService); + ViewResolutionResultHandler handler = new ViewResolutionResultHandler(resolvers, this.conversionService); handle(handler, Mono.just(view), methodReturnType("handleMonoView")); new TestSubscriber().bindTo(this.response.getBody()) @@ -138,7 +138,7 @@ public class ViewResolverResultHandlerTests { TestView view = new TestView("account"); TestViewResolver resolver = new TestViewResolver().addView(view); List resolvers = Collections.singletonList(resolver); - ViewResolverResultHandler handler = new ViewResolverResultHandler(resolvers, this.conversionService); + ViewResolutionResultHandler handler = new ViewResolutionResultHandler(resolvers, this.conversionService); handle(handler, "account", ResolvableType.forClass(String.class)); TestSubscriber subscriber = new TestSubscriber<>(); @@ -151,7 +151,7 @@ public class ViewResolverResultHandlerTests { TestView view = new TestView("account"); TestViewResolver resolver = new TestViewResolver().addView(view); List resolvers = Collections.singletonList(resolver); - ViewResolverResultHandler handler = new ViewResolverResultHandler(resolvers, this.conversionService); + ViewResolutionResultHandler handler = new ViewResolutionResultHandler(resolvers, this.conversionService); handle(handler, Mono.just("account"), methodReturnType("handleMonoString")); new TestSubscriber().bindTo(this.response.getBody()) @@ -165,7 +165,7 @@ public class ViewResolverResultHandlerTests { TestViewResolver resolver1 = new TestViewResolver().addView(view1); TestViewResolver resolver2 = new TestViewResolver().addView(view2); List resolvers = Arrays.asList(resolver1, resolver2); - ViewResolverResultHandler handler = new ViewResolverResultHandler(resolvers, this.conversionService); + ViewResolutionResultHandler handler = new ViewResolutionResultHandler(resolvers, this.conversionService); handle(handler, "profile", ResolvableType.forClass(String.class)); new TestSubscriber().bindTo(this.response.getBody()) @@ -175,7 +175,7 @@ public class ViewResolverResultHandlerTests { @Test public void viewNameWithNoMatch() throws Exception { List resolvers = Collections.singletonList(mock(ViewResolver.class)); - ViewResolverResultHandler handler = new ViewResolverResultHandler(resolvers, this.conversionService); + ViewResolutionResultHandler handler = new ViewResolutionResultHandler(resolvers, this.conversionService); TestSubscriber subscriber = handle(handler, "account", ResolvableType.forClass(String.class)); subscriber.assertNoValues(); @@ -184,7 +184,7 @@ public class ViewResolverResultHandlerTests { @Test public void viewNameNotSpecified() throws Exception { List resolvers = Collections.singletonList(mock(ViewResolver.class)); - ViewResolverResultHandler handler = new ViewResolverResultHandler(resolvers, this.conversionService); + ViewResolutionResultHandler handler = new ViewResolutionResultHandler(resolvers, this.conversionService); TestSubscriber subscriber = handle(handler, null, ResolvableType.forClass(String.class)); subscriber.assertErrorWith(ex -> @@ -196,7 +196,7 @@ public class ViewResolverResultHandlerTests { TestView view = new TestView("account"); TestViewResolver resolver = new TestViewResolver().addView(view); List resolvers = Collections.singletonList(resolver); - HandlerResultHandler handler = new ViewResolverResultHandler(resolvers, this.conversionService) { + HandlerResultHandler handler = new ViewResolutionResultHandler(resolvers, this.conversionService) { @Override protected String getDefaultViewName(ServerWebExchange exchange, HandlerResult result) { return "account"; @@ -218,8 +218,8 @@ public class ViewResolverResultHandlerTests { resolver1.setOrder(2); resolver2.setOrder(1); - ViewResolverResultHandler resultHandler = - new ViewResolverResultHandler(resolvers, this.conversionService); + ViewResolutionResultHandler resultHandler = + new ViewResolutionResultHandler(resolvers, this.conversionService); assertEquals(Arrays.asList(resolver2, resolver1), resultHandler.getViewResolvers()); } From f0c8c4e7a44f75cfed97a22fdec95df590204026 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 26 May 2016 18:08:18 -0400 Subject: [PATCH 360/498] Add request path based default view name support --- .../view/ViewResolutionResultHandler.java | 13 +++- .../ViewResolutionResultHandlerTests.java | 63 +++++++++++-------- 2 files changed, 50 insertions(+), 26 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java index 93f11c57dc..aee5abb6e1 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java @@ -30,9 +30,11 @@ import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.convert.ConversionService; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.HandlerResultHandler; import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.util.HttpRequestPathHelper; /** @@ -54,6 +56,8 @@ public class ViewResolutionResultHandler implements HandlerResultHandler, Ordere private int order = Ordered.LOWEST_PRECEDENCE; + private final HttpRequestPathHelper pathHelper = new HttpRequestPathHelper(); + /** * Constructor with {@code ViewResolver}s tand a {@code ConversionService}. @@ -180,7 +184,14 @@ public class ViewResolutionResultHandler implements HandlerResultHandler, Ordere } protected String getDefaultViewName(ServerWebExchange exchange, HandlerResult result) { - return null; + String path = this.pathHelper.getLookupPathForRequest(exchange); + if (path.startsWith("/")) { + path = path.substring(1); + } + if (path.endsWith("/")) { + path = path.substring(0, path.length() - 1); + } + return StringUtils.stripFilenameExtension(path); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java index ce8373647f..04f00bdd4c 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java @@ -17,6 +17,7 @@ package org.springframework.web.reactive.result.view; import java.lang.reflect.Method; import java.net.URI; +import java.net.URISyntaxException; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.time.Duration; @@ -54,10 +55,8 @@ import org.springframework.web.server.adapter.DefaultServerWebExchange; import org.springframework.web.server.session.DefaultWebSessionManager; import org.springframework.web.server.session.WebSessionManager; -import static org.hamcrest.CoreMatchers.endsWith; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; @@ -82,10 +81,7 @@ public class ViewResolutionResultHandlerTests { @Before public void setUp() throws Exception { - ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/path")); - this.response = new MockServerHttpResponse(); - WebSessionManager sessionManager = new DefaultWebSessionManager(); - this.exchange = new DefaultServerWebExchange(request, this.response, sessionManager); + this.exchange = createExchange("/path"); this.model = new ExtendedModelMap().addAttribute("id", "123"); this.conversionService = new DefaultConversionService(); this.conversionService.addConverter(new ReactiveStreamsToRxJava1Converter()); @@ -116,7 +112,7 @@ public class ViewResolutionResultHandlerTests { TestView view = new TestView("account"); List resolvers = Collections.singletonList(mock(ViewResolver.class)); ViewResolutionResultHandler handler = new ViewResolutionResultHandler(resolvers, this.conversionService); - handle(handler, view, ResolvableType.forClass(View.class)); + handle(this.exchange, handler, view, ResolvableType.forClass(View.class)); new TestSubscriber().bindTo(this.response.getBody()) .assertValuesWith(buf -> assertEquals("account: {id=123}", asString(buf))); @@ -127,7 +123,7 @@ public class ViewResolutionResultHandlerTests { TestView view = new TestView("account"); List resolvers = Collections.singletonList(mock(ViewResolver.class)); ViewResolutionResultHandler handler = new ViewResolutionResultHandler(resolvers, this.conversionService); - handle(handler, Mono.just(view), methodReturnType("handleMonoView")); + handle(this.exchange, handler, Mono.just(view), methodReturnType("handleMonoView")); new TestSubscriber().bindTo(this.response.getBody()) .assertValuesWith(buf -> assertEquals("account: {id=123}", asString(buf))); @@ -139,7 +135,7 @@ public class ViewResolutionResultHandlerTests { TestViewResolver resolver = new TestViewResolver().addView(view); List resolvers = Collections.singletonList(resolver); ViewResolutionResultHandler handler = new ViewResolutionResultHandler(resolvers, this.conversionService); - handle(handler, "account", ResolvableType.forClass(String.class)); + handle(this.exchange, handler, "account", ResolvableType.forClass(String.class)); TestSubscriber subscriber = new TestSubscriber<>(); subscriber.bindTo(this.response.getBody()) @@ -152,7 +148,7 @@ public class ViewResolutionResultHandlerTests { TestViewResolver resolver = new TestViewResolver().addView(view); List resolvers = Collections.singletonList(resolver); ViewResolutionResultHandler handler = new ViewResolutionResultHandler(resolvers, this.conversionService); - handle(handler, Mono.just("account"), methodReturnType("handleMonoString")); + handle(this.exchange, handler, Mono.just("account"), methodReturnType("handleMonoString")); new TestSubscriber().bindTo(this.response.getBody()) .assertValuesWith(buf -> assertEquals("account: {id=123}", asString(buf))); @@ -166,7 +162,7 @@ public class ViewResolutionResultHandlerTests { TestViewResolver resolver2 = new TestViewResolver().addView(view2); List resolvers = Arrays.asList(resolver1, resolver2); ViewResolutionResultHandler handler = new ViewResolutionResultHandler(resolvers, this.conversionService); - handle(handler, "profile", ResolvableType.forClass(String.class)); + handle(this.exchange, handler, "profile", ResolvableType.forClass(String.class)); new TestSubscriber().bindTo(this.response.getBody()) .assertValuesWith(buf -> assertEquals("profile: {id=123}", asString(buf))); @@ -176,19 +172,32 @@ public class ViewResolutionResultHandlerTests { public void viewNameWithNoMatch() throws Exception { List resolvers = Collections.singletonList(mock(ViewResolver.class)); ViewResolutionResultHandler handler = new ViewResolutionResultHandler(resolvers, this.conversionService); - TestSubscriber subscriber = handle(handler, "account", ResolvableType.forClass(String.class)); + TestSubscriber subscriber = handle(this.exchange, handler, "account", ResolvableType.forClass(String.class)); subscriber.assertNoValues(); } @Test public void viewNameNotSpecified() throws Exception { - List resolvers = Collections.singletonList(mock(ViewResolver.class)); + TestView view = new TestView("account"); + TestViewResolver resolver = new TestViewResolver().addView(view); + List resolvers = Collections.singletonList(resolver); ViewResolutionResultHandler handler = new ViewResolutionResultHandler(resolvers, this.conversionService); - TestSubscriber subscriber = handle(handler, null, ResolvableType.forClass(String.class)); - subscriber.assertErrorWith(ex -> - assertThat(ex.getMessage(), endsWith("neither returned a view name nor a View object"))); + ServerWebExchange exchange = createExchange("/account"); + handle(exchange, handler, null, ResolvableType.forClass(String.class)); + new TestSubscriber().bindTo(this.response.getBody()) + .assertValuesWith(buf -> assertEquals("account: {id=123}", asString(buf))); + + exchange = createExchange("/account/"); + handle(exchange, handler, null, ResolvableType.forClass(String.class)); + new TestSubscriber().bindTo(this.response.getBody()) + .assertValuesWith(buf -> assertEquals("account: {id=123}", asString(buf))); + + exchange = createExchange("/account.123"); + handle(exchange, handler, null, ResolvableType.forClass(String.class)); + new TestSubscriber().bindTo(this.response.getBody()) + .assertValuesWith(buf -> assertEquals("account: {id=123}", asString(buf))); } @Test @@ -196,13 +205,9 @@ public class ViewResolutionResultHandlerTests { TestView view = new TestView("account"); TestViewResolver resolver = new TestViewResolver().addView(view); List resolvers = Collections.singletonList(resolver); - HandlerResultHandler handler = new ViewResolutionResultHandler(resolvers, this.conversionService) { - @Override - protected String getDefaultViewName(ServerWebExchange exchange, HandlerResult result) { - return "account"; - } - }; - handle(handler, Mono.empty(), methodReturnType("handleMonoString")); + HandlerResultHandler handler = new ViewResolutionResultHandler(resolvers, this.conversionService); + ServerWebExchange exchange = createExchange("/account"); + handle(exchange, handler, Mono.empty(), methodReturnType("handleMonoString")); new TestSubscriber().bindTo(this.response.getBody()) .assertValuesWith(buf -> assertEquals("account: {id=123}", asString(buf))); @@ -225,10 +230,18 @@ public class ViewResolutionResultHandlerTests { } + private ServerWebExchange createExchange(String path) throws URISyntaxException { + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI(path)); + this.response = new MockServerHttpResponse(); + WebSessionManager sessionManager = new DefaultWebSessionManager(); + return new DefaultServerWebExchange(request, this.response, sessionManager); + } + + private TestSubscriber handle(ServerWebExchange exchange, HandlerResultHandler handler, + Object value, ResolvableType type) { - private TestSubscriber handle(HandlerResultHandler handler, Object value, ResolvableType type) { HandlerResult result = new HandlerResult(new Object(), value, type, this.model); - Mono mono = handler.handleResult(this.exchange, result); + Mono mono = handler.handleResult(exchange, result); TestSubscriber subscriber = new TestSubscriber<>(); return subscriber.bindTo(mono).await(Duration.ofSeconds(1)); } From 74abe9280447b41117cf423fba938cfff437c455 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Fri, 27 May 2016 09:06:48 +0200 Subject: [PATCH 361/498] Added DataBuffer indexOf and lastIndexOf This commit introduces DataBuffer.indexOf(IntPredicate) and lastIndexOf(IntPredicate), for finding the index of a byte in a DataBuffer. It also introduces DataBufferUtils.tokenize, which tokenizes a DataBuffer into separate tokens, given a delimiter function. --- .../core/io/buffer/DataBuffer.java | 34 ++++- .../core/io/buffer/DefaultDataBuffer.java | 73 +++++++-- .../core/io/buffer/NettyDataBuffer.java | 21 ++- .../io/buffer/support/DataBufferUtils.java | 138 ++++++++++-------- .../core/io/buffer/DataBufferTests.java | 61 +++++++- .../buffer/support/DataBufferUtilsTests.java | 23 ++- 6 files changed, 266 insertions(+), 84 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DataBuffer.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DataBuffer.java index cf4770adee..31cf7f63ac 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DataBuffer.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DataBuffer.java @@ -19,6 +19,7 @@ package org.springframework.core.io.buffer; import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; +import java.util.function.IntPredicate; /** * Basic abstraction over byte buffers. @@ -34,12 +35,22 @@ public interface DataBuffer { DataBufferAllocator allocator(); /** - * Gets the byte at the specified index. - * @param index the index - * @return the byte at the specified index - * @throws IndexOutOfBoundsException if the given index is out of bounds + * Returns the index of the first byte in this buffer that matches the given + * predicate. + * @param predicate the predicate to match + * @return the index of the first byte that matches {@code predicate}; or {@code -1} + * if none match */ - byte get(int index); + int indexOf(IntPredicate predicate); + + /** + * Returns the index of the last byte in this buffer that matches the given + * predicate. + * @param predicate the predicate to match + * @return the index of the last byte that matches {@code predicate}; or {@code -1} + * if none match + */ + int lastIndexOf(IntPredicate predicate); /** * Returns the number of bytes that can be read from this data buffer. @@ -113,11 +124,22 @@ public interface DataBuffer { */ DataBuffer write(ByteBuffer... buffers); + /** + * Creates a new {@code DataBuffer} whose contents is a shared subsequence of this + * data buffer's content. Data between this data buffer and the returned buffer is + * shared; though changes in the returned buffer's position will not be reflected + * in the reading nor writing position of this data buffer. + * @param index the index at which to start the slice + * @param length the length of the slice + * @return the specified slice of this data buffer + */ + DataBuffer slice(int index, int length); + /** * Exposes this buffer's bytes as a {@link ByteBuffer}. Data between this {@code * DataBuffer} and the returned {@code ByteBuffer} is shared; though changes in the * returned buffer's {@linkplain ByteBuffer#position() position} will not be reflected - * in the position(s) of this data buffer. + * in the reading nor writing position of this data buffer. * @return this data buffer as a byte buffer */ ByteBuffer asByteBuffer(); diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java index 823f644eec..940c0d1a8b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java @@ -22,6 +22,7 @@ import java.io.OutputStream; import java.nio.ByteBuffer; import java.util.Arrays; import java.util.function.Function; +import java.util.function.IntPredicate; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -82,8 +83,25 @@ public class DefaultDataBuffer implements DataBuffer { } @Override - public byte get(int index) { - return this.byteBuffer.get(index); + public int indexOf(IntPredicate predicate) { + for (int i = 0; i < readableByteCount(); i++) { + byte b = this.byteBuffer.get(i); + if (predicate.test(b)) { + return i; + } + } + return -1; + } + + @Override + public int lastIndexOf(IntPredicate predicate) { + for (int i = readableByteCount() - 1; i >= 0; i--) { + byte b = this.byteBuffer.get(i); + if (predicate.test(b)) { + return i; + } + } + return -1; } @Override @@ -120,14 +138,16 @@ public class DefaultDataBuffer implements DataBuffer { */ private T readInternal(Function function) { this.byteBuffer.position(this.readPosition); - T result = function.apply(this.byteBuffer); - this.readPosition = this.byteBuffer.position(); - return result; + try { + return function.apply(this.byteBuffer); + } + finally { + this.readPosition = this.byteBuffer.position(); + } } @Override public DefaultDataBuffer write(byte b) { - ensureExtraCapacity(1); writeInternal(buffer -> buffer.put(b)); @@ -184,9 +204,27 @@ public class DefaultDataBuffer implements DataBuffer { */ private T writeInternal(Function function) { this.byteBuffer.position(this.writePosition); - T result = function.apply(this.byteBuffer); - this.writePosition = this.byteBuffer.position(); - return result; + try { + return function.apply(this.byteBuffer); + } + finally { + this.writePosition = this.byteBuffer.position(); + } + } + + @Override + public DataBuffer slice(int index, int length) { + int oldPosition = this.byteBuffer.position(); + try { + this.byteBuffer.position(index); + ByteBuffer slice = this.byteBuffer.slice(); + slice.limit(length); + return new SlicedDefaultDataBuffer(slice, 0, length, this.allocator); + } + finally { + this.byteBuffer.position(oldPosition); + } + } @Override @@ -214,7 +252,7 @@ public class DefaultDataBuffer implements DataBuffer { } } - private void grow(int minCapacity) { + void grow(int minCapacity) { ByteBuffer oldBuffer = this.byteBuffer; ByteBuffer newBuffer = (oldBuffer.isDirect() ? ByteBuffer.allocateDirect(minCapacity) : @@ -227,6 +265,7 @@ public class DefaultDataBuffer implements DataBuffer { oldBuffer.clear(); } + @Override public int hashCode() { return this.byteBuffer.hashCode(); @@ -294,4 +333,18 @@ public class DefaultDataBuffer implements DataBuffer { writeInternal(buffer -> buffer.put(bytes, off, len)); } } + + private static class SlicedDefaultDataBuffer extends DefaultDataBuffer { + + SlicedDefaultDataBuffer(ByteBuffer byteBuffer, int readPosition, + int writePosition, DefaultDataBufferAllocator allocator) { + super(byteBuffer, readPosition, writePosition, allocator); + } + + @Override + void grow(int minCapacity) { + throw new UnsupportedOperationException( + "Growing the capacity of a sliced buffer is not supported"); + } + } } diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/NettyDataBuffer.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/NettyDataBuffer.java index 7078142d6e..c5aa526c04 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/NettyDataBuffer.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/NettyDataBuffer.java @@ -20,6 +20,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; import java.util.Arrays; +import java.util.function.IntPredicate; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufInputStream; @@ -68,8 +69,15 @@ public class NettyDataBuffer implements PooledDataBuffer { } @Override - public byte get(int index) { - return this.byteBuf.getByte(index); + public int indexOf(IntPredicate predicate) { + IntPredicate negated = predicate.negate(); + return this.byteBuf.forEachByte(negated::test); + } + + @Override + public int lastIndexOf(IntPredicate predicate) { + IntPredicate negated = predicate.negate(); + return this.byteBuf.forEachByteDesc(negated::test); } @Override @@ -166,6 +174,12 @@ public class NettyDataBuffer implements PooledDataBuffer { return this; } + @Override + public DataBuffer slice(int index, int length) { + ByteBuf slice = this.byteBuf.slice(index, length); + return new NettyDataBuffer(slice, this.allocator); + } + @Override public ByteBuffer asByteBuffer() { return this.byteBuf.nioBuffer(); @@ -183,8 +197,7 @@ public class NettyDataBuffer implements PooledDataBuffer { @Override public PooledDataBuffer retain() { - this.byteBuf.retain(); - return this; + return new NettyDataBuffer(this.byteBuf.retain(), allocator); } @Override diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java index 85b475e107..5c264a6dad 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java @@ -21,15 +21,16 @@ import java.io.InputStream; import java.nio.ByteBuffer; import java.nio.channels.Channels; import java.nio.channels.ReadableByteChannel; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.concurrent.atomic.AtomicLong; import java.util.function.BiFunction; import java.util.function.Consumer; +import java.util.function.IntPredicate; import org.reactivestreams.Publisher; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; import reactor.core.publisher.Flux; -import reactor.core.publisher.FluxSource; import reactor.core.subscriber.SignalEmitter; import org.springframework.core.io.buffer.DataBuffer; @@ -102,7 +103,79 @@ public abstract class DataBufferUtils { Assert.notNull(publisher, "'publisher' must not be null"); Assert.isTrue(maxByteCount >= 0, "'maxByteCount' must be a positive number"); - return new TakeByteUntilCount(publisher, maxByteCount); + AtomicLong byteCountDown = new AtomicLong(maxByteCount); + + return Flux.from(publisher). + takeWhile(dataBuffer -> { + int delta = -dataBuffer.readableByteCount(); + long currentCount = byteCountDown.getAndAdd(delta); + return currentCount >= 0; + }). + map(dataBuffer -> { + long currentCount = byteCountDown.get(); + if (currentCount >= 0) { + return dataBuffer; + } + else { + // last buffer + int size = (int) (currentCount + dataBuffer.readableByteCount()); + return dataBuffer.slice(0, size); + } + }); + } + + /** + * Tokenize the {@link DataBuffer} using the given delimiter + * function. Does not include the delimiter in the result. + * @param dataBuffer the data buffer to tokenize + * @param delimiter the delimiter function + * @return the tokens + */ + public static List tokenize(DataBuffer dataBuffer, + IntPredicate delimiter) { + Assert.notNull(dataBuffer, "'dataBuffer' must not be null"); + Assert.notNull(delimiter, "'delimiter' must not be null"); + + List results = new ArrayList(); + int idx; + do { + idx = dataBuffer.indexOf(delimiter); + if (idx < 0) { + results.add(dataBuffer); + } + else { + if (idx > 0) { + DataBuffer slice = dataBuffer.slice(0, idx); + slice = retain(slice); + results.add(slice); + } + int remainingLen = dataBuffer.readableByteCount() - (idx + 1); + if (remainingLen > 0) { + dataBuffer = dataBuffer.slice(idx + 1, remainingLen); + } + else { + release(dataBuffer); + idx = -1; + } + } + } + while (idx != -1); + return Collections.unmodifiableList(results); + } + + /** + * Retains the given data buffer, it it is a {@link PooledDataBuffer}. + * @param dataBuffer the data buffer to retain + * @return the retained buffer + */ + @SuppressWarnings("unchecked") + public static T retain(T dataBuffer) { + if (dataBuffer instanceof PooledDataBuffer) { + return (T) ((PooledDataBuffer) dataBuffer).retain(); + } + else { + return dataBuffer; + } } /** @@ -117,63 +190,6 @@ public abstract class DataBufferUtils { return false; } - private static final class TakeByteUntilCount extends FluxSource { - - final long maxByteCount; - - TakeByteUntilCount(Publisher source, long maxByteCount) { - super(source); - this.maxByteCount = maxByteCount; - } - - @Override - public void subscribe(Subscriber subscriber) { - source.subscribe(new Subscriber() { - - private Subscription subscription; - - private final AtomicLong byteCount = new AtomicLong(); - - @Override - public void onSubscribe(Subscription s) { - this.subscription = s; - subscriber.onSubscribe(s); - } - - @Override - public void onNext(DataBuffer dataBuffer) { - int delta = dataBuffer.readableByteCount(); - long currentCount = this.byteCount.addAndGet(delta); - if (currentCount > maxByteCount) { - int size = (int) (maxByteCount - currentCount + delta); - ByteBuffer byteBuffer = - (ByteBuffer) dataBuffer.asByteBuffer().limit(size); - DataBuffer partialBuffer = - dataBuffer.allocator().allocateBuffer(size); - partialBuffer.write(byteBuffer); - - subscriber.onNext(partialBuffer); - subscriber.onComplete(); - this.subscription.cancel(); - } - else { - subscriber.onNext(dataBuffer); - } - } - - @Override - public void onError(Throwable t) { - subscriber.onError(t); - } - - @Override - public void onComplete() { - subscriber.onComplete(); - } - }); - } - } - private static class ReadableByteChannelGenerator implements BiFunction, ReadableByteChannel> { diff --git a/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/DataBufferTests.java b/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/DataBufferTests.java index cf9806f098..4014fd629a 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/DataBufferTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/DataBufferTests.java @@ -24,8 +24,7 @@ import java.util.Arrays; import org.junit.Test; -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; +import static org.junit.Assert.*; /** * @author Arjen Poutsma @@ -184,7 +183,65 @@ public class DataBufferTests extends AbstractDataBufferAllocatingTestCase { assertArrayEquals(new byte[]{'b', 'c'}, resultBytes); release(buffer); + } + @Test + public void indexOf() { + DataBuffer buffer = createDataBuffer(3); + buffer.write(new byte[]{'a', 'b', 'c'}); + + int result = buffer.indexOf(b -> b == 'c'); + assertEquals(2, result); + + result = buffer.indexOf(b -> b == 'z'); + assertEquals(-1, result); + + release(buffer); + } + + @Test + public void lastIndexOf() { + DataBuffer buffer = createDataBuffer(3); + buffer.write(new byte[]{'a', 'b', 'c'}); + + int result = buffer.lastIndexOf(b -> b == 'b'); + assertEquals(1, result); + + result = buffer.lastIndexOf(b -> b == 'z'); + assertEquals(-1, result); + + release(buffer); + } + + @Test + public void slice() { + DataBuffer buffer = createDataBuffer(3); + buffer.write(new byte[]{'a', 'b'}); + + DataBuffer slice = buffer.slice(1, 2); + assertEquals(2, slice.readableByteCount()); + try { + slice.write((byte) 0); + fail("IndexOutOfBoundsException expected"); + } + catch (Exception ignored) { + } + buffer.write((byte) 'c'); + + assertEquals(3, buffer.readableByteCount()); + byte[] result = new byte[3]; + buffer.read(result); + + assertArrayEquals(new byte[]{'a', 'b', 'c'}, result); + + assertEquals(2, slice.readableByteCount()); + result = new byte[2]; + slice.read(result); + + assertArrayEquals(new byte[]{'b', 'c'}, result); + + + release(buffer); } diff --git a/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/support/DataBufferUtilsTests.java b/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/support/DataBufferUtilsTests.java index f4971bc5c0..e00aa4125b 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/support/DataBufferUtilsTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/support/DataBufferUtilsTests.java @@ -19,8 +19,10 @@ package org.springframework.core.io.buffer.support; import java.io.InputStream; import java.net.URI; import java.nio.channels.FileChannel; +import java.nio.charset.StandardCharsets; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; +import java.util.List; import org.junit.Test; import reactor.core.publisher.Flux; @@ -29,6 +31,7 @@ import reactor.core.test.TestSubscriber; import org.springframework.core.io.buffer.AbstractDataBufferAllocatingTestCase; import org.springframework.core.io.buffer.DataBuffer; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; /** @@ -103,7 +106,25 @@ public class DataBufferUtilsTests extends AbstractDataBufferAllocatingTestCase { assertComplete(). assertValuesWith(stringConsumer("foo"), stringConsumer("ba")); - release(bar, baz); + release(baz); + } + + @Test + public void tokenize() { + DataBuffer dataBuffer = stringBuffer("-foo--bar-"); + + List results = DataBufferUtils.tokenize(dataBuffer, b -> b == '-'); + assertEquals(2, results.size()); + + DataBuffer result = results.get(0); + String value = DataBufferTestUtils.dumpString(result, StandardCharsets.UTF_8); + assertEquals("foo", value); + + result = results.get(1); + value = DataBufferTestUtils.dumpString(result, StandardCharsets.UTF_8); + assertEquals("bar", value); + + results.stream().forEach(b -> release(b)); } From 6f461647274eb58781c4434d59668e111d0fbf6c Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Fri, 27 May 2016 09:19:42 +0200 Subject: [PATCH 362/498] Changed reduceToSingleBuffer to splitOnNewline In order to be more "reactive", changed StringDecoder's default from merging all buffers in the stream to a single buffer into splitting the buffers along newline (\r, \n) characters. --- .../core/codec/support/StringDecoder.java | 34 +++++++------- .../core/codec/support/XmlEventDecoder.java | 8 +++- .../codec/support/StringDecoderTests.java | 46 +++++++------------ .../codec/support/StringEncoderTests.java | 8 ++-- 4 files changed, 45 insertions(+), 51 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java index f27ff8005e..ca477cde85 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java @@ -19,6 +19,7 @@ package org.springframework.core.codec.support; import java.nio.CharBuffer; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.util.List; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -32,11 +33,9 @@ import org.springframework.util.MimeTypeUtils; /** * Decode from a bytes stream to a String stream. * - *

    By default, this decoder will buffer the received elements into a single - * {@code ByteBuffer} and will emit a single {@code String} once the stream of - * elements is complete. This behavior can be turned off using an constructor - * argument but the {@code Subcriber} should pay attention to split characters - * issues. + *

    By default, this decoder will split the received {@link DataBuffer}s along newline + * characters ({@code \r\n}), but this can be changed by passing {@code false} as + * constructor argument. * * @author Sebastien Deleuze * @author Brian Clozel @@ -48,13 +47,12 @@ public class StringDecoder extends AbstractDecoder { public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; - private final boolean reduceToSingleBuffer; + private final boolean splitOnNewline; /** * Create a {@code StringDecoder} that decodes a bytes stream to a String stream * - *

    By default, this decoder will buffer bytes and - * emit a single String as a result. + *

    By default, this decoder will split along new lines. */ public StringDecoder() { this(true); @@ -63,12 +61,12 @@ public class StringDecoder extends AbstractDecoder { /** * Create a {@code StringDecoder} that decodes a bytes stream to a String stream * - * @param reduceToSingleBuffer whether this decoder should buffer all received items - * and decode a single consolidated String or re-emit items as they are provided + * @param splitOnNewline whether this decoder should split the received data buffers + * along newline characters */ - public StringDecoder(boolean reduceToSingleBuffer) { + public StringDecoder(boolean splitOnNewline) { super(new MimeType("text", "*", DEFAULT_CHARSET), MimeTypeUtils.ALL); - this.reduceToSingleBuffer = reduceToSingleBuffer; + this.splitOnNewline = splitOnNewline; } @Override @@ -81,8 +79,12 @@ public class StringDecoder extends AbstractDecoder { public Flux decode(Publisher inputStream, ResolvableType type, MimeType mimeType, Object... hints) { Flux inputFlux = Flux.from(inputStream); - if (this.reduceToSingleBuffer) { - inputFlux = Flux.from(inputFlux.reduce(DataBuffer::write)); + if (this.splitOnNewline) { + inputFlux = inputFlux.flatMap(dataBuffer -> { + List tokens = + DataBufferUtils.tokenize(dataBuffer, b -> b == '\n' || b == '\r'); + return Flux.fromIterable(tokens); + }); } Charset charset = getCharset(mimeType); return inputFlux.map(dataBuffer -> { @@ -93,8 +95,8 @@ public class StringDecoder extends AbstractDecoder { } private Charset getCharset(MimeType mimeType) { - if (mimeType != null && mimeType.getCharSet() != null) { - return mimeType.getCharSet(); + if (mimeType != null && mimeType.getCharset() != null) { + return mimeType.getCharset(); } else { return DEFAULT_CHARSET; diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/XmlEventDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/XmlEventDecoder.java index b0174ad4c7..3f27b97fb7 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/XmlEventDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/XmlEventDecoder.java @@ -16,6 +16,7 @@ package org.springframework.core.codec.support; +import java.io.InputStream; import java.util.ArrayList; import java.util.List; import java.util.function.Function; @@ -91,9 +92,9 @@ public class XmlEventDecoder extends AbstractDecoder { else { Mono singleBuffer = flux.reduce(DataBuffer::write); return singleBuffer. - map(DataBuffer::asInputStream). - flatMap(is -> { + flatMap(dataBuffer -> { try { + InputStream is = dataBuffer.asInputStream(); XMLEventReader eventReader = inputFactory.createXMLEventReader(is); return Flux @@ -102,6 +103,9 @@ public class XmlEventDecoder extends AbstractDecoder { catch (XMLStreamException ex) { return Mono.error(ex); } + finally { + DataBufferUtils.release(dataBuffer); + } }); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringDecoderTests.java index c1d69680af..34a8b0ae24 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringDecoderTests.java @@ -18,18 +18,16 @@ package org.springframework.core.codec.support; import org.junit.Before; import org.junit.Test; -import reactor.core.converter.RxJava1SingleConverter; import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; import reactor.core.test.TestSubscriber; -import rx.Single; import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.AbstractDataBufferAllocatingTestCase; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.MediaType; -import static org.junit.Assert.*; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; /** * @author Sebastien Deleuze @@ -62,48 +60,36 @@ public class StringDecoderTests extends AbstractDataBufferAllocatingTestCase { @Test public void decode() throws InterruptedException { + this.decoder = new StringDecoder(false); Flux source = Flux.just(stringBuffer("foo"), stringBuffer("bar"), stringBuffer("baz")); Flux output = this.decoder.decode(source, ResolvableType.forClass(String.class), null); TestSubscriber testSubscriber = new TestSubscriber<>(); - testSubscriber.bindTo(output).assertValues("foobarbaz"); + testSubscriber.bindTo(output). + assertNoError(). + assertComplete(). + assertValues("foo", "bar", "baz"); } @Test - public void decodeDoNotBuffer() throws InterruptedException { - StringDecoder decoder = new StringDecoder(false); - Flux source = Flux.just(stringBuffer("foo"), stringBuffer("bar")); + public void decodeNewLine() throws InterruptedException { + DataBuffer fooBar = stringBuffer("\nfoo\r\nbar\r"); + DataBuffer baz = stringBuffer("\nbaz"); + Flux source = Flux.just(fooBar, baz); Flux output = decoder.decode(source, ResolvableType.forClass(String.class), null); TestSubscriber testSubscriber = new TestSubscriber<>(); - testSubscriber.bindTo(output).assertValues("foo", "bar"); + testSubscriber.bindTo(output). + assertNoError(). + assertComplete(). + assertValues("foo", "bar", "baz"); } - @Test - public void decodeMono() throws InterruptedException { - Flux source = - Flux.just(stringBuffer("foo"), stringBuffer("bar"), stringBuffer("baz")); - Mono mono = Mono.from(this.decoder.decode(source, - ResolvableType.forClassWithGenerics(Mono.class, String.class), - MediaType.TEXT_PLAIN)); - TestSubscriber testSubscriber = new TestSubscriber<>(); - testSubscriber.bindTo(mono).assertValues("foobarbaz"); - } - - @Test - public void decodeSingle() throws InterruptedException { - Flux source = Flux.just(stringBuffer("foo"), stringBuffer("bar")); - Single single = RxJava1SingleConverter.from(this.decoder.decode(source, - ResolvableType.forClassWithGenerics(Single.class, String.class), - MediaType.TEXT_PLAIN)); - String result = single.toBlocking().value(); - assertEquals("foobar", result); - } @Test public void decodeEmpty() throws InterruptedException { - Mono source = Mono.just(stringBuffer("")); + Flux source = Flux.just(stringBuffer("")); Flux output = this.decoder.decode(source, ResolvableType.forClass(String.class), null); TestSubscriber testSubscriber = new TestSubscriber<>(); diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringEncoderTests.java index 3742a6589a..20e642d934 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringEncoderTests.java @@ -27,6 +27,7 @@ import reactor.core.test.TestSubscriber; import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.AbstractDataBufferAllocatingTestCase; +import org.springframework.core.io.buffer.support.DataBufferUtils; import org.springframework.http.MediaType; import static org.junit.Assert.assertFalse; @@ -60,9 +61,10 @@ public class StringEncoderTests extends AbstractDataBufferAllocatingTestCase { Flux output = Flux.from( this.encoder.encode(Flux.just("foo"), this.allocator, null, null)) .map(chunk -> { - byte[] b = new byte[chunk.readableByteCount()]; - chunk.read(b); - return new String(b, StandardCharsets.UTF_8); + byte[] b = new byte[chunk.readableByteCount()]; + chunk.read(b); + DataBufferUtils.release(chunk); + return new String(b, StandardCharsets.UTF_8); }); TestSubscriber testSubscriber = new TestSubscriber<>(); testSubscriber.bindTo(output).assertValues("foo"); From d36286c7d1b5ffd13d847e3b97f4da2d8df66d74 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Fri, 27 May 2016 09:51:42 +0200 Subject: [PATCH 363/498] Renamed DataBufferAllocator to DataBufferFactory --- .../springframework/core/codec/Encoder.java | 6 ++--- .../support/AbstractSingleValueEncoder.java | 10 +++---- .../core/codec/support/ByteBufferEncoder.java | 6 ++--- .../codec/support/JacksonJsonEncoder.java | 25 +++++++++-------- .../core/codec/support/Jaxb2Encoder.java | 6 ++--- .../core/codec/support/JsonObjectDecoder.java | 8 +++--- .../core/codec/support/ResourceEncoder.java | 7 ++--- .../core/codec/support/StringEncoder.java | 6 ++--- .../core/io/buffer/DataBuffer.java | 6 ++--- ...rAllocator.java => DataBufferFactory.java} | 6 ++--- .../core/io/buffer/DefaultDataBuffer.java | 27 ++++++++++--------- ...tor.java => DefaultDataBufferFactory.java} | 20 +++++++------- .../core/io/buffer/NettyDataBuffer.java | 18 ++++++------- ...cator.java => NettyDataBufferFactory.java} | 12 ++++----- .../io/buffer/support/DataBufferUtils.java | 27 ++++++++++--------- .../http/ReactiveHttpOutputMessage.java | 8 +++--- .../reactive/ReactorClientHttpRequest.java | 19 +++++++------ .../reactive/ReactorClientHttpResponse.java | 13 +++++---- .../ReactorHttpClientRequestFactory.java | 3 --- .../reactive/RxNettyClientHttpRequest.java | 22 +++++++-------- .../reactive/RxNettyClientHttpResponse.java | 13 ++++----- .../RxNettyHttpClientRequestFactory.java | 11 ++++---- .../reactive/CodecHttpMessageConverter.java | 6 ++--- .../reactive/AbstractServerHttpResponse.java | 14 +++++----- .../reactive/ReactorHttpHandlerAdapter.java | 10 +++---- .../reactive/ReactorServerHttpRequest.java | 13 ++++----- .../reactive/ReactorServerHttpResponse.java | 6 ++--- .../reactive/RxNettyHttpHandlerAdapter.java | 10 +++---- .../reactive/RxNettyServerHttpRequest.java | 13 ++++----- .../reactive/RxNettyServerHttpResponse.java | 6 ++--- .../reactive/ServletHttpHandlerAdapter.java | 24 ++++++++--------- .../reactive/ServletServerHttpResponse.java | 6 ++--- .../reactive/UndertowHttpHandlerAdapter.java | 24 ++++++++--------- .../reactive/UndertowServerHttpResponse.java | 6 ++--- .../reactive/boot/UndertowHttpServer.java | 13 ++++----- .../reactive/DefaultHttpRequestBuilder.java | 7 ++--- .../reactive/result/view/AbstractView.java | 20 +++++++------- .../result/view/ViewResolverSupport.java | 19 ++++++------- .../codec/support/ByteBufferEncoderTests.java | 2 +- .../support/JacksonJsonEncoderTests.java | 3 ++- .../core/codec/support/Jaxb2EncoderTests.java | 4 +-- .../codec/support/ResourceEncoderTests.java | 4 +-- .../codec/support/StringEncoderTests.java | 2 +- .../AbstractDataBufferAllocatingTestCase.java | 20 +++++++------- .../core/io/buffer/PooledDataBufferTests.java | 12 ++++----- .../support/DataBufferTestUtilsTests.java | 4 +-- .../buffer/support/DataBufferUtilsTests.java | 6 ++--- .../reactive/AsyncIntegrationTests.java | 11 ++++---- .../reactive/MockServerHttpResponse.java | 10 +++---- .../RandomHandlerIntegrationTests.java | 8 +++--- .../reactive/ServerHttpResponseTests.java | 6 ++--- .../reactive/DispatcherHandlerErrorTests.java | 8 +++--- .../result/WebHandlerIntegrationTests.java | 5 ++-- .../RequestMappingIntegrationTests.java | 16 +++++------ .../ViewResolutionResultHandlerTests.java | 9 +++---- 55 files changed, 303 insertions(+), 303 deletions(-) rename spring-web-reactive/src/main/java/org/springframework/core/io/buffer/{DataBufferAllocator.java => DataBufferFactory.java} (91%) rename spring-web-reactive/src/main/java/org/springframework/core/io/buffer/{DefaultDataBufferAllocator.java => DefaultDataBufferFactory.java} (78%) rename spring-web-reactive/src/main/java/org/springframework/core/io/buffer/{NettyDataBufferAllocator.java => NettyDataBufferFactory.java} (83%) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/Encoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/Encoder.java index 0a53c9552b..f7b5395659 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/Encoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/Encoder.java @@ -23,7 +23,7 @@ import reactor.core.publisher.Flux; import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferAllocator; +import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.util.MimeType; /** @@ -46,14 +46,14 @@ public interface Encoder { /** * Encode an input stream of {@code T} to an output {@link DataBuffer} stream. * @param inputStream the input stream to process. - * @param allocator a buffer allocator used to create the output + * @param dataBufferFactory a buffer factory used to create the output * @param type the stream element type to process. * @param mimeType the mime type to process. * @param hints Additional information about how to do decode, optional. * @return the output stream */ Flux encode(Publisher inputStream, - DataBufferAllocator allocator, ResolvableType type, + DataBufferFactory dataBufferFactory, ResolvableType type, MimeType mimeType, Object... hints); /** diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractSingleValueEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractSingleValueEncoder.java index 15c8ffcc42..82264f003a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractSingleValueEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractSingleValueEncoder.java @@ -21,7 +21,7 @@ import reactor.core.publisher.Flux; import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferAllocator; +import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.util.MimeType; /** @@ -37,13 +37,13 @@ public abstract class AbstractSingleValueEncoder extends AbstractEncoder { @Override public final Flux encode(Publisher inputStream, - DataBufferAllocator allocator, ResolvableType type, MimeType mimeType, + DataBufferFactory dataBufferFactory, ResolvableType type, MimeType mimeType, Object... hints) { return Flux.from(inputStream). take(1). concatMap(t -> { try { - return encode(t, allocator, type, mimeType); + return encode(t, dataBufferFactory, type, mimeType); } catch (Exception ex) { return Flux.error(ex); @@ -54,14 +54,14 @@ public abstract class AbstractSingleValueEncoder extends AbstractEncoder { /** * Encodes {@code T} to an output {@link DataBuffer} stream. * @param t the value to process - * @param allocator a buffer allocator used to create the output + * @param dataBufferFactory a buffer factory used to create the output * @param type the stream element type to process * @param mimeType the mime type to process * @param hints Additional information about how to do decode, optional * @return the output stream * @throws Exception in case of errors */ - protected abstract Flux encode(T t, DataBufferAllocator allocator, + protected abstract Flux encode(T t, DataBufferFactory dataBufferFactory, ResolvableType type, MimeType mimeType, Object... hints) throws Exception; diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferEncoder.java index 3d006f2c99..fe1f0a9803 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferEncoder.java @@ -23,7 +23,7 @@ import reactor.core.publisher.Flux; import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferAllocator; +import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.util.MimeType; import org.springframework.util.MimeTypeUtils; @@ -45,10 +45,10 @@ public class ByteBufferEncoder extends AbstractEncoder { @Override public Flux encode(Publisher inputStream, - DataBufferAllocator allocator, ResolvableType type, MimeType mimeType, + DataBufferFactory dataBufferFactory, ResolvableType type, MimeType mimeType, Object... hints) { - return Flux.from(inputStream).map(allocator::wrap); + return Flux.from(inputStream).map(dataBufferFactory::wrap); } } \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonEncoder.java index 81e317b114..7c0a9924aa 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonEncoder.java @@ -29,7 +29,7 @@ import reactor.core.publisher.Mono; import org.springframework.core.ResolvableType; import org.springframework.core.codec.CodecException; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferAllocator; +import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.util.Assert; import org.springframework.util.MimeType; @@ -64,21 +64,24 @@ public class JacksonJsonEncoder extends AbstractEncoder { @Override public Flux encode(Publisher inputStream, - DataBufferAllocator allocator, ResolvableType type, MimeType mimeType, + DataBufferFactory dataBufferFactory, ResolvableType type, MimeType mimeType, Object... hints) { if (inputStream instanceof Mono) { // single object - return Flux.from(inputStream).map(value -> serialize(value, allocator)); + return Flux.from(inputStream) + .map(value -> serialize(value, dataBufferFactory)); } else { // array - Mono startArray = Mono.just(allocator.wrap(START_ARRAY_BUFFER)); - Flux arraySeparators = Mono.just(allocator.wrap(SEPARATOR_BUFFER)) - .repeat(); - Mono endArray = Mono.just(allocator.wrap(END_ARRAY_BUFFER)); + Mono startArray = + Mono.just(dataBufferFactory.wrap(START_ARRAY_BUFFER)); + Flux arraySeparators = + Mono.just(dataBufferFactory.wrap(SEPARATOR_BUFFER)).repeat(); + Mono endArray = + Mono.just(dataBufferFactory.wrap(END_ARRAY_BUFFER)); - Flux serializedObjects = - Flux.from(inputStream).map(value -> serialize(value, allocator)); + Flux serializedObjects = Flux.from(inputStream) + .map(value -> serialize(value, dataBufferFactory)); Flux array = Flux.zip(serializedObjects, arraySeparators) .flatMap(tuple -> Flux.just(tuple.getT1(), tuple.getT2())); @@ -89,8 +92,8 @@ public class JacksonJsonEncoder extends AbstractEncoder { } } - private DataBuffer serialize(Object value, DataBufferAllocator allocator) { - DataBuffer buffer = allocator.allocateBuffer(); + private DataBuffer serialize(Object value, DataBufferFactory dataBufferFactory) { + DataBuffer buffer = dataBufferFactory.allocateBuffer(); OutputStream outputStream = buffer.asOutputStream(); try { this.mapper.writeValue(outputStream, value); diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Encoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Encoder.java index 9674c8b79b..d2d9d2f6cb 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Encoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Encoder.java @@ -27,7 +27,7 @@ import reactor.core.publisher.Flux; import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferAllocator; +import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.util.ClassUtils; import org.springframework.util.MimeType; import org.springframework.util.MimeTypeUtils; @@ -61,10 +61,10 @@ public class Jaxb2Encoder extends AbstractSingleValueEncoder { } @Override - protected Flux encode(Object value, DataBufferAllocator allocator, + protected Flux encode(Object value, DataBufferFactory dataBufferFactory, ResolvableType type, MimeType mimeType, Object... hints) { try { - DataBuffer buffer = allocator.allocateBuffer(1024); + DataBuffer buffer = dataBufferFactory.allocateBuffer(1024); OutputStream outputStream = buffer.asOutputStream(); Class clazz = ClassUtils.getUserClass(value); Marshaller marshaller = jaxbContexts.createMarshaller(clazz); diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectDecoder.java index 1c37fe14f3..fbcb00ab3d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectDecoder.java @@ -29,7 +29,7 @@ import reactor.core.publisher.Flux; import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferAllocator; +import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.support.DataBufferUtils; import org.springframework.util.MimeType; @@ -131,7 +131,7 @@ public class JsonObjectDecoder extends AbstractDecoder { return Flux.error(new IllegalStateException("object length exceeds " + maxObjectLength + ": " + this.writerIndex + " bytes discarded")); } - DataBufferAllocator allocator = b.allocator(); + DataBufferFactory dataBufferFactory = b.factory(); for (/* use current index */; this.index < this.writerIndex; this.index++) { byte c = this.input.getByte(this.index); if (this.state == ST_DECODING_NORMAL) { @@ -143,7 +143,7 @@ public class JsonObjectDecoder extends AbstractDecoder { ByteBuf json = extractObject(this.input, this.input.readerIndex(), this.index + 1 - this.input.readerIndex()); if (json != null) { - chunks.add(allocator.wrap(json.nioBuffer())); + chunks.add(dataBufferFactory.wrap(json.nioBuffer())); } // The JSON object/array was extracted => discard the bytes from @@ -177,7 +177,7 @@ public class JsonObjectDecoder extends AbstractDecoder { idxNoSpaces + 1 - this.input.readerIndex()); if (json != null) { - chunks.add(allocator.wrap(json.nioBuffer())); + chunks.add(dataBufferFactory.wrap(json.nioBuffer())); } this.input.readerIndex(this.index + 1); diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ResourceEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ResourceEncoder.java index 497b79efd8..742c8de634 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ResourceEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ResourceEncoder.java @@ -24,7 +24,7 @@ import reactor.core.publisher.Flux; import org.springframework.core.ResolvableType; import org.springframework.core.io.Resource; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferAllocator; +import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.support.DataBufferUtils; import org.springframework.util.Assert; import org.springframework.util.MimeType; @@ -59,10 +59,11 @@ public class ResourceEncoder extends AbstractSingleValueEncoder { } @Override - protected Flux encode(Resource resource, DataBufferAllocator allocator, + protected Flux encode(Resource resource, + DataBufferFactory dataBufferFactory, ResolvableType type, MimeType mimeType, Object... hints) throws IOException { InputStream is = resource.getInputStream(); - return DataBufferUtils.read(is, allocator, bufferSize); + return DataBufferUtils.read(is, dataBufferFactory, bufferSize); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringEncoder.java index 14914e4b15..21c09cfc82 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringEncoder.java @@ -24,7 +24,7 @@ import reactor.core.publisher.Flux; import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferAllocator; +import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.util.MimeType; /** @@ -50,7 +50,7 @@ public class StringEncoder extends AbstractEncoder { @Override public Flux encode(Publisher inputStream, - DataBufferAllocator allocator, ResolvableType type, MimeType mimeType, + DataBufferFactory dataBufferFactory, ResolvableType type, MimeType mimeType, Object... hints) { Charset charset; if (mimeType != null && mimeType.getCharSet() != null) { @@ -61,7 +61,7 @@ public class StringEncoder extends AbstractEncoder { } return Flux.from(inputStream).map(s -> { byte[] bytes = s.getBytes(charset); - DataBuffer dataBuffer = allocator.allocateBuffer(bytes.length); + DataBuffer dataBuffer = dataBufferFactory.allocateBuffer(bytes.length); dataBuffer.write(bytes); return dataBuffer; }); diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DataBuffer.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DataBuffer.java index 31cf7f63ac..8fd84476ff 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DataBuffer.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DataBuffer.java @@ -29,10 +29,10 @@ import java.util.function.IntPredicate; public interface DataBuffer { /** - * Returns the {@link DataBufferAllocator} that created this buffer. - * @return the creating buffer allocator + * Returns the {@link DataBufferFactory} that created this buffer. + * @return the creating buffer factory */ - DataBufferAllocator allocator(); + DataBufferFactory factory(); /** * Returns the index of the first byte in this buffer that matches the given diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DataBufferAllocator.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DataBufferFactory.java similarity index 91% rename from spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DataBufferAllocator.java rename to spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DataBufferFactory.java index 881de92684..9fba5de665 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DataBufferAllocator.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DataBufferFactory.java @@ -19,13 +19,13 @@ package org.springframework.core.io.buffer; import java.nio.ByteBuffer; /** - * A factory for {@link DataBuffer}s, allowing for allocation of heap-based and direct - * data buffers. + * A factory for {@link DataBuffer}s, allowing for allocation and wrapping of data + * buffers. * * @author Arjen Poutsma * @see DataBuffer */ -public interface DataBufferAllocator { +public interface DataBufferFactory { /** * Allocates a data buffer of a default initial capacity. Depending on the underlying diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java index 940c0d1a8b..b46a03ce90 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java @@ -30,14 +30,14 @@ import org.springframework.util.ObjectUtils; /** * Default implementation of the {@link DataBuffer} interface that uses a {@link * ByteBuffer} internally, with separate read and write positions. Constructed - * using the {@link DefaultDataBufferAllocator}. + * using the {@link DefaultDataBufferFactory}. * * @author Arjen Poutsma - * @see DefaultDataBufferAllocator + * @see DefaultDataBufferFactory */ public class DefaultDataBuffer implements DataBuffer { - private final DefaultDataBufferAllocator allocator; + private final DefaultDataBufferFactory dataBufferFactory; private ByteBuffer byteBuffer; @@ -51,27 +51,28 @@ public class DefaultDataBuffer implements DataBuffer { * ByteBuffer#position() position} of the given buffer. * @param byteBuffer the buffer to base this buffer on */ - DefaultDataBuffer(ByteBuffer byteBuffer, DefaultDataBufferAllocator allocator) { - this(byteBuffer, byteBuffer.position(), byteBuffer.position(), allocator); + DefaultDataBuffer(ByteBuffer byteBuffer, DefaultDataBufferFactory dataBufferFactory) { + this(byteBuffer, byteBuffer.position(), byteBuffer.position(), dataBufferFactory); } - DefaultDataBuffer(ByteBuffer byteBuffer, int readPosition, int writePosition, DefaultDataBufferAllocator allocator) { + DefaultDataBuffer(ByteBuffer byteBuffer, int readPosition, int writePosition, + DefaultDataBufferFactory dataBufferFactory) { Assert.notNull(byteBuffer, "'byteBuffer' must not be null"); Assert.isTrue(readPosition >= 0, "'readPosition' must be 0 or higher"); Assert.isTrue(writePosition >= 0, "'writePosition' must be 0 or higher"); Assert.isTrue(readPosition <= writePosition, "'readPosition' must be smaller than or equal to 'writePosition'"); - Assert.notNull(allocator, "'allocator' must not be null"); + Assert.notNull(dataBufferFactory, "'dataBufferFactory' must not be null"); this.byteBuffer = byteBuffer; this.readPosition = readPosition; this.writePosition = writePosition; - this.allocator = allocator; + this.dataBufferFactory = dataBufferFactory; } @Override - public DefaultDataBufferAllocator allocator() { - return this.allocator; + public DefaultDataBufferFactory factory() { + return this.dataBufferFactory; } /** @@ -219,7 +220,7 @@ public class DefaultDataBuffer implements DataBuffer { this.byteBuffer.position(index); ByteBuffer slice = this.byteBuffer.slice(); slice.limit(length); - return new SlicedDefaultDataBuffer(slice, 0, length, this.allocator); + return new SlicedDefaultDataBuffer(slice, 0, length, this.dataBufferFactory); } finally { this.byteBuffer.position(oldPosition); @@ -337,8 +338,8 @@ public class DefaultDataBuffer implements DataBuffer { private static class SlicedDefaultDataBuffer extends DefaultDataBuffer { SlicedDefaultDataBuffer(ByteBuffer byteBuffer, int readPosition, - int writePosition, DefaultDataBufferAllocator allocator) { - super(byteBuffer, readPosition, writePosition, allocator); + int writePosition, DefaultDataBufferFactory dataBufferFactory) { + super(byteBuffer, readPosition, writePosition, dataBufferFactory); } @Override diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBufferAllocator.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBufferFactory.java similarity index 78% rename from spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBufferAllocator.java rename to spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBufferFactory.java index 0d3c6661b1..7b1dd007b8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBufferAllocator.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBufferFactory.java @@ -21,18 +21,18 @@ import java.nio.ByteBuffer; import org.springframework.util.Assert; /** - * Default implementation of the {@code DataBufferAllocator} interface. Allows for + * Default implementation of the {@code DataBufferFactory} interface. Allows for * specification of the default initial capacity at construction time, as well as whether * heap-based or direct buffers are to be preferred. * * @author Arjen Poutsma */ -public class DefaultDataBufferAllocator implements DataBufferAllocator { +public class DefaultDataBufferFactory implements DataBufferFactory { /** * The default capacity when none is specified. - * @see #DefaultDataBufferAllocator() - * @see #DefaultDataBufferAllocator(boolean) + * @see #DefaultDataBufferFactory() + * @see #DefaultDataBufferFactory(boolean) */ public static final int DEFAULT_INITIAL_CAPACITY = 256; @@ -42,30 +42,30 @@ public class DefaultDataBufferAllocator implements DataBufferAllocator { private final int defaultInitialCapacity; /** - * Creates a new {@code DefaultDataBufferAllocator} with default settings. + * Creates a new {@code DefaultDataBufferFactory} with default settings. */ - public DefaultDataBufferAllocator() { + public DefaultDataBufferFactory() { this(false); } /** - * Creates a new {@code DefaultDataBufferAllocator}, indicating whether direct buffers + * Creates a new {@code DefaultDataBufferFactory}, indicating whether direct buffers * should be created by {@link #allocateBuffer()} and {@link #allocateBuffer(int)}. * @param preferDirect {@code true} if direct buffers are to be preferred; {@code * false} otherwise */ - public DefaultDataBufferAllocator(boolean preferDirect) { + public DefaultDataBufferFactory(boolean preferDirect) { this(preferDirect, DEFAULT_INITIAL_CAPACITY); } /** - * Creates a new {@code DefaultDataBufferAllocator}, indicating whether direct buffers + * Creates a new {@code DefaultDataBufferFactory}, indicating whether direct buffers * should be created by {@link #allocateBuffer()} and {@link #allocateBuffer(int)}, * and what the capacity is to be used for {@link #allocateBuffer()}. * @param preferDirect {@code true} if direct buffers are to be preferred; {@code * false} otherwise */ - public DefaultDataBufferAllocator(boolean preferDirect, int defaultInitialCapacity) { + public DefaultDataBufferFactory(boolean preferDirect, int defaultInitialCapacity) { Assert.isTrue(defaultInitialCapacity > 0, "'defaultInitialCapacity' should be larger than 0"); this.preferDirect = preferDirect; diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/NettyDataBuffer.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/NettyDataBuffer.java index c5aa526c04..4686490fd1 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/NettyDataBuffer.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/NettyDataBuffer.java @@ -33,13 +33,13 @@ import org.springframework.util.ObjectUtils; /** * Implementation of the {@code DataBuffer} interface that wraps a Netty {@link ByteBuf}. - * Typically constructed using the {@link NettyDataBufferAllocator}. + * Typically constructed using the {@link NettyDataBufferFactory}. * * @author Arjen Poutsma */ public class NettyDataBuffer implements PooledDataBuffer { - private final NettyDataBufferAllocator allocator; + private final NettyDataBufferFactory dataBufferFactory; private ByteBuf byteBuf; @@ -47,17 +47,17 @@ public class NettyDataBuffer implements PooledDataBuffer { * Creates a new {@code NettyDataBuffer} based on the given {@code ByteBuff}. * @param byteBuf the buffer to base this buffer on */ - NettyDataBuffer(ByteBuf byteBuf, NettyDataBufferAllocator allocator) { + NettyDataBuffer(ByteBuf byteBuf, NettyDataBufferFactory dataBufferFactory) { Assert.notNull(byteBuf, "'byteBuf' must not be null"); - Assert.notNull(allocator, "'allocator' must not be null"); + Assert.notNull(dataBufferFactory, "'dataBufferFactory' must not be null"); this.byteBuf = byteBuf; - this.allocator = allocator; + this.dataBufferFactory = dataBufferFactory; } @Override - public NettyDataBufferAllocator allocator() { - return allocator; + public NettyDataBufferFactory factory() { + return this.dataBufferFactory; } /** @@ -177,7 +177,7 @@ public class NettyDataBuffer implements PooledDataBuffer { @Override public DataBuffer slice(int index, int length) { ByteBuf slice = this.byteBuf.slice(index, length); - return new NettyDataBuffer(slice, this.allocator); + return new NettyDataBuffer(slice, this.dataBufferFactory); } @Override @@ -197,7 +197,7 @@ public class NettyDataBuffer implements PooledDataBuffer { @Override public PooledDataBuffer retain() { - return new NettyDataBuffer(this.byteBuf.retain(), allocator); + return new NettyDataBuffer(this.byteBuf.retain(), dataBufferFactory); } @Override diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/NettyDataBufferAllocator.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/NettyDataBufferFactory.java similarity index 83% rename from spring-web-reactive/src/main/java/org/springframework/core/io/buffer/NettyDataBufferAllocator.java rename to spring-web-reactive/src/main/java/org/springframework/core/io/buffer/NettyDataBufferFactory.java index 12f8dae0ab..440c2c0f4b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/NettyDataBufferAllocator.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/NettyDataBufferFactory.java @@ -25,24 +25,24 @@ import io.netty.buffer.Unpooled; import org.springframework.util.Assert; /** - * Implementation of the {@code DataBufferAllocator} interface based on a Netty + * Implementation of the {@code DataBufferFactory} interface based on a Netty * {@link ByteBufAllocator}. * * @author Arjen Poutsma * @see io.netty.buffer.PooledByteBufAllocator * @see io.netty.buffer.UnpooledByteBufAllocator */ -public class NettyDataBufferAllocator implements DataBufferAllocator { +public class NettyDataBufferFactory implements DataBufferFactory { private final ByteBufAllocator byteBufAllocator; /** - * Creates a new {@code NettyDataBufferAllocator} based on the given allocator. - * @param byteBufAllocator the allocator to use + * Creates a new {@code NettyDataBufferFactory} based on the given factory. + * @param byteBufAllocator the factory to use * @see io.netty.buffer.PooledByteBufAllocator * @see io.netty.buffer.UnpooledByteBufAllocator */ - public NettyDataBufferAllocator(ByteBufAllocator byteBufAllocator) { + public NettyDataBufferFactory(ByteBufAllocator byteBufAllocator) { Assert.notNull(byteBufAllocator, "'byteBufAllocator' must not be null"); this.byteBufAllocator = byteBufAllocator; @@ -77,6 +77,6 @@ public class NettyDataBufferAllocator implements DataBufferAllocator { @Override public String toString() { - return "NettyDataBufferAllocator (" + this.byteBufAllocator + ")"; + return "NettyDataBufferFactory (" + this.byteBufAllocator + ")"; } } diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java index 5c264a6dad..09270ff7db 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java @@ -34,7 +34,7 @@ import reactor.core.publisher.Flux; import reactor.core.subscriber.SignalEmitter; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferAllocator; +import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.PooledDataBuffer; import org.springframework.util.Assert; @@ -59,34 +59,34 @@ public abstract class DataBufferUtils { * Reads the given {@code InputStream} into a {@code Flux} of * {@code DataBuffer}s. Closes the stream when the flux inputStream terminated. * @param inputStream the input stream to read from - * @param allocator the allocator to create data buffers with + * @param dataBufferFactory the factory to create data buffers with * @param bufferSize the maximum size of the data buffers * @return a flux of data buffers read from the given channel */ public static Flux read(InputStream inputStream, - DataBufferAllocator allocator, int bufferSize) { + DataBufferFactory dataBufferFactory, int bufferSize) { Assert.notNull(inputStream, "'inputStream' must not be null"); - Assert.notNull(allocator, "'allocator' must not be null"); + Assert.notNull(dataBufferFactory, "'dataBufferFactory' must not be null"); ReadableByteChannel channel = Channels.newChannel(inputStream); - return read(channel, allocator, bufferSize); + return read(channel, dataBufferFactory, bufferSize); } /** * Reads the given {@code ReadableByteChannel} into a {@code Flux} of * {@code DataBuffer}s. Closes the channel when the flux is terminated. * @param channel the channel to read from - * @param allocator the allocator to create data buffers with + * @param dataBufferFactory the factory to create data buffers with * @param bufferSize the maximum size of the data buffers * @return a flux of data buffers read from the given channel */ public static Flux read(ReadableByteChannel channel, - DataBufferAllocator allocator, int bufferSize) { + DataBufferFactory dataBufferFactory, int bufferSize) { Assert.notNull(channel, "'channel' must not be null"); - Assert.notNull(allocator, "'allocator' must not be null"); + Assert.notNull(dataBufferFactory, "'dataBufferFactory' must not be null"); return Flux.generate(() -> channel, - new ReadableByteChannelGenerator(allocator, bufferSize), + new ReadableByteChannelGenerator(dataBufferFactory, bufferSize), CLOSE_CONSUMER); } @@ -194,12 +194,13 @@ public abstract class DataBufferUtils { implements BiFunction, ReadableByteChannel> { - private final DataBufferAllocator allocator; + private final DataBufferFactory dataBufferFactory; private final int chunkSize; - public ReadableByteChannelGenerator(DataBufferAllocator allocator, int chunkSize) { - this.allocator = allocator; + public ReadableByteChannelGenerator(DataBufferFactory dataBufferFactory, + int chunkSize) { + this.dataBufferFactory = dataBufferFactory; this.chunkSize = chunkSize; } @@ -212,7 +213,7 @@ public abstract class DataBufferUtils { if ((read = channel.read(byteBuffer)) > 0) { byteBuffer.flip(); boolean release = true; - DataBuffer dataBuffer = this.allocator.allocateBuffer(read); + DataBuffer dataBuffer = this.dataBufferFactory.allocateBuffer(read); try { dataBuffer.write(byteBuffer); release = false; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java b/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java index 0eaa89515d..4e84c91886 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java @@ -22,7 +22,7 @@ import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferAllocator; +import org.springframework.core.io.buffer.DataBufferFactory; /** * A "reactive" HTTP output message that accepts output as a {@link Publisher}. @@ -50,10 +50,10 @@ public interface ReactiveHttpOutputMessage extends HttpMessage { Mono setBody(Publisher body); /** - * Returns a {@link DataBufferAllocator} that can be used for creating the body. - * @return a buffer allocator + * Returns a {@link DataBufferFactory} that can be used for creating the body. + * @return a buffer factory * @see #setBody(Publisher) */ - DataBufferAllocator allocator(); + DataBufferFactory dataBufferFactory(); } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java index 07ce7068ca..d97c495d75 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java @@ -28,10 +28,9 @@ import reactor.core.publisher.Mono; import reactor.io.netty.http.HttpClient; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferAllocator; -import org.springframework.core.io.buffer.DefaultDataBufferAllocator; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.core.io.buffer.NettyDataBuffer; -import org.springframework.core.io.buffer.NettyDataBufferAllocator; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -43,7 +42,7 @@ import org.springframework.http.HttpMethod; */ public class ReactorClientHttpRequest extends AbstractClientHttpRequest { - private final DataBufferAllocator allocator; + private final DataBufferFactory dataBufferFactory; private final HttpMethod httpMethod; @@ -56,16 +55,16 @@ public class ReactorClientHttpRequest extends AbstractClientHttpRequest { public ReactorClientHttpRequest(HttpMethod httpMethod, URI uri, HttpClient httpClient, HttpHeaders headers) { super(headers); - //FIXME use Netty allocator - this.allocator = new DefaultDataBufferAllocator(); + //FIXME use Netty factory + this.dataBufferFactory = new DefaultDataBufferFactory(); this.httpMethod = httpMethod; this.uri = uri; this.httpClient = httpClient; } @Override - public DataBufferAllocator allocator() { - return this.allocator; + public DataBufferFactory dataBufferFactory() { + return this.dataBufferFactory; } @Override @@ -121,8 +120,8 @@ public class ReactorClientHttpRequest extends AbstractClientHttpRequest { return channel.sendHeaders(); } }); - }) - .map(httpChannel -> new ReactorClientHttpResponse(httpChannel, allocator)); + }).map(httpChannel -> new ReactorClientHttpResponse(httpChannel, + dataBufferFactory)); } private ByteBuf toByteBuf(DataBuffer buffer) { diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpResponse.java index 8e687132b7..0a6e4f6d73 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpResponse.java @@ -22,8 +22,7 @@ import reactor.core.publisher.Flux; import reactor.io.netty.http.HttpInbound; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferAllocator; -import org.springframework.core.io.buffer.NettyDataBufferAllocator; +import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseCookie; @@ -39,19 +38,19 @@ import org.springframework.util.MultiValueMap; */ public class ReactorClientHttpResponse implements ClientHttpResponse { - private final DataBufferAllocator allocator; + private final DataBufferFactory dataBufferFactory; private final HttpInbound channel; - - public ReactorClientHttpResponse(HttpInbound channel, DataBufferAllocator allocator) { - this.allocator = allocator; + public ReactorClientHttpResponse(HttpInbound channel, + DataBufferFactory dataBufferFactory) { + this.dataBufferFactory = dataBufferFactory; this.channel = channel; } @Override public Flux getBody() { - return channel.receiveByteBuffer().map(allocator::wrap); + return channel.receiveByteBuffer().map(dataBufferFactory::wrap); } @Override diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorHttpClientRequestFactory.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorHttpClientRequestFactory.java index ad1a9aad42..70becdd999 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorHttpClientRequestFactory.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorHttpClientRequestFactory.java @@ -20,9 +20,6 @@ import java.net.URI; import reactor.io.netty.http.HttpClient; -import org.springframework.core.io.buffer.DataBufferAllocator; -import org.springframework.core.io.buffer.DefaultDataBufferAllocator; -import org.springframework.core.io.buffer.NettyDataBufferAllocator; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.util.Assert; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpRequest.java index cf42da74bf..46d9550c31 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpRequest.java @@ -31,8 +31,8 @@ import reactor.core.publisher.Mono; import rx.Observable; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferAllocator; -import org.springframework.core.io.buffer.NettyDataBufferAllocator; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.NettyDataBufferFactory; import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -44,7 +44,7 @@ import org.springframework.http.HttpMethod; */ public class RxNettyClientHttpRequest extends AbstractClientHttpRequest { - private final NettyDataBufferAllocator allocator; + private final NettyDataBufferFactory dataBufferFactory; private final HttpMethod httpMethod; @@ -52,17 +52,17 @@ public class RxNettyClientHttpRequest extends AbstractClientHttpRequest { private Observable body; - - public RxNettyClientHttpRequest(HttpMethod httpMethod, URI uri, HttpHeaders headers, NettyDataBufferAllocator allocator) { + public RxNettyClientHttpRequest(HttpMethod httpMethod, URI uri, HttpHeaders headers, + NettyDataBufferFactory dataBufferFactory) { super(headers); this.httpMethod = httpMethod; this.uri = uri; - this.allocator = allocator; + this.dataBufferFactory = dataBufferFactory; } @Override - public DataBufferAllocator allocator() { - return this.allocator; + public DataBufferFactory dataBufferFactory() { + return this.dataBufferFactory; } /** @@ -80,7 +80,7 @@ public class RxNettyClientHttpRequest extends AbstractClientHttpRequest { public Mono setBody(Publisher body) { this.body = RxJava1ObservableConverter.from(Flux.from(body) - .map(b -> allocator.wrap(b.asByteBuffer()).getNativeBuffer())); + .map(b -> dataBufferFactory.wrap(b.asByteBuffer()).getNativeBuffer())); return Mono.empty(); } @@ -126,8 +126,8 @@ public class RxNettyClientHttpRequest extends AbstractClientHttpRequest { } }) .flatMap(resp -> resp) - .next() - .map(response -> new RxNettyClientHttpResponse(response, this.allocator)); + .next().map(response -> new RxNettyClientHttpResponse(response, + this.dataBufferFactory)); } catch (IllegalArgumentException exc) { return Mono.error(exc); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpResponse.java index 9077bdc0f5..4495d134f5 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpResponse.java @@ -24,7 +24,7 @@ import reactor.core.converter.RxJava1ObservableConverter; import reactor.core.publisher.Flux; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.NettyDataBufferAllocator; +import org.springframework.core.io.buffer.NettyDataBufferFactory; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseCookie; @@ -46,14 +46,14 @@ public class RxNettyClientHttpResponse implements ClientHttpResponse { private final MultiValueMap cookies; - private final NettyDataBufferAllocator allocator; + private final NettyDataBufferFactory dataBufferFactory; public RxNettyClientHttpResponse(HttpClientResponse response, - NettyDataBufferAllocator allocator) { + NettyDataBufferFactory dataBufferFactory) { Assert.notNull("'request', request must not be null"); - Assert.notNull(allocator, "'allocator' must not be null"); - this.allocator = allocator; + Assert.notNull(dataBufferFactory, "'dataBufferFactory' must not be null"); + this.dataBufferFactory = dataBufferFactory; this.response = response; this.headers = new HttpHeaders(); this.response.headerIterator().forEachRemaining(e -> this.headers.set(e.getKey().toString(), e.getValue().toString())); @@ -84,7 +84,8 @@ public class RxNettyClientHttpResponse implements ClientHttpResponse { @Override public Flux getBody() { - return RxJava1ObservableConverter.from(this.response.getContent().map(allocator::wrap)); + return RxJava1ObservableConverter + .from(this.response.getContent().map(dataBufferFactory::wrap)); } @Override diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyHttpClientRequestFactory.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyHttpClientRequestFactory.java index f96e97a0c2..0b935af812 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyHttpClientRequestFactory.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyHttpClientRequestFactory.java @@ -18,7 +18,7 @@ package org.springframework.http.client.reactive; import java.net.URI; -import org.springframework.core.io.buffer.NettyDataBufferAllocator; +import org.springframework.core.io.buffer.NettyDataBufferFactory; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.util.Assert; @@ -30,10 +30,10 @@ import org.springframework.util.Assert; */ public class RxNettyHttpClientRequestFactory implements ClientHttpRequestFactory { - private final NettyDataBufferAllocator allocator; + private final NettyDataBufferFactory dataBufferFactory; - public RxNettyHttpClientRequestFactory(NettyDataBufferAllocator allocator) { - this.allocator = allocator; + public RxNettyHttpClientRequestFactory(NettyDataBufferFactory dataBufferFactory) { + this.dataBufferFactory = dataBufferFactory; } @Override @@ -42,6 +42,7 @@ public class RxNettyHttpClientRequestFactory implements ClientHttpRequestFactory Assert.notNull(uri, "request URI is required"); Assert.notNull(headers, "request headers are required"); - return new RxNettyClientHttpRequest(httpMethod, uri, headers, this.allocator); + return new RxNettyClientHttpRequest(httpMethod, uri, headers, + this.dataBufferFactory); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/CodecHttpMessageConverter.java b/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/CodecHttpMessageConverter.java index 4444a42619..3544bf0531 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/CodecHttpMessageConverter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/CodecHttpMessageConverter.java @@ -28,7 +28,7 @@ import org.springframework.core.ResolvableType; import org.springframework.core.codec.Decoder; import org.springframework.core.codec.Encoder; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferAllocator; +import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ReactiveHttpInputMessage; @@ -128,9 +128,9 @@ public class CodecHttpMessageConverter implements HttpMessageConverter { if (headers.getContentType() == null) { headers.setContentType(contentType); } - DataBufferAllocator allocator = outputMessage.allocator(); + DataBufferFactory dataBufferFactory = outputMessage.dataBufferFactory(); Flux body = - this.encoder.encode(inputStream, allocator, type, contentType); + this.encoder.encode(inputStream, dataBufferFactory, type, contentType); return outputMessage.setBody(body); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java index 57d03643c0..5700146307 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java @@ -25,7 +25,7 @@ import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferAllocator; +import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseCookie; import org.springframework.util.Assert; @@ -55,19 +55,19 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse { private final List>> beforeCommitActions = new ArrayList<>(4); - private final DataBufferAllocator allocator; + private final DataBufferFactory dataBufferFactory; - public AbstractServerHttpResponse(DataBufferAllocator allocator) { - Assert.notNull(allocator, "'allocator' must not be null"); + public AbstractServerHttpResponse(DataBufferFactory dataBufferFactory) { + Assert.notNull(dataBufferFactory, "'dataBufferFactory' must not be null"); - this.allocator = allocator; + this.dataBufferFactory = dataBufferFactory; this.headers = new HttpHeaders(); this.cookies = new LinkedMultiValueMap(); } @Override - public final DataBufferAllocator allocator() { - return this.allocator; + public final DataBufferFactory dataBufferFactory() { + return this.dataBufferFactory; } @Override diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorHttpHandlerAdapter.java index 24358893b4..a582963d4b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorHttpHandlerAdapter.java @@ -21,7 +21,7 @@ import reactor.core.publisher.Mono; import reactor.io.ipc.ChannelHandler; import reactor.io.netty.http.HttpChannel; -import org.springframework.core.io.buffer.NettyDataBufferAllocator; +import org.springframework.core.io.buffer.NettyDataBufferFactory; import org.springframework.util.Assert; /** @@ -39,13 +39,13 @@ public class ReactorHttpHandlerAdapter @Override public Mono apply(HttpChannel channel) { - NettyDataBufferAllocator allocator = - new NettyDataBufferAllocator(channel.delegate().alloc()); + NettyDataBufferFactory dataBufferFactory = + new NettyDataBufferFactory(channel.delegate().alloc()); ReactorServerHttpRequest adaptedRequest = - new ReactorServerHttpRequest(channel, allocator); + new ReactorServerHttpRequest(channel, dataBufferFactory); ReactorServerHttpResponse adaptedResponse = - new ReactorServerHttpResponse(channel, allocator); + new ReactorServerHttpResponse(channel, dataBufferFactory); return this.httpHandler.handle(adaptedRequest, adaptedResponse); } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java index da38767324..bdd587c0ca 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.http.server.reactive; import java.net.URI; @@ -23,7 +24,7 @@ import reactor.core.publisher.Flux; import reactor.io.netty.http.HttpChannel; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.NettyDataBufferAllocator; +import org.springframework.core.io.buffer.NettyDataBufferFactory; import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -40,14 +41,14 @@ public class ReactorServerHttpRequest extends AbstractServerHttpRequest { private final HttpChannel channel; - private final NettyDataBufferAllocator allocator; + private final NettyDataBufferFactory dataBufferFactory; public ReactorServerHttpRequest(HttpChannel request, - NettyDataBufferAllocator allocator) { + NettyDataBufferFactory dataBufferFactory) { Assert.notNull("'request' must not be null"); - Assert.notNull(allocator, "'allocator' must not be null"); + Assert.notNull(dataBufferFactory, "'dataBufferFactory' must not be null"); this.channel = request; - this.allocator = allocator; + this.dataBufferFactory = dataBufferFactory; } @@ -90,7 +91,7 @@ public class ReactorServerHttpRequest extends AbstractServerHttpRequest { public Flux getBody() { return this.channel.receive() .retain() //FIXME Rogue reference holding - .map(allocator::wrap); + .map(dataBufferFactory::wrap); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java index 361e4e9630..a132f635aa 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java @@ -29,7 +29,7 @@ import reactor.core.publisher.Mono; import reactor.io.netty.http.HttpChannel; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferAllocator; +import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.NettyDataBuffer; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseCookie; @@ -48,8 +48,8 @@ public class ReactorServerHttpResponse extends AbstractServerHttpResponse private final HttpChannel channel; public ReactorServerHttpResponse(HttpChannel response, - DataBufferAllocator allocator) { - super(allocator); + DataBufferFactory dataBufferFactory) { + super(dataBufferFactory); Assert.notNull("'response' must not be null."); this.channel = response; } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyHttpHandlerAdapter.java index a3ec1909b1..6d0b8cb720 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyHttpHandlerAdapter.java @@ -24,7 +24,7 @@ import org.reactivestreams.Publisher; import reactor.core.converter.RxJava1ObservableConverter; import rx.Observable; -import org.springframework.core.io.buffer.NettyDataBufferAllocator; +import org.springframework.core.io.buffer.NettyDataBufferFactory; import org.springframework.util.Assert; /** @@ -41,13 +41,13 @@ public class RxNettyHttpHandlerAdapter implements RequestHandler handle(HttpServerRequest request, HttpServerResponse response) { - NettyDataBufferAllocator allocator = - new NettyDataBufferAllocator(response.unsafeNettyChannel().alloc()); + NettyDataBufferFactory dataBufferFactory = + new NettyDataBufferFactory(response.unsafeNettyChannel().alloc()); RxNettyServerHttpRequest adaptedRequest = - new RxNettyServerHttpRequest(request, allocator); + new RxNettyServerHttpRequest(request, dataBufferFactory); RxNettyServerHttpResponse adaptedResponse = - new RxNettyServerHttpResponse(response, allocator); + new RxNettyServerHttpResponse(response, dataBufferFactory); Publisher result = this.httpHandler.handle(adaptedRequest, adaptedResponse); return RxJava1ObservableConverter.from(result); } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java index dab7c23f83..cd07391ade 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java @@ -27,7 +27,7 @@ import reactor.core.publisher.Flux; import rx.Observable; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.NettyDataBufferAllocator; +import org.springframework.core.io.buffer.NettyDataBufferFactory; import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -45,13 +45,13 @@ public class RxNettyServerHttpRequest extends AbstractServerHttpRequest { private final HttpServerRequest request; - private final NettyDataBufferAllocator allocator; + private final NettyDataBufferFactory dataBufferFactory; public RxNettyServerHttpRequest(HttpServerRequest request, - NettyDataBufferAllocator allocator) { + NettyDataBufferFactory dataBufferFactory) { Assert.notNull("'request', request must not be null"); - Assert.notNull(allocator, "'allocator' must not be null"); - this.allocator = allocator; + Assert.notNull(dataBufferFactory, "'dataBufferFactory' must not be null"); + this.dataBufferFactory = dataBufferFactory; this.request = request; } @@ -93,7 +93,8 @@ public class RxNettyServerHttpRequest extends AbstractServerHttpRequest { @Override public Flux getBody() { - Observable content = this.request.getContent().map(allocator::wrap); + Observable content = + this.request.getContent().map(dataBufferFactory::wrap); content = content.concatWith(Observable.empty()); // See GH issue #58 return RxJava1ObservableConverter.from(content); } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java index 7fa7aac5bb..bb6f571526 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java @@ -29,7 +29,7 @@ import rx.Observable; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.NettyDataBuffer; -import org.springframework.core.io.buffer.NettyDataBufferAllocator; +import org.springframework.core.io.buffer.NettyDataBufferFactory; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseCookie; import org.springframework.util.Assert; @@ -45,8 +45,8 @@ public class RxNettyServerHttpResponse extends AbstractServerHttpResponse { private final HttpServerResponse response; public RxNettyServerHttpResponse(HttpServerResponse response, - NettyDataBufferAllocator allocator) { - super(allocator); + NettyDataBufferFactory dataBufferFactory) { + super(dataBufferFactory); Assert.notNull("'response', response must not be null."); this.response = response; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java index f65ec1a16a..0c463bbda2 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java @@ -37,8 +37,8 @@ import reactor.core.publisher.Mono; import reactor.core.util.BackpressureUtils; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferAllocator; -import org.springframework.core.io.buffer.DefaultDataBufferAllocator; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.core.io.buffer.support.DataBufferUtils; import org.springframework.http.HttpStatus; import org.springframework.util.Assert; @@ -59,7 +59,7 @@ public class ServletHttpHandlerAdapter extends HttpServlet { // Servlet is based on blocking I/O, hence the usage of non-direct, heap-based buffers // (i.e. 'false' as constructor argument) - private DataBufferAllocator allocator = new DefaultDataBufferAllocator(false); + private DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory(false); private int bufferSize = DEFAULT_BUFFER_SIZE; @@ -69,9 +69,9 @@ public class ServletHttpHandlerAdapter extends HttpServlet { this.handler = handler; } - public void setAllocator(DataBufferAllocator allocator) { - Assert.notNull(allocator, "'allocator' must not be null"); - this.allocator = allocator; + public void setDataBufferFactory(DataBufferFactory dataBufferFactory) { + Assert.notNull(dataBufferFactory, "'dataBufferFactory' must not be null"); + this.dataBufferFactory = dataBufferFactory; } public void setBufferSize(int bufferSize) { @@ -87,7 +87,7 @@ public class ServletHttpHandlerAdapter extends HttpServlet { ServletAsyncContextSynchronizer synchronizer = new ServletAsyncContextSynchronizer(context); RequestBodyPublisher requestBody = - new RequestBodyPublisher(synchronizer, allocator, bufferSize); + new RequestBodyPublisher(synchronizer, dataBufferFactory, bufferSize); requestBody.registerListener(); ServletServerHttpRequest request = new ServletServerHttpRequest(servletRequest, requestBody); @@ -96,7 +96,7 @@ public class ServletHttpHandlerAdapter extends HttpServlet { new ResponseBodySubscriber(synchronizer, bufferSize); responseBody.registerListener(); ServletServerHttpResponse response = - new ServletServerHttpResponse(servletResponse, allocator, + new ServletServerHttpResponse(servletResponse, dataBufferFactory, publisher -> Mono .from(subscriber -> publisher.subscribe(responseBody))); @@ -149,14 +149,14 @@ public class ServletHttpHandlerAdapter extends HttpServlet { private final ServletAsyncContextSynchronizer synchronizer; - private final DataBufferAllocator allocator; + private final DataBufferFactory dataBufferFactory; private final byte[] buffer; public RequestBodyPublisher(ServletAsyncContextSynchronizer synchronizer, - DataBufferAllocator allocator, int bufferSize) { + DataBufferFactory dataBufferFactory, int bufferSize) { this.synchronizer = synchronizer; - this.allocator = allocator; + this.dataBufferFactory = dataBufferFactory; this.buffer = new byte[bufferSize]; } @@ -204,7 +204,7 @@ public class ServletHttpHandlerAdapter extends HttpServlet { break; } else if (read > 0) { - DataBuffer dataBuffer = allocator.allocateBuffer(read); + DataBuffer dataBuffer = dataBufferFactory.allocateBuffer(read); dataBuffer.write(buffer, 0, read); publishOnNext(dataBuffer); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java index 17e69de735..59e3ea26d5 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java @@ -29,7 +29,7 @@ import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferAllocator; +import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseCookie; @@ -49,9 +49,9 @@ public class ServletServerHttpResponse extends AbstractServerHttpResponse { private final Function, Mono> responseBodyWriter; public ServletServerHttpResponse(HttpServletResponse response, - DataBufferAllocator allocator, + DataBufferFactory dataBufferFactory, Function, Mono> responseBodyWriter) { - super(allocator); + super(dataBufferFactory); Assert.notNull(response, "'response' must not be null"); Assert.notNull(responseBodyWriter, "'responseBodyWriter' must not be null"); this.response = response; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java index 05cf650a5e..f0ed1d5a99 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java @@ -34,7 +34,7 @@ import reactor.core.publisher.Mono; import reactor.core.util.BackpressureUtils; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferAllocator; +import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.util.Assert; /** @@ -48,22 +48,22 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle private final HttpHandler delegate; - // TODO: use UndertowDBA when introduced - private final DataBufferAllocator allocator; + private final DataBufferFactory dataBufferFactory; public UndertowHttpHandlerAdapter(HttpHandler delegate, - DataBufferAllocator allocator) { + DataBufferFactory dataBufferFactory) { Assert.notNull(delegate, "'delegate' is required"); - Assert.notNull(allocator, "'allocator' must not be null"); + Assert.notNull(dataBufferFactory, "'dataBufferFactory' must not be null"); this.delegate = delegate; - this.allocator = allocator; + this.dataBufferFactory = dataBufferFactory; } @Override public void handleRequest(HttpServerExchange exchange) throws Exception { - RequestBodyPublisher requestBody = new RequestBodyPublisher(exchange, allocator); + RequestBodyPublisher requestBody = + new RequestBodyPublisher(exchange, dataBufferFactory); requestBody.registerListener(); ServerHttpRequest request = new UndertowServerHttpRequest(exchange, requestBody); @@ -74,7 +74,7 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle ServerHttpResponse response = new UndertowServerHttpResponse(exchange, responseChannel, publisher -> Mono.from(subscriber -> publisher.subscribe(responseBody)), - allocator); + dataBufferFactory); this.delegate.handle(request, response).subscribe(new Subscriber() { @@ -115,16 +115,16 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle private final StreamSourceChannel requestChannel; - private final DataBufferAllocator allocator; + private final DataBufferFactory dataBufferFactory; private final PooledByteBuffer pooledByteBuffer; public RequestBodyPublisher(HttpServerExchange exchange, - DataBufferAllocator allocator) { + DataBufferFactory dataBufferFactory) { this.requestChannel = exchange.getRequestChannel(); this.pooledByteBuffer = exchange.getConnection().getByteBufferPool().allocate(); - this.allocator = allocator; + this.dataBufferFactory = dataBufferFactory; } public void registerListener() { @@ -175,7 +175,7 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle } else { byteBuffer.flip(); - DataBuffer dataBuffer = allocator.wrap(byteBuffer); + DataBuffer dataBuffer = dataBufferFactory.wrap(byteBuffer); publishOnNext(dataBuffer); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java index 67dd0a893c..dd7507a78e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java @@ -33,7 +33,7 @@ import org.xnio.channels.StreamSinkChannel; import reactor.core.publisher.Mono; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferAllocator; +import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseCookie; import org.springframework.http.ZeroCopyHttpOutputMessage; @@ -57,8 +57,8 @@ public class UndertowServerHttpResponse extends AbstractServerHttpResponse public UndertowServerHttpResponse(HttpServerExchange exchange, StreamSinkChannel responseChannel, Function, Mono> responseBodyWriter, - DataBufferAllocator allocator) { - super(allocator); + DataBufferFactory dataBufferFactory) { + super(dataBufferFactory); Assert.notNull(exchange, "'exchange' is required."); Assert.notNull(responseChannel, "'responseChannel' must not be null"); Assert.notNull(responseBodyWriter, "'responseBodyWriter' must not be null"); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/UndertowHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/UndertowHttpServer.java index 7226e505d9..66d92e2c25 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/UndertowHttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/UndertowHttpServer.java @@ -19,8 +19,8 @@ package org.springframework.http.server.reactive.boot; import io.undertow.Undertow; import io.undertow.server.HttpHandler; -import org.springframework.core.io.buffer.DataBufferAllocator; -import org.springframework.core.io.buffer.DefaultDataBufferAllocator; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.server.reactive.UndertowHttpHandlerAdapter; import org.springframework.util.Assert; @@ -31,18 +31,19 @@ public class UndertowHttpServer extends HttpServerSupport implements HttpServer private Undertow server; - private DataBufferAllocator allocator = new DefaultDataBufferAllocator(); + private DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory(); private boolean running; - public void setAllocator(DataBufferAllocator allocator) { - this.allocator = allocator; + public void setDataBufferFactory(DataBufferFactory dataBufferFactory) { + this.dataBufferFactory = dataBufferFactory; } @Override public void afterPropertiesSet() throws Exception { Assert.notNull(getHttpHandler()); - HttpHandler handler = new UndertowHttpHandlerAdapter(getHttpHandler(), allocator); + HttpHandler handler = + new UndertowHttpHandlerAdapter(getHttpHandler(), dataBufferFactory); int port = (getPort() != -1 ? getPort() : 8080); this.server = Undertow.builder().addHttpListener(port, "localhost") .setHandler(handler).build(); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultHttpRequestBuilder.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultHttpRequestBuilder.java index cdbe4c9ed2..d57db2042c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultHttpRequestBuilder.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultHttpRequestBuilder.java @@ -16,7 +16,6 @@ package org.springframework.web.client.reactive; - import java.net.URI; import java.util.ArrayList; import java.util.Arrays; @@ -30,7 +29,6 @@ import reactor.core.publisher.Mono; import org.springframework.core.ResolvableType; import org.springframework.core.codec.Encoder; -import org.springframework.core.io.buffer.DataBufferAllocator; import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -150,10 +148,9 @@ public class DefaultHttpRequestBuilder implements HttpRequestBuilder { .findFirst(); if (messageEncoder.isPresent()) { - DataBufferAllocator allocator = request.allocator(); request.setBody(messageEncoder.get() - .encode(this.contentPublisher, allocator, requestBodyType, - mediaType)); + .encode(this.contentPublisher, request.dataBufferFactory(), + requestBodyType, mediaType)); } else { throw new WebClientException("Can't write request body " + diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/AbstractView.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/AbstractView.java index 5593926007..ecf8ab2c62 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/AbstractView.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/AbstractView.java @@ -13,13 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.web.reactive.result.view; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Optional; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -28,8 +28,8 @@ import reactor.core.publisher.Flux; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferAllocator; -import org.springframework.core.io.buffer.DefaultDataBufferAllocator; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.MediaType; import org.springframework.ui.ModelMap; import org.springframework.util.Assert; @@ -48,7 +48,7 @@ public abstract class AbstractView implements View, ApplicationContextAware { private final List mediaTypes = new ArrayList<>(4); - private DataBufferAllocator bufferAllocator = new DefaultDataBufferAllocator(); + private DataBufferFactory bufferAllocator = new DefaultDataBufferFactory(); private ApplicationContext applicationContext; @@ -79,19 +79,19 @@ public abstract class AbstractView implements View, ApplicationContextAware { } /** - * Configure the {@link DataBufferAllocator} to use for write I/O. - *

    By default this is set to {@link DefaultDataBufferAllocator}. - * @param bufferAllocator the allocator to use + * Configure the {@link DataBufferFactory} to use for write I/O. + *

    By default this is set to {@link DefaultDataBufferFactory}. + * @param bufferAllocator the factory to use */ - public void setBufferAllocator(DataBufferAllocator bufferAllocator) { + public void setBufferAllocator(DataBufferFactory bufferAllocator) { Assert.notNull(bufferAllocator, "'bufferAllocator' is required."); this.bufferAllocator = bufferAllocator; } /** - * Return the configured buffer allocator, never {@code null}. + * Return the configured buffer factory, never {@code null}. */ - public DataBufferAllocator getBufferAllocator() { + public DataBufferFactory getBufferAllocator() { return this.bufferAllocator; } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolverSupport.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolverSupport.java index fb4d2a0f72..9d334dbaba 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolverSupport.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolverSupport.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.web.reactive.result.view; import java.util.ArrayList; @@ -21,8 +22,8 @@ import java.util.List; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.core.Ordered; -import org.springframework.core.io.buffer.DataBufferAllocator; -import org.springframework.core.io.buffer.DefaultDataBufferAllocator; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.MediaType; import org.springframework.util.Assert; @@ -39,7 +40,7 @@ public abstract class ViewResolverSupport implements ApplicationContextAware, Or private List mediaTypes = new ArrayList<>(4); - private DataBufferAllocator bufferAllocator = new DefaultDataBufferAllocator(); + private DataBufferFactory bufferAllocator = new DefaultDataBufferFactory(); private ApplicationContext applicationContext; @@ -71,19 +72,19 @@ public abstract class ViewResolverSupport implements ApplicationContextAware, Or } /** - * Configure the {@link DataBufferAllocator} to use for write I/O. - *

    By default this is set to {@link DefaultDataBufferAllocator}. - * @param bufferAllocator the allocator to use + * Configure the {@link DataBufferFactory} to use for write I/O. + *

    By default this is set to {@link DefaultDataBufferFactory}. + * @param bufferAllocator the factory to use */ - public void setBufferAllocator(DataBufferAllocator bufferAllocator) { + public void setBufferAllocator(DataBufferFactory bufferAllocator) { Assert.notNull(bufferAllocator, "'bufferAllocator' is required."); this.bufferAllocator = bufferAllocator; } /** - * Return the configured buffer allocator, never {@code null}. + * Return the configured buffer factory, never {@code null}. */ - public DataBufferAllocator getBufferAllocator() { + public DataBufferFactory getBufferAllocator() { return this.bufferAllocator; } diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ByteBufferEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ByteBufferEncoderTests.java index 0a1b904dc9..611662dad9 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ByteBufferEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ByteBufferEncoderTests.java @@ -61,7 +61,7 @@ public class ByteBufferEncoderTests extends AbstractDataBufferAllocatingTestCase Flux source = Flux.just(ByteBuffer.wrap(fooBytes), ByteBuffer.wrap(barBytes)); - Flux output = this.encoder.encode(source, this.allocator, + Flux output = this.encoder.encode(source, this.dataBufferFactory, ResolvableType.forClassWithGenerics(Publisher.class, ByteBuffer.class), null); TestSubscriber testSubscriber = new TestSubscriber<>(); diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonEncoderTests.java index 676769cbb5..2523899e57 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonEncoderTests.java @@ -50,7 +50,8 @@ public class JacksonJsonEncoderTests extends AbstractDataBufferAllocatingTestCas public void write() { Flux source = Flux.just(new Pojo("foofoo", "barbar"), new Pojo("foofoofoo", "barbarbar")); - Flux output = this.encoder.encode(source, this.allocator, null, null); + Flux output = + this.encoder.encode(source, this.dataBufferFactory, null, null); TestSubscriber testSubscriber = new TestSubscriber<>(); testSubscriber.bindTo(output). diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2EncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2EncoderTests.java index 56d2a7b6a2..8d956741c0 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2EncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2EncoderTests.java @@ -70,8 +70,8 @@ public class Jaxb2EncoderTests extends AbstractDataBufferAllocatingTestCase { @Test public void encode() { Flux source = Flux.just(new Pojo("foofoo", "barbar"), new Pojo("foofoofoo", "barbarbar")); - Flux output = this.encoder - .encode(source, this.allocator, ResolvableType.forClass(Pojo.class), + Flux output = this.encoder.encode(source, this.dataBufferFactory, + ResolvableType.forClass(Pojo.class), MediaType.APPLICATION_XML); TestSubscriber testSubscriber = new TestSubscriber<>(); testSubscriber.bindTo(output).assertValuesWith(dataBuffer -> { diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ResourceEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ResourceEncoderTests.java index c3d67320df..19b4c3bec9 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ResourceEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ResourceEncoderTests.java @@ -62,8 +62,8 @@ public class ResourceEncoderTests extends AbstractDataBufferAllocatingTestCase { Mono source = Mono.just(resource); - Flux output = this.encoder - .encode(source, this.allocator, ResolvableType.forClass(Resource.class), + Flux output = this.encoder.encode(source, this.dataBufferFactory, + ResolvableType.forClass(Resource.class), null); TestSubscriber testSubscriber = new TestSubscriber<>(); diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringEncoderTests.java index 20e642d934..eb3062e8bc 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringEncoderTests.java @@ -59,7 +59,7 @@ public class StringEncoderTests extends AbstractDataBufferAllocatingTestCase { @Test public void write() throws InterruptedException { Flux output = Flux.from( - this.encoder.encode(Flux.just("foo"), this.allocator, null, null)) + this.encoder.encode(Flux.just("foo"), this.dataBufferFactory, null, null)) .map(chunk -> { byte[] b = new byte[chunk.readableByteCount()]; chunk.read(b); diff --git a/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/AbstractDataBufferAllocatingTestCase.java b/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/AbstractDataBufferAllocatingTestCase.java index 958bf947e0..8afe4d5abf 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/AbstractDataBufferAllocatingTestCase.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/AbstractDataBufferAllocatingTestCase.java @@ -37,28 +37,28 @@ import static org.junit.Assert.assertEquals; public abstract class AbstractDataBufferAllocatingTestCase { @Parameterized.Parameter - public DataBufferAllocator allocator; + public DataBufferFactory dataBufferFactory; @Parameterized.Parameters(name = "{0}") - public static Object[][] allocators() { + public static Object[][] dataBufferFactories() { return new Object[][]{ - {new NettyDataBufferAllocator(new UnpooledByteBufAllocator(true))}, - {new NettyDataBufferAllocator(new UnpooledByteBufAllocator(false))}, - {new NettyDataBufferAllocator(new PooledByteBufAllocator(true))}, - {new NettyDataBufferAllocator(new PooledByteBufAllocator(false))}, - {new DefaultDataBufferAllocator(true)}, - {new DefaultDataBufferAllocator(false)} + {new NettyDataBufferFactory(new UnpooledByteBufAllocator(true))}, + {new NettyDataBufferFactory(new UnpooledByteBufAllocator(false))}, + {new NettyDataBufferFactory(new PooledByteBufAllocator(true))}, + {new NettyDataBufferFactory(new PooledByteBufAllocator(false))}, + {new DefaultDataBufferFactory(true)}, + {new DefaultDataBufferFactory(false)} }; } protected DataBuffer createDataBuffer(int capacity) { - return this.allocator.allocateBuffer(capacity); + return this.dataBufferFactory.allocateBuffer(capacity); } protected DataBuffer stringBuffer(String value) { byte[] bytes = value.getBytes(StandardCharsets.UTF_8); - DataBuffer buffer = this.allocator.allocateBuffer(bytes.length); + DataBuffer buffer = this.dataBufferFactory.allocateBuffer(bytes.length); buffer.write(bytes); return buffer; } diff --git a/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/PooledDataBufferTests.java b/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/PooledDataBufferTests.java index c0f4ae591f..1acc24fa0e 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/PooledDataBufferTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/PooledDataBufferTests.java @@ -32,20 +32,20 @@ import static org.junit.Assert.assertTrue; public class PooledDataBufferTests { @Parameterized.Parameter - public DataBufferAllocator allocator; + public DataBufferFactory dataBufferFactory; @Parameterized.Parameters(name = "{0}") public static Object[][] buffers() { return new Object[][]{ - {new NettyDataBufferAllocator(new UnpooledByteBufAllocator(true))}, - {new NettyDataBufferAllocator(new UnpooledByteBufAllocator(false))}, - {new NettyDataBufferAllocator(new PooledByteBufAllocator(true))}, - {new NettyDataBufferAllocator(new PooledByteBufAllocator(false))}}; + {new NettyDataBufferFactory(new UnpooledByteBufAllocator(true))}, + {new NettyDataBufferFactory(new UnpooledByteBufAllocator(false))}, + {new NettyDataBufferFactory(new PooledByteBufAllocator(true))}, + {new NettyDataBufferFactory(new PooledByteBufAllocator(false))}}; } private PooledDataBuffer createDataBuffer(int capacity) { - return (PooledDataBuffer) allocator.allocateBuffer(capacity); + return (PooledDataBuffer) dataBufferFactory.allocateBuffer(capacity); } @Test diff --git a/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/support/DataBufferTestUtilsTests.java b/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/support/DataBufferTestUtilsTests.java index 05fa82283a..6c5012e5a5 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/support/DataBufferTestUtilsTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/support/DataBufferTestUtilsTests.java @@ -33,7 +33,7 @@ public class DataBufferTestUtilsTests extends AbstractDataBufferAllocatingTestCa @Test public void dumpBytes() { - DataBuffer buffer = this.allocator.allocateBuffer(4); + DataBuffer buffer = this.dataBufferFactory.allocateBuffer(4); byte[] source = {'a', 'b', 'c', 'd'}; buffer.write(source); @@ -46,7 +46,7 @@ public class DataBufferTestUtilsTests extends AbstractDataBufferAllocatingTestCa @Test public void dumpString() { - DataBuffer buffer = this.allocator.allocateBuffer(4); + DataBuffer buffer = this.dataBufferFactory.allocateBuffer(4); String source = "abcd"; buffer.write(source.getBytes(StandardCharsets.UTF_8)); diff --git a/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/support/DataBufferUtilsTests.java b/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/support/DataBufferUtilsTests.java index e00aa4125b..22e123cbf3 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/support/DataBufferUtilsTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/support/DataBufferUtilsTests.java @@ -45,7 +45,7 @@ public class DataBufferUtilsTests extends AbstractDataBufferAllocatingTestCase { .toURI(); FileChannel channel = FileChannel.open(Paths.get(uri), StandardOpenOption.READ); - Flux flux = DataBufferUtils.read(channel, this.allocator, 4); + Flux flux = DataBufferUtils.read(channel, this.dataBufferFactory, 4); TestSubscriber testSubscriber = new TestSubscriber<>(); testSubscriber.bindTo(flux). @@ -63,7 +63,7 @@ public class DataBufferUtilsTests extends AbstractDataBufferAllocatingTestCase { .toURI(); FileChannel channel = FileChannel.open(Paths.get(uri), StandardOpenOption.READ); - Flux flux = DataBufferUtils.read(channel, this.allocator, 3); + Flux flux = DataBufferUtils.read(channel, this.dataBufferFactory, 3); TestSubscriber testSubscriber = new TestSubscriber<>(); testSubscriber.bindTo(flux). @@ -81,7 +81,7 @@ public class DataBufferUtilsTests extends AbstractDataBufferAllocatingTestCase { InputStream is = DataBufferUtilsTests.class .getResourceAsStream("DataBufferUtilsTests.txt"); - Flux flux = DataBufferUtils.read(is, this.allocator, 4); + Flux flux = DataBufferUtils.read(is, this.dataBufferFactory, 4); TestSubscriber testSubscriber = new TestSubscriber<>(); testSubscriber.bindTo(flux). diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java index 69658622bc..54c922baec 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java @@ -21,14 +21,14 @@ import java.time.Duration; import org.hamcrest.Matchers; import org.junit.Test; +import reactor.core.publisher.Computations; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import reactor.core.publisher.Computations; import reactor.core.scheduler.Scheduler; import reactor.core.scheduler.Timer; -import org.springframework.core.io.buffer.DataBufferAllocator; -import org.springframework.core.io.buffer.DefaultDataBufferAllocator; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; import org.springframework.web.client.RestTemplate; @@ -44,7 +44,7 @@ public class AsyncIntegrationTests extends AbstractHttpHandlerIntegrationTests { private final Scheduler asyncGroup = Computations.parallel(); - private final DataBufferAllocator allocator = new DefaultDataBufferAllocator(); + private final DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory(); @Override protected AsyncHandler createHttpHandler() { @@ -69,8 +69,7 @@ public class AsyncIntegrationTests extends AbstractHttpHandlerIntegrationTests { .useTimer(Timer.global()) .delay(Duration.ofMillis(100)) .publishOn(asyncGroup) - .collect(allocator::allocateBuffer, - (buffer, str) -> buffer.write(str.getBytes()))); + .collect(dataBufferFactory::allocateBuffer, (buffer, str) -> buffer.write(str.getBytes()))); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java index 91f370a056..baae492abf 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java @@ -23,8 +23,8 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferAllocator; -import org.springframework.core.io.buffer.DefaultDataBufferAllocator; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseCookie; @@ -44,7 +44,7 @@ public class MockServerHttpResponse implements ServerHttpResponse { private Publisher body; - private DataBufferAllocator allocator = new DefaultDataBufferAllocator(); + private DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory(); @Override @@ -86,8 +86,8 @@ public class MockServerHttpResponse implements ServerHttpResponse { } @Override - public DataBufferAllocator allocator() { - return this.allocator; + public DataBufferFactory dataBufferFactory() { + return this.dataBufferFactory; } } diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/RandomHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/RandomHandlerIntegrationTests.java index 7e92d620df..8ab8aa7917 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/RandomHandlerIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/RandomHandlerIntegrationTests.java @@ -27,8 +27,8 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferAllocator; -import org.springframework.core.io.buffer.DefaultDataBufferAllocator; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; import org.springframework.http.server.reactive.boot.ReactorHttpServer; @@ -48,7 +48,7 @@ public class RandomHandlerIntegrationTests extends AbstractHttpHandlerIntegratio private final RandomHandler handler = new RandomHandler(); - private final DataBufferAllocator allocator = new DefaultDataBufferAllocator(); + private final DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory(); @Override @@ -146,7 +146,7 @@ public class RandomHandlerIntegrationTests extends AbstractHttpHandlerIntegratio private DataBuffer randomBuffer(int size) { byte[] bytes = new byte[size]; rnd.nextBytes(bytes); - DataBuffer buffer = allocator.allocateBuffer(size); + DataBuffer buffer = dataBufferFactory.allocateBuffer(size); buffer.write(bytes); return buffer; } diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java index 1d0577d5b7..049e5f5d4e 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java @@ -27,7 +27,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DefaultDataBufferAllocator; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseCookie; @@ -133,7 +133,7 @@ public class ServerHttpResponseTests { private DataBuffer wrap(String a) { - return new DefaultDataBufferAllocator().wrap(ByteBuffer.wrap(a.getBytes(UTF_8))); + return new DefaultDataBufferFactory().wrap(ByteBuffer.wrap(a.getBytes(UTF_8))); } @@ -146,7 +146,7 @@ public class ServerHttpResponseTests { private final List content = new ArrayList<>(); public TestServerHttpResponse() { - super(new DefaultDataBufferAllocator()); + super(new DefaultDataBufferFactory()); } @Override diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java index c7f84a8028..f014b1fcee 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java @@ -34,7 +34,7 @@ import org.springframework.core.codec.support.StringDecoder; import org.springframework.core.codec.support.StringEncoder; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DefaultDataBufferAllocator; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -62,9 +62,7 @@ import org.springframework.web.server.handler.FilteringWebHandler; import org.springframework.web.server.session.WebSessionManager; import static org.hamcrest.CoreMatchers.startsWith; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertThat; +import static org.junit.Assert.*; import static org.mockito.Mockito.mock; /** @@ -162,7 +160,7 @@ public class DispatcherHandlerErrorTests { public void notAcceptable() throws Exception { this.request.setUri(new URI("/request-body")); this.request.getHeaders().setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); - DataBuffer buffer = new DefaultDataBufferAllocator().allocateBuffer() + DataBuffer buffer = new DefaultDataBufferFactory().allocateBuffer() .write("body".getBytes("UTF-8")); this.request.setBody(Mono.just(buffer)); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/WebHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/WebHandlerIntegrationTests.java index 7eee8878ed..5445bf9b9f 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/WebHandlerIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/WebHandlerIntegrationTests.java @@ -28,7 +28,7 @@ import reactor.core.publisher.Mono; import org.springframework.context.support.StaticApplicationContext; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DefaultDataBufferAllocator; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.HttpStatus; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; @@ -141,7 +141,8 @@ public class WebHandlerIntegrationTests extends AbstractHttpHandlerIntegrationTe } private static DataBuffer asDataBuffer(String text) { - return new DefaultDataBufferAllocator().allocateBuffer().write(text.getBytes(StandardCharsets.UTF_8)); + return new DefaultDataBufferFactory().allocateBuffer() + .write(text.getBytes(StandardCharsets.UTF_8)); } private static class FooHandler implements WebHandler { diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java index c866052e9d..561d407395 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java @@ -53,8 +53,8 @@ import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converte import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferAllocator; -import org.springframework.core.io.buffer.DefaultDataBufferAllocator; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.RequestEntity; @@ -82,9 +82,7 @@ import org.springframework.web.reactive.result.view.freemarker.FreeMarkerConfigu import org.springframework.web.reactive.result.view.freemarker.FreeMarkerViewResolver; import org.springframework.web.server.adapter.WebHttpHandlerBuilder; -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; /** @@ -384,7 +382,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @SuppressWarnings("unused") static class FrameworkConfig { - private DataBufferAllocator allocator = new DefaultDataBufferAllocator(); + private DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory(); @Bean @@ -432,7 +430,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @Bean public ViewResolver freeMarkerViewResolver() { FreeMarkerViewResolver viewResolver = new FreeMarkerViewResolver("", ".ftl"); - viewResolver.setBufferAllocator(this.allocator); + viewResolver.setBufferAllocator(this.dataBufferFactory); return viewResolver; } @@ -485,9 +483,9 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @RequestMapping("/raw") public Publisher rawResponseBody() { - DataBufferAllocator allocator = new DefaultDataBufferAllocator(); + DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory(); JacksonJsonEncoder encoder = new JacksonJsonEncoder(); - return encoder.encode(Mono.just(new Person("Robert")), allocator, + return encoder.encode(Mono.just(new Person("Robert")), dataBufferFactory, ResolvableType.forClass(Person.class), MediaType.APPLICATION_JSON).map(DataBuffer::asByteBuffer); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java index 04f00bdd4c..eff32fb304 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.web.reactive.result.view; import java.lang.reflect.Method; @@ -40,7 +41,7 @@ import org.springframework.core.ResolvableType; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DefaultDataBufferAllocator; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.http.server.reactive.MockServerHttpRequest; @@ -55,9 +56,7 @@ import org.springframework.web.server.adapter.DefaultServerWebExchange; import org.springframework.web.server.session.DefaultWebSessionManager; import org.springframework.web.server.session.WebSessionManager; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; import static org.mockito.Mockito.mock; @@ -253,7 +252,7 @@ public class ViewResolutionResultHandlerTests { private static DataBuffer asDataBuffer(String value) { ByteBuffer byteBuffer = ByteBuffer.wrap(value.getBytes(UTF_8)); - return new DefaultDataBufferAllocator().wrap(byteBuffer); + return new DefaultDataBufferFactory().wrap(byteBuffer); } private static String asString(DataBuffer dataBuffer) { From 03fe209862e2609f9bd25725b501b9c6bf0c751a Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Fri, 27 May 2016 10:37:04 +0200 Subject: [PATCH 364/498] Renamed ReactiveHttpOutputMessage dataBufferFactory() method to bufferFactory() --- .../org/springframework/http/ReactiveHttpOutputMessage.java | 2 +- .../http/client/reactive/ReactorClientHttpRequest.java | 2 +- .../http/client/reactive/RxNettyClientHttpRequest.java | 2 +- .../http/converter/reactive/CodecHttpMessageConverter.java | 2 +- .../http/server/reactive/AbstractServerHttpResponse.java | 2 +- .../web/client/reactive/DefaultHttpRequestBuilder.java | 2 +- .../http/server/reactive/MockServerHttpResponse.java | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java b/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java index 4e84c91886..686a780583 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java @@ -54,6 +54,6 @@ public interface ReactiveHttpOutputMessage extends HttpMessage { * @return a buffer factory * @see #setBody(Publisher) */ - DataBufferFactory dataBufferFactory(); + DataBufferFactory bufferFactory(); } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java index d97c495d75..fff7f7a485 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java @@ -63,7 +63,7 @@ public class ReactorClientHttpRequest extends AbstractClientHttpRequest { } @Override - public DataBufferFactory dataBufferFactory() { + public DataBufferFactory bufferFactory() { return this.dataBufferFactory; } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpRequest.java index 46d9550c31..aa45ed7404 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpRequest.java @@ -61,7 +61,7 @@ public class RxNettyClientHttpRequest extends AbstractClientHttpRequest { } @Override - public DataBufferFactory dataBufferFactory() { + public DataBufferFactory bufferFactory() { return this.dataBufferFactory; } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/CodecHttpMessageConverter.java b/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/CodecHttpMessageConverter.java index 3544bf0531..e48716b794 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/CodecHttpMessageConverter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/CodecHttpMessageConverter.java @@ -128,7 +128,7 @@ public class CodecHttpMessageConverter implements HttpMessageConverter { if (headers.getContentType() == null) { headers.setContentType(contentType); } - DataBufferFactory dataBufferFactory = outputMessage.dataBufferFactory(); + DataBufferFactory dataBufferFactory = outputMessage.bufferFactory(); Flux body = this.encoder.encode(inputStream, dataBufferFactory, type, contentType); return outputMessage.setBody(body); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java index 5700146307..34566e8ff0 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java @@ -66,7 +66,7 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse { } @Override - public final DataBufferFactory dataBufferFactory() { + public final DataBufferFactory bufferFactory() { return this.dataBufferFactory; } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultHttpRequestBuilder.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultHttpRequestBuilder.java index d57db2042c..5293508c93 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultHttpRequestBuilder.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultHttpRequestBuilder.java @@ -149,7 +149,7 @@ public class DefaultHttpRequestBuilder implements HttpRequestBuilder { if (messageEncoder.isPresent()) { request.setBody(messageEncoder.get() - .encode(this.contentPublisher, request.dataBufferFactory(), + .encode(this.contentPublisher, request.bufferFactory(), requestBodyType, mediaType)); } else { diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java index baae492abf..3a6ea38952 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java @@ -86,7 +86,7 @@ public class MockServerHttpResponse implements ServerHttpResponse { } @Override - public DataBufferFactory dataBufferFactory() { + public DataBufferFactory bufferFactory() { return this.dataBufferFactory; } From c6ed12297f284ef8b7385683fe40e1faf4d0ef95 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Fri, 27 May 2016 15:50:41 +0200 Subject: [PATCH 365/498] Add Host property in HttpServerSupport And change the defaults to: * "0.0.0.0" for the host * any available port, starting at 8080 --- .../http/server/reactive/boot/HttpServer.java | 2 ++ .../server/reactive/boot/HttpServerSupport.java | 14 ++++++++++++++ .../http/server/reactive/boot/JettyHttpServer.java | 6 +----- .../server/reactive/boot/ReactorHttpServer.java | 4 +--- .../server/reactive/boot/RxNettyHttpServer.java | 7 ++++--- .../server/reactive/boot/TomcatHttpServer.java | 6 ++---- .../server/reactive/boot/UndertowHttpServer.java | 3 +-- 7 files changed, 25 insertions(+), 17 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/HttpServer.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/HttpServer.java index d2255b3609..e706861852 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/HttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/HttpServer.java @@ -26,6 +26,8 @@ import org.springframework.http.server.reactive.HttpHandler; */ public interface HttpServer extends InitializingBean, Lifecycle { + void setHost(String host); + void setPort(int port); void setHandler(HttpHandler handler); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/HttpServerSupport.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/HttpServerSupport.java index e498af70e6..bb710f40b4 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/HttpServerSupport.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/HttpServerSupport.java @@ -17,23 +17,37 @@ package org.springframework.http.server.reactive.boot; +import org.springframework.beans.factory.InitializingBean; import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.util.SocketUtils; /** * @author Rossen Stoyanchev */ public class HttpServerSupport { + private String host = "0.0.0.0"; + private int port = -1; private HttpHandler httpHandler; + public void setHost(String host) { + this.host = host; + } + + public String getHost() { + return host; + } public void setPort(int port) { this.port = port; } public int getPort() { + if(this.port == -1) { + this.port = SocketUtils.findAvailableTcpPort(8080); + } return this.port; } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/JettyHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/JettyHttpServer.java index f462a8ad20..51837739ce 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/JettyHttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/JettyHttpServer.java @@ -24,7 +24,6 @@ import org.eclipse.jetty.servlet.ServletHolder; import org.springframework.beans.factory.InitializingBean; import org.springframework.http.server.reactive.ServletHttpHandlerAdapter; import org.springframework.util.Assert; -import org.springframework.util.SocketUtils; /** * @author Rossen Stoyanchev @@ -44,10 +43,6 @@ public class JettyHttpServer extends HttpServerSupport implements InitializingBe @Override public void afterPropertiesSet() throws Exception { - if (getPort() == -1) { - setPort(SocketUtils.findAvailableTcpPort(8080)); - } - this.jettyServer = new Server(); Assert.notNull(getHttpHandler()); @@ -59,6 +54,7 @@ public class JettyHttpServer extends HttpServerSupport implements InitializingBe contextHandler.addServlet(servletHolder, "/"); ServerConnector connector = new ServerConnector(this.jettyServer); + connector.setHost(getHost()); connector.setPort(getPort()); this.jettyServer.addConnector(connector); } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java index b83e2d0fa0..8bb845a534 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java @@ -39,9 +39,7 @@ public class ReactorHttpServer extends HttpServerSupport Assert.notNull(getHttpHandler()); this.reactorHandler = new ReactorHttpHandlerAdapter(getHttpHandler()); - - this.reactorServer = (getPort() != -1 ? reactor.io.netty.http.HttpServer.create(getPort()) : - reactor.io.netty.http.HttpServer.create()); + this.reactorServer = reactor.io.netty.http.HttpServer.create(getHost(), getPort()); } @Override diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/RxNettyHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/RxNettyHttpServer.java index d73fe0385f..1117199684 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/RxNettyHttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/RxNettyHttpServer.java @@ -16,6 +16,8 @@ package org.springframework.http.server.reactive.boot; +import java.net.InetSocketAddress; + import io.netty.buffer.ByteBuf; import org.springframework.http.server.reactive.RxNettyHttpHandlerAdapter; @@ -38,9 +40,8 @@ public class RxNettyHttpServer extends HttpServerSupport implements HttpServer { Assert.notNull(getHttpHandler()); this.rxNettyHandler = new RxNettyHttpHandlerAdapter(getHttpHandler()); - this.rxNettyServer = (getPort() != -1 ? - io.reactivex.netty.protocol.http.server.HttpServer.newServer(getPort()) : - io.reactivex.netty.protocol.http.server.HttpServer.newServer()); + this.rxNettyServer = io.reactivex.netty.protocol.http.server.HttpServer + .newServer(new InetSocketAddress(getHost(), getPort())); } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/TomcatHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/TomcatHttpServer.java index 90c75057c8..d0170d890b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/TomcatHttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/TomcatHttpServer.java @@ -20,6 +20,7 @@ import java.io.File; import org.apache.catalina.Context; import org.apache.catalina.LifecycleException; +import org.apache.catalina.core.StandardHost; import org.apache.catalina.startup.Tomcat; import org.springframework.beans.factory.InitializingBean; @@ -46,11 +47,8 @@ public class TomcatHttpServer extends HttpServerSupport implements InitializingB @Override public void afterPropertiesSet() throws Exception { - if (getPort() == -1) { - setPort(SocketUtils.findAvailableTcpPort(8080)); - } - this.tomcatServer = new Tomcat(); + this.tomcatServer.setHostname(getHost()); this.tomcatServer.setPort(getPort()); Assert.notNull(getHttpHandler()); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/UndertowHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/UndertowHttpServer.java index 66d92e2c25..5c6048353c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/UndertowHttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/UndertowHttpServer.java @@ -44,8 +44,7 @@ public class UndertowHttpServer extends HttpServerSupport implements HttpServer Assert.notNull(getHttpHandler()); HttpHandler handler = new UndertowHttpHandlerAdapter(getHttpHandler(), dataBufferFactory); - int port = (getPort() != -1 ? getPort() : 8080); - this.server = Undertow.builder().addHttpListener(port, "localhost") + this.server = Undertow.builder().addHttpListener(getPort(), getHost()) .setHandler(handler).build(); } From f7d4688b8457f9ce3527dc70cb69c3fe3f7762a2 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 27 May 2016 13:50:04 -0400 Subject: [PATCH 366/498] Improve 404 "handler not found" handling Remove handler inserted at the end to generate error in favor of doing the same using the otherwiseIfEmpty operator. --- .../web/reactive/DispatcherHandler.java | 20 +++++-------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java index bf21007c8a..095eccfdb1 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java @@ -58,6 +58,10 @@ public class DispatcherHandler implements WebHandler, ApplicationContextAware { private static final Log logger = LogFactory.getLog(DispatcherHandler.class); + @SuppressWarnings("ThrowableInstanceNeverThrown") + private static final Exception HANDLER_NOT_FOUND_EXCEPTION = + new ResponseStatusException(HttpStatus.NOT_FOUND, "No matching handler"); + private List handlerMappings; @@ -78,7 +82,6 @@ public class DispatcherHandler implements WebHandler, ApplicationContextAware { context, HandlerMapping.class, true, false); this.handlerMappings = new ArrayList<>(mappingBeans.values()); - this.handlerMappings.add(new NotFoundHandlerMapping()); AnnotationAwareOrderComparator.sort(this.handlerMappings); Map adapterBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors( @@ -104,6 +107,7 @@ public class DispatcherHandler implements WebHandler, ApplicationContextAware { return Flux.fromIterable(this.handlerMappings) .concatMap(mapping -> mapping.getHandler(exchange)) .next() + .otherwiseIfEmpty(Mono.error(HANDLER_NOT_FOUND_EXCEPTION)) .then(handler -> invokeHandler(exchange, handler)) .then(result -> handleResult(exchange, result)); } @@ -132,18 +136,4 @@ public class DispatcherHandler implements WebHandler, ApplicationContextAware { throw new IllegalStateException("No HandlerResultHandler for " + handlerResult.getReturnValue()); } - - private static class NotFoundHandlerMapping implements HandlerMapping { - - @SuppressWarnings("ThrowableInstanceNeverThrown") - private static final Exception HANDLER_NOT_FOUND_EXCEPTION = - new ResponseStatusException(HttpStatus.NOT_FOUND, "No matching handler"); - - - @Override - public Mono getHandler(ServerWebExchange exchange) { - return Mono.error(HANDLER_NOT_FOUND_EXCEPTION); - } - } - } \ No newline at end of file From a72462cd8c17e41c62fcfc8dc7538d0d9c52f5e2 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 27 May 2016 14:48:01 -0400 Subject: [PATCH 367/498] Polish view resolution tests --- .../view/ViewResolutionResultHandler.java | 9 +- .../ViewResolutionResultHandlerTests.java | 243 ++++++++---------- 2 files changed, 113 insertions(+), 139 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java index aee5abb6e1..62ae5c6621 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java @@ -65,7 +65,6 @@ public class ViewResolutionResultHandler implements HandlerResultHandler, Ordere * @param service for converting other reactive types (e.g. rx.Single) to Mono */ public ViewResolutionResultHandler(List resolvers, ConversionService service) { - Assert.notEmpty(resolvers, "At least one ViewResolver is required."); Assert.notNull(service, "'conversionService' is required."); this.viewResolvers.addAll(resolvers); AnnotationAwareOrderComparator.sort(this.viewResolvers); @@ -183,6 +182,14 @@ public class ViewResolutionResultHandler implements HandlerResultHandler, Ordere } } + /** + * Translate the given request into a default view name. This is useful when + * the application leaves the view name unspecified. + *

    The default implementation strips the leading and trailing slash from + * the as well as any extension and uses that as the view name. + * @return the default view name to use; if {@code null} is returned + * processing will result in an IllegalStateException. + */ protected String getDefaultViewName(ServerWebExchange exchange, HandlerResult result) { String path = this.pathHelper.getLookupPathForRequest(exchange); if (path.startsWith("/")) { diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java index eff32fb304..05adf2c9f6 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java @@ -59,7 +59,6 @@ import org.springframework.web.server.session.WebSessionManager; import static org.junit.Assert.*; import static org.mockito.Mockito.mock; - /** * Unit tests for {@link ViewResolutionResultHandler}. * @author Rossen Stoyanchev @@ -71,8 +70,6 @@ public class ViewResolutionResultHandlerTests { private MockServerHttpResponse response; - private ServerWebExchange exchange; - private ModelMap model; private DefaultConversionService conversionService; @@ -80,7 +77,6 @@ public class ViewResolutionResultHandlerTests { @Before public void setUp() throws Exception { - this.exchange = createExchange("/path"); this.model = new ExtendedModelMap().addAttribute("id", "123"); this.conversionService = new DefaultConversionService(); this.conversionService.addConverter(new ReactiveStreamsToRxJava1Converter()); @@ -97,6 +93,99 @@ public class ViewResolutionResultHandlerTests { testSupports("handleSingleView", null); } + @Test + public void order() throws Exception { + TestViewResolver resolver1 = new TestViewResolver(); + TestViewResolver resolver2 = new TestViewResolver(); + resolver1.setOrder(2); + resolver2.setOrder(1); + + assertEquals(Arrays.asList(resolver2, resolver1), + new ViewResolutionResultHandler(Arrays.asList(resolver1, resolver2), this.conversionService) + .getViewResolvers()); + } + + @Test + public void viewReference() throws Exception { + Object value = new TestView("account"); + handle("/path", value, ResolvableType.forClass(View.class)); + + new TestSubscriber().bindTo(this.response.getBody()) + .assertValuesWith(buf -> assertEquals("account: {id=123}", asString(buf))); + } + + @Test + public void viewReferenceInMono() throws Exception { + Object value = Mono.just(new TestView("account")); + handle("/path", value, returnTypeFor("handleMonoView")); + + new TestSubscriber().bindTo(this.response.getBody()) + .assertValuesWith(buf -> assertEquals("account: {id=123}", asString(buf))); + } + + @Test + public void viewName() throws Exception { + Object value = "account"; + handle("/path", value, ResolvableType.forClass(String.class), new TestViewResolver("account")); + + TestSubscriber subscriber = new TestSubscriber<>(); + subscriber.bindTo(this.response.getBody()) + .assertValuesWith(buf -> assertEquals("account: {id=123}", asString(buf))); + } + + @Test + public void viewNameInMono() throws Exception { + Object value = Mono.just("account"); + handle("/path", value, returnTypeFor("handleMonoString"), new TestViewResolver("account")); + + new TestSubscriber().bindTo(this.response.getBody()) + .assertValuesWith(buf -> assertEquals("account: {id=123}", asString(buf))); + } + + @Test + public void viewNameWithMultipleResolvers() throws Exception { + String value = "profile"; + handle("/path", value, ResolvableType.forClass(String.class), + new TestViewResolver("account"), new TestViewResolver("profile")); + + new TestSubscriber().bindTo(this.response.getBody()) + .assertValuesWith(buf -> assertEquals("profile: {id=123}", asString(buf))); + } + + @Test + public void viewNameUnresolved() throws Exception { + TestSubscriber subscriber = handle("/path", "account", ResolvableType.forClass(String.class)); + + subscriber.assertNoValues(); + } + + @Test + public void viewNameIsNull() throws Exception { + ViewResolver resolver = new TestViewResolver("account"); + + handle("/account", null, ResolvableType.forClass(String.class), resolver); + new TestSubscriber().bindTo(this.response.getBody()) + .assertValuesWith(buf -> assertEquals("account: {id=123}", asString(buf))); + + handle("/account/", null, ResolvableType.forClass(String.class), resolver); + new TestSubscriber().bindTo(this.response.getBody()) + .assertValuesWith(buf -> assertEquals("account: {id=123}", asString(buf))); + + handle("/account.123", null, ResolvableType.forClass(String.class), resolver); + new TestSubscriber().bindTo(this.response.getBody()) + .assertValuesWith(buf -> assertEquals("account: {id=123}", asString(buf))); + } + + @Test + public void viewNameIsEmptyMono() throws Exception { + Object value = Mono.empty(); + handle("/account", value, returnTypeFor("handleMonoString"), new TestViewResolver("account")); + + new TestSubscriber().bindTo(this.response.getBody()) + .assertValuesWith(buf -> assertEquals("account: {id=123}", asString(buf))); + } + + private void testSupports(String methodName, Object returnValue) throws NoSuchMethodException { Method method = TestController.class.getMethod(methodName); ResolvableType returnType = ResolvableType.forMethodParameter(method, -1); @@ -106,146 +195,25 @@ public class ViewResolutionResultHandlerTests { assertTrue(handler.supports(result)); } - @Test - public void viewReference() throws Exception { - TestView view = new TestView("account"); - List resolvers = Collections.singletonList(mock(ViewResolver.class)); - ViewResolutionResultHandler handler = new ViewResolutionResultHandler(resolvers, this.conversionService); - handle(this.exchange, handler, view, ResolvableType.forClass(View.class)); + private TestSubscriber handle(String path, Object value, ResolvableType type, + ViewResolver... resolvers) throws URISyntaxException { - new TestSubscriber().bindTo(this.response.getBody()) - .assertValuesWith(buf -> assertEquals("account: {id=123}", asString(buf))); - } + List resolverList = Arrays.asList(resolvers); + HandlerResultHandler handler = new ViewResolutionResultHandler(resolverList, this.conversionService); + HandlerResult handlerResult = new HandlerResult(new Object(), value, type, this.model); - @Test - public void viewReferenceMono() throws Exception { - TestView view = new TestView("account"); - List resolvers = Collections.singletonList(mock(ViewResolver.class)); - ViewResolutionResultHandler handler = new ViewResolutionResultHandler(resolvers, this.conversionService); - handle(this.exchange, handler, Mono.just(view), methodReturnType("handleMonoView")); - - new TestSubscriber().bindTo(this.response.getBody()) - .assertValuesWith(buf -> assertEquals("account: {id=123}", asString(buf))); - } - - @Test - public void viewName() throws Exception { - TestView view = new TestView("account"); - TestViewResolver resolver = new TestViewResolver().addView(view); - List resolvers = Collections.singletonList(resolver); - ViewResolutionResultHandler handler = new ViewResolutionResultHandler(resolvers, this.conversionService); - handle(this.exchange, handler, "account", ResolvableType.forClass(String.class)); - - TestSubscriber subscriber = new TestSubscriber<>(); - subscriber.bindTo(this.response.getBody()) - .assertValuesWith(buf -> assertEquals("account: {id=123}", asString(buf))); - } - - @Test - public void viewNameMono() throws Exception { - TestView view = new TestView("account"); - TestViewResolver resolver = new TestViewResolver().addView(view); - List resolvers = Collections.singletonList(resolver); - ViewResolutionResultHandler handler = new ViewResolutionResultHandler(resolvers, this.conversionService); - handle(this.exchange, handler, Mono.just("account"), methodReturnType("handleMonoString")); - - new TestSubscriber().bindTo(this.response.getBody()) - .assertValuesWith(buf -> assertEquals("account: {id=123}", asString(buf))); - } - - @Test - public void viewNameWithMultipleResolvers() throws Exception { - TestView view1 = new TestView("account"); - TestView view2 = new TestView("profile"); - TestViewResolver resolver1 = new TestViewResolver().addView(view1); - TestViewResolver resolver2 = new TestViewResolver().addView(view2); - List resolvers = Arrays.asList(resolver1, resolver2); - ViewResolutionResultHandler handler = new ViewResolutionResultHandler(resolvers, this.conversionService); - handle(this.exchange, handler, "profile", ResolvableType.forClass(String.class)); - - new TestSubscriber().bindTo(this.response.getBody()) - .assertValuesWith(buf -> assertEquals("profile: {id=123}", asString(buf))); - } - - @Test - public void viewNameWithNoMatch() throws Exception { - List resolvers = Collections.singletonList(mock(ViewResolver.class)); - ViewResolutionResultHandler handler = new ViewResolutionResultHandler(resolvers, this.conversionService); - TestSubscriber subscriber = handle(this.exchange, handler, "account", ResolvableType.forClass(String.class)); - - subscriber.assertNoValues(); - } - - @Test - public void viewNameNotSpecified() throws Exception { - TestView view = new TestView("account"); - TestViewResolver resolver = new TestViewResolver().addView(view); - List resolvers = Collections.singletonList(resolver); - ViewResolutionResultHandler handler = new ViewResolutionResultHandler(resolvers, this.conversionService); - - ServerWebExchange exchange = createExchange("/account"); - handle(exchange, handler, null, ResolvableType.forClass(String.class)); - new TestSubscriber().bindTo(this.response.getBody()) - .assertValuesWith(buf -> assertEquals("account: {id=123}", asString(buf))); - - exchange = createExchange("/account/"); - handle(exchange, handler, null, ResolvableType.forClass(String.class)); - new TestSubscriber().bindTo(this.response.getBody()) - .assertValuesWith(buf -> assertEquals("account: {id=123}", asString(buf))); - - exchange = createExchange("/account.123"); - handle(exchange, handler, null, ResolvableType.forClass(String.class)); - new TestSubscriber().bindTo(this.response.getBody()) - .assertValuesWith(buf -> assertEquals("account: {id=123}", asString(buf))); - } - - @Test - public void viewNameMonoEmpty() throws Exception { - TestView view = new TestView("account"); - TestViewResolver resolver = new TestViewResolver().addView(view); - List resolvers = Collections.singletonList(resolver); - HandlerResultHandler handler = new ViewResolutionResultHandler(resolvers, this.conversionService); - ServerWebExchange exchange = createExchange("/account"); - handle(exchange, handler, Mono.empty(), methodReturnType("handleMonoString")); - - new TestSubscriber().bindTo(this.response.getBody()) - .assertValuesWith(buf -> assertEquals("account: {id=123}", asString(buf))); - } - - - @Test - public void ordered() throws Exception { - TestViewResolver resolver1 = new TestViewResolver(); - TestViewResolver resolver2 = new TestViewResolver(); - List resolvers = Arrays.asList(resolver1, resolver2); - - resolver1.setOrder(2); - resolver2.setOrder(1); - - ViewResolutionResultHandler resultHandler = - new ViewResolutionResultHandler(resolvers, this.conversionService); - - assertEquals(Arrays.asList(resolver2, resolver1), resultHandler.getViewResolvers()); - } - - - private ServerWebExchange createExchange(String path) throws URISyntaxException { ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI(path)); this.response = new MockServerHttpResponse(); WebSessionManager sessionManager = new DefaultWebSessionManager(); - return new DefaultServerWebExchange(request, this.response, sessionManager); - } + ServerWebExchange exchange = new DefaultServerWebExchange(request, this.response, sessionManager); - private TestSubscriber handle(ServerWebExchange exchange, HandlerResultHandler handler, - Object value, ResolvableType type) { + Mono mono = handler.handleResult(exchange, handlerResult); - HandlerResult result = new HandlerResult(new Object(), value, type, this.model); - Mono mono = handler.handleResult(exchange, result); TestSubscriber subscriber = new TestSubscriber<>(); return subscriber.bindTo(mono).await(Duration.ofSeconds(1)); } - private ResolvableType methodReturnType(String methodName, Class... args) throws NoSuchMethodException { + private ResolvableType returnTypeFor(String methodName, Class... args) throws NoSuchMethodException { Method method = TestController.class.getDeclaredMethod(methodName, args); return ResolvableType.forMethodReturnType(method); } @@ -270,6 +238,10 @@ public class ViewResolutionResultHandlerTests { private int order = Ordered.LOWEST_PRECEDENCE; + public TestViewResolver(String... viewNames) { + Arrays.stream(viewNames).forEach(name -> this.views.put(name, new TestView(name))); + } + public void setOrder(int order) { this.order = order; } @@ -279,11 +251,6 @@ public class ViewResolutionResultHandlerTests { return this.order; } - public TestViewResolver addView(TestView view) { - this.views.put(view.getName(), view); - return this; - } - @Override public Mono resolveViewName(String viewName, Locale locale) { View view = this.views.get(viewName); From 33c5a197698fdd87ff9edcafdf77d3993e35ae98 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 27 May 2016 15:18:11 -0400 Subject: [PATCH 368/498] Handle unresolved view name --- .../reactive/result/view/ViewResolutionResultHandler.java | 6 ++++++ .../result/view/ViewResolutionResultHandlerTests.java | 5 ++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java index 62ae5c6621..2ad057b546 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java @@ -151,6 +151,7 @@ public class ViewResolutionResultHandler implements HandlerResultHandler, Ordere return Flux.fromIterable(getViewResolvers()) .concatMap(resolver -> resolver.resolveViewName(viewName, locale)) .next() + .otherwiseIfEmpty(handleUnresolvedViewName(viewName)) .then(view -> { Flux body = view.render(result, null, exchange); return exchange.getResponse().setBody(body); @@ -201,4 +202,9 @@ public class ViewResolutionResultHandler implements HandlerResultHandler, Ordere return StringUtils.stripFilenameExtension(path); } + private Mono handleUnresolvedViewName(String viewName) { + return Mono.error(new IllegalStateException( + "Could not resolve view with name '" + viewName + "'.")); + } + } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java index 05adf2c9f6..ff34752123 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java @@ -154,9 +154,8 @@ public class ViewResolutionResultHandlerTests { @Test public void viewNameUnresolved() throws Exception { - TestSubscriber subscriber = handle("/path", "account", ResolvableType.forClass(String.class)); - - subscriber.assertNoValues(); + handle("/path", "account", ResolvableType.forClass(String.class)) + .assertErrorMessage("Could not resolve view with name 'account'."); } @Test From 58307ebac49336bc6923ed162fbf6f1249407dd5 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 27 May 2016 20:22:58 -0400 Subject: [PATCH 369/498] Support Model-related return values This commit adds support for Model-related return values types such as Map, Model, @ModelAttribute annotated, and non-simple types, which helps to clarify the logic in ViewResolutionResultHandler. --- .../view/ViewResolutionResultHandler.java | 146 +++++++++++--- .../ViewResolutionResultHandlerTests.java | 178 +++++++++++++----- 2 files changed, 247 insertions(+), 77 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java index 2ad057b546..d46e127ad6 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java @@ -15,22 +15,31 @@ */ package org.springframework.web.reactive.result.view; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Optional; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import org.springframework.beans.BeanUtils; +import org.springframework.core.Conventions; +import org.springframework.core.GenericTypeResolver; +import org.springframework.core.MethodParameter; import org.springframework.core.Ordered; import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.convert.ConversionService; import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.ui.Model; import org.springframework.util.Assert; import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.HandlerResultHandler; import org.springframework.web.server.ServerWebExchange; @@ -38,10 +47,22 @@ import org.springframework.web.util.HttpRequestPathHelper; /** - * {@code HandlerResultHandler} that performs view resolution by resolving a - * {@link View} instance first and then rendering the response with it. - * If the return value is a String, the configured {@link ViewResolver}s will - * be consulted to resolve that to a {@link View} instance. + * {@code HandlerResultHandler} that encapsulates the view resolution algorithm + * supporting the following return types: + *

      + *
    • String-based view name + *
    • Reference to a {@link View} + *
    • {@link Model} + *
    • {@link Map} + *
    • Return types annotated with {@code @ModelAttribute} + *
    • {@link BeanUtils#isSimpleProperty Non-simple} return types are + * treated as a model attribute + *
    + * + *

    A String-based view name is resolved through the configured + * {@link ViewResolver} instances into a {@link View} to use for rendering. + * If a view is left unspecified (e.g. by returning {@code null} or a + * model-related return value), a default view name is selected. * *

    This result handler should be ordered late relative to other result * handlers. See {@link #setOrder(int)} for more details. @@ -96,51 +117,72 @@ public class ViewResolutionResultHandler implements HandlerResultHandler, Ordere return this.order; } - // TODO: Support for Model, ModelAndView, @ModelAttribute, Object with no method annotations - @Override public boolean supports(HandlerResult result) { Class clazz = result.getReturnValueType().getRawClass(); - if (isStringOrViewReference(clazz)) { + if (hasModelAttributeAnnotation(result)) { + return true; + } + if (isSupportedType(clazz)) { return true; } if (this.conversionService.canConvert(clazz, Mono.class)) { clazz = result.getReturnValueType().getGeneric(0).getRawClass(); - return isStringOrViewReference(clazz); + return isSupportedType(clazz); } return false; } - private boolean isStringOrViewReference(Class clazz) { - return (CharSequence.class.isAssignableFrom(clazz) || View.class.isAssignableFrom(clazz)); + private boolean hasModelAttributeAnnotation(HandlerResult result) { + if (result.getHandler() instanceof HandlerMethod) { + MethodParameter returnType = ((HandlerMethod) result.getHandler()).getReturnType(); + if (returnType.hasMethodAnnotation(ModelAttribute.class)) { + return true; + } + } + return false; + } + + private boolean isSupportedType(Class clazz) { + return (CharSequence.class.isAssignableFrom(clazz) || View.class.isAssignableFrom(clazz) || + Model.class.isAssignableFrom(clazz) || Map.class.isAssignableFrom(clazz) || + !BeanUtils.isSimpleProperty(clazz)); } @Override public Mono handleResult(ServerWebExchange exchange, HandlerResult result) { - Mono mono; + Mono valueMono; ResolvableType elementType; ResolvableType returnType = result.getReturnValueType(); if (this.conversionService.canConvert(returnType.getRawClass(), Mono.class)) { Optional optionalValue = result.getReturnValue(); if (optionalValue.isPresent()) { - Mono convertedMono = this.conversionService.convert(optionalValue.get(), Mono.class); - mono = convertedMono.map(o -> o); + Mono converted = this.conversionService.convert(optionalValue.get(), Mono.class); + valueMono = converted.map(o -> o); } else { - mono = Mono.empty(); + valueMono = Mono.empty(); } elementType = returnType.getGeneric(0); } else { - mono = Mono.justOrEmpty(result.getReturnValue()); + valueMono = Mono.justOrEmpty(result.getReturnValue()); elementType = returnType; } - mono = mono.otherwiseIfEmpty(handleMissingReturnValue(exchange, result, elementType)); + Mono viewMono; + if (isViewReturnType(result, elementType)) { + viewMono = valueMono.otherwiseIfEmpty(selectDefaultViewName(exchange, result)); + } + else { + viewMono = valueMono.map(value -> updateModel(result, value)) + .defaultIfEmpty(result.getModel()) + .then(model -> selectDefaultViewName(exchange, result)); + } - return mono.then(returnValue -> { + return viewMono.then(returnValue -> { if (returnValue instanceof View) { Flux body = ((View) returnValue).render(result, null, exchange); return exchange.getResponse().setBody(body); @@ -158,28 +200,26 @@ public class ViewResolutionResultHandler implements HandlerResultHandler, Ordere }); } else { - // Eventually for model-related return values (should not happen now) + // Should not happen return Mono.error(new IllegalStateException("Unexpected return value")); } }); } - private Mono handleMissingReturnValue(ServerWebExchange exchange, HandlerResult result, - ResolvableType elementType) { + private boolean isViewReturnType(HandlerResult result, ResolvableType elementType) { + Class clazz = elementType.getRawClass(); + return (View.class.isAssignableFrom(clazz) || + (CharSequence.class.isAssignableFrom(clazz) && !hasModelAttributeAnnotation(result))); + } - if (isStringOrViewReference(elementType.getRawClass())) { - String defaultViewName = getDefaultViewName(exchange, result); - if (defaultViewName != null) { - return Mono.just(defaultViewName); - } - else { - return Mono.error(new IllegalStateException("Handler [" + result.getHandler() + "] " + - "neither returned a view name nor a View object")); - } + private Mono selectDefaultViewName(ServerWebExchange exchange, HandlerResult result) { + String defaultViewName = getDefaultViewName(exchange, result); + if (defaultViewName != null) { + return Mono.just(defaultViewName); } else { - // Eventually for model-related return values (should not happen now) - return Mono.error(new IllegalStateException("Unexpected return value type")); + return Mono.error(new IllegalStateException("Handler [" + result.getHandler() + "] " + + "neither returned a view name nor a View object")); } } @@ -191,6 +231,7 @@ public class ViewResolutionResultHandler implements HandlerResultHandler, Ordere * @return the default view name to use; if {@code null} is returned * processing will result in an IllegalStateException. */ + @SuppressWarnings("UnusedParameters") protected String getDefaultViewName(ServerWebExchange exchange, HandlerResult result) { String path = this.pathHelper.getLookupPathForRequest(exchange); if (path.startsWith("/")) { @@ -202,6 +243,49 @@ public class ViewResolutionResultHandler implements HandlerResultHandler, Ordere return StringUtils.stripFilenameExtension(path); } + private Object updateModel(HandlerResult result, Object value) { + if (value instanceof Model) { + result.getModel().addAllAttributes(((Model) value).asMap()); + } + else if (value instanceof Map) { + //noinspection unchecked + result.getModel().addAllAttributes((Map) value); + } + else if (result.getHandler() instanceof HandlerMethod) { + MethodParameter returnType = ((HandlerMethod) result.getHandler()).getReturnType(); + String name = getNameForReturnValue(value, returnType); + result.getModel().addAttribute(name, value); + } + else { + result.getModel().addAttribute(value); + } + return value; + } + + /** + * Derive the model attribute name for the given return value using one of: + *
      + *
    1. The method {@code ModelAttribute} annotation value + *
    2. The declared return type if it is more specific than {@code Object} + *
    3. The actual return value type + *
    + * @param returnValue the value returned from a method invocation + * @param returnType the return type of the method + * @return the model name, never {@code null} nor empty + */ + private static String getNameForReturnValue(Object returnValue, MethodParameter returnType) { + ModelAttribute annotation = returnType.getMethodAnnotation(ModelAttribute.class); + if (annotation != null && StringUtils.hasText(annotation.value())) { + return annotation.value(); + } + else { + Method method = returnType.getMethod(); + Class containingClass = returnType.getContainingClass(); + Class resolvedType = GenericTypeResolver.resolveReturnType(method, containingClass); + return Conventions.getVariableNameForReturnType(method, resolvedType, returnValue); + } + } + private Mono handleUnresolvedViewName(String viewName) { return Mono.error(new IllegalStateException( "Could not resolve view with name '" + viewName + "'.")); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java index ff34752123..492a1b4a55 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java @@ -18,7 +18,6 @@ package org.springframework.web.reactive.result.view; import java.lang.reflect.Method; import java.net.URI; -import java.net.URISyntaxException; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.time.Duration; @@ -38,6 +37,8 @@ import rx.Single; import org.springframework.core.Ordered; import org.springframework.core.ResolvableType; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.support.ConfigurableConversionService; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter; import org.springframework.core.io.buffer.DataBuffer; @@ -48,7 +49,10 @@ import org.springframework.http.server.reactive.MockServerHttpRequest; import org.springframework.http.server.reactive.MockServerHttpResponse; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.ui.ExtendedModelMap; +import org.springframework.ui.Model; import org.springframework.ui.ModelMap; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.HandlerResultHandler; import org.springframework.web.server.ServerWebExchange; @@ -56,7 +60,10 @@ import org.springframework.web.server.adapter.DefaultServerWebExchange; import org.springframework.web.server.session.DefaultWebSessionManager; import org.springframework.web.server.session.WebSessionManager; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; /** @@ -65,32 +72,30 @@ import static org.mockito.Mockito.mock; */ public class ViewResolutionResultHandlerTests { - private static final Charset UTF_8 = Charset.forName("UTF-8"); - - private MockServerHttpResponse response; private ModelMap model; - private DefaultConversionService conversionService; - @Before public void setUp() throws Exception { this.model = new ExtendedModelMap().addAttribute("id", "123"); - this.conversionService = new DefaultConversionService(); - this.conversionService.addConverter(new ReactiveStreamsToRxJava1Converter()); } @Test public void supportsWithNullReturnValue() throws Exception { - testSupports("handleString", null); - testSupports("handleView", null); - testSupports("handleMonoString", null); - testSupports("handleMonoView", null); - testSupports("handleSingleString", null); - testSupports("handleSingleView", null); + testSupports("handleString", true); + testSupports("handleView", true); + testSupports("handleMonoString", true); + testSupports("handleMonoView", true); + testSupports("handleSingleString", true); + testSupports("handleSingleView", true); + testSupports("handleModel", true); + testSupports("handleMap", true); + testSupports("handleModelAttributeAnnotation", true); + testSupports("handleTestBean", true); + testSupports("handleInteger", false); } @Test @@ -100,15 +105,15 @@ public class ViewResolutionResultHandlerTests { resolver1.setOrder(2); resolver2.setOrder(1); - assertEquals(Arrays.asList(resolver2, resolver1), - new ViewResolutionResultHandler(Arrays.asList(resolver1, resolver2), this.conversionService) - .getViewResolvers()); + assertEquals(Arrays.asList(resolver2, resolver1), new ViewResolutionResultHandler( + Arrays.asList(resolver1, resolver2), new DefaultConversionService()) + .getViewResolvers()); } @Test public void viewReference() throws Exception { Object value = new TestView("account"); - handle("/path", value, ResolvableType.forClass(View.class)); + handle("/path", value, "handleView"); new TestSubscriber().bindTo(this.response.getBody()) .assertValuesWith(buf -> assertEquals("account: {id=123}", asString(buf))); @@ -117,7 +122,7 @@ public class ViewResolutionResultHandlerTests { @Test public void viewReferenceInMono() throws Exception { Object value = Mono.just(new TestView("account")); - handle("/path", value, returnTypeFor("handleMonoView")); + handle("/path", value, "handleMonoView"); new TestSubscriber().bindTo(this.response.getBody()) .assertValuesWith(buf -> assertEquals("account: {id=123}", asString(buf))); @@ -126,7 +131,7 @@ public class ViewResolutionResultHandlerTests { @Test public void viewName() throws Exception { Object value = "account"; - handle("/path", value, ResolvableType.forClass(String.class), new TestViewResolver("account")); + handle("/path", value, "handleString", new TestViewResolver("account")); TestSubscriber subscriber = new TestSubscriber<>(); subscriber.bindTo(this.response.getBody()) @@ -136,7 +141,7 @@ public class ViewResolutionResultHandlerTests { @Test public void viewNameInMono() throws Exception { Object value = Mono.just("account"); - handle("/path", value, returnTypeFor("handleMonoString"), new TestViewResolver("account")); + handle("/path", value, "handleMonoString", new TestViewResolver("account")); new TestSubscriber().bindTo(this.response.getBody()) .assertValuesWith(buf -> assertEquals("account: {id=123}", asString(buf))); @@ -145,7 +150,7 @@ public class ViewResolutionResultHandlerTests { @Test public void viewNameWithMultipleResolvers() throws Exception { String value = "profile"; - handle("/path", value, ResolvableType.forClass(String.class), + handle("/path", value, "handleString", new TestViewResolver("account"), new TestViewResolver("profile")); new TestSubscriber().bindTo(this.response.getBody()) @@ -154,7 +159,7 @@ public class ViewResolutionResultHandlerTests { @Test public void viewNameUnresolved() throws Exception { - handle("/path", "account", ResolvableType.forClass(String.class)) + handle("/path", "account", "handleString") .assertErrorMessage("Could not resolve view with name 'account'."); } @@ -162,15 +167,15 @@ public class ViewResolutionResultHandlerTests { public void viewNameIsNull() throws Exception { ViewResolver resolver = new TestViewResolver("account"); - handle("/account", null, ResolvableType.forClass(String.class), resolver); + handle("/account", null, "handleString", resolver); new TestSubscriber().bindTo(this.response.getBody()) .assertValuesWith(buf -> assertEquals("account: {id=123}", asString(buf))); - handle("/account/", null, ResolvableType.forClass(String.class), resolver); + handle("/account/", null, "handleString", resolver); new TestSubscriber().bindTo(this.response.getBody()) .assertValuesWith(buf -> assertEquals("account: {id=123}", asString(buf))); - handle("/account.123", null, ResolvableType.forClass(String.class), resolver); + handle("/account.123", null, "handleString", resolver); new TestSubscriber().bindTo(this.response.getBody()) .assertValuesWith(buf -> assertEquals("account: {id=123}", asString(buf))); } @@ -178,28 +183,75 @@ public class ViewResolutionResultHandlerTests { @Test public void viewNameIsEmptyMono() throws Exception { Object value = Mono.empty(); - handle("/account", value, returnTypeFor("handleMonoString"), new TestViewResolver("account")); + handle("/account", value, "handleMonoString", new TestViewResolver("account")); new TestSubscriber().bindTo(this.response.getBody()) .assertValuesWith(buf -> assertEquals("account: {id=123}", asString(buf))); } + @Test + public void model() throws Exception { + Model value = new ExtendedModelMap().addAttribute("name", "Joe"); + handle("/account", value, "handleModel", new TestViewResolver("account")); - private void testSupports(String methodName, Object returnValue) throws NoSuchMethodException { - Method method = TestController.class.getMethod(methodName); - ResolvableType returnType = ResolvableType.forMethodParameter(method, -1); - HandlerResult result = new HandlerResult(new Object(), returnValue, returnType, this.model); - List resolvers = Collections.singletonList(mock(ViewResolver.class)); - ViewResolutionResultHandler handler = new ViewResolutionResultHandler(resolvers, this.conversionService); - assertTrue(handler.supports(result)); + new TestSubscriber().bindTo(this.response.getBody()) + .assertValuesWith(buf -> assertEquals("account: {id=123, name=Joe}", asString(buf))); } - private TestSubscriber handle(String path, Object value, ResolvableType type, - ViewResolver... resolvers) throws URISyntaxException { + @Test + public void map() throws Exception { + Map value = Collections.singletonMap("name", "Joe"); + handle("/account", value, "handleMap", new TestViewResolver("account")); + + new TestSubscriber().bindTo(this.response.getBody()) + .assertValuesWith(buf -> assertEquals("account: {id=123, name=Joe}", asString(buf))); + } + + @Test + public void modelAttributeAnnotation() throws Exception { + String value = "Joe"; + handle("/account", value, "handleModelAttributeAnnotation", new TestViewResolver("account")); + + new TestSubscriber().bindTo(this.response.getBody()) + .assertValuesWith(buf -> assertEquals("account: {id=123, name=Joe}", asString(buf))); + } + + @Test + public void testBean() throws Exception { + Object value = new TestBean("Joe"); + handle("/account", value, "handleTestBean", new TestViewResolver("account")); + + new TestSubscriber().bindTo(this.response.getBody()) + .assertValuesWith(buf -> assertEquals("account: {id=123, testBean=TestBean[name=Joe]}", asString(buf))); + } + + + private void testSupports(String methodName, boolean supports) throws NoSuchMethodException { + Method method = TestController.class.getMethod(methodName); + ResolvableType returnType = ResolvableType.forMethodParameter(method, -1); + HandlerResult result = new HandlerResult(new Object(), null, returnType, this.model); + List resolvers = Collections.singletonList(mock(ViewResolver.class)); + ConfigurableConversionService conversionService = new DefaultConversionService(); + conversionService.addConverter(new ReactiveStreamsToRxJava1Converter()); + ViewResolutionResultHandler handler = new ViewResolutionResultHandler(resolvers, conversionService); + if (supports) { + assertTrue(handler.supports(result)); + } + else { + assertFalse(handler.supports(result)); + } + } + + private TestSubscriber handle(String path, Object value, String methodName, + ViewResolver... resolvers) throws Exception { List resolverList = Arrays.asList(resolvers); - HandlerResultHandler handler = new ViewResolutionResultHandler(resolverList, this.conversionService); - HandlerResult handlerResult = new HandlerResult(new Object(), value, type, this.model); + ConversionService conversionService = new DefaultConversionService(); + HandlerResultHandler handler = new ViewResolutionResultHandler(resolverList, conversionService); + Method method = TestController.class.getMethod(methodName); + HandlerMethod handlerMethod = new HandlerMethod(new TestController(), method); + ResolvableType type = ResolvableType.forMethodReturnType(method); + HandlerResult handlerResult = new HandlerResult(handlerMethod, value, type, this.model); ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI(path)); this.response = new MockServerHttpResponse(); @@ -212,13 +264,8 @@ public class ViewResolutionResultHandlerTests { return subscriber.bindTo(mono).await(Duration.ofSeconds(1)); } - private ResolvableType returnTypeFor(String methodName, Class... args) throws NoSuchMethodException { - Method method = TestController.class.getDeclaredMethod(methodName, args); - return ResolvableType.forMethodReturnType(method); - } - private static DataBuffer asDataBuffer(String value) { - ByteBuffer byteBuffer = ByteBuffer.wrap(value.getBytes(UTF_8)); + ByteBuffer byteBuffer = ByteBuffer.wrap(value.getBytes(Charset.forName("UTF-8"))); return new DefaultDataBufferFactory().wrap(byteBuffer); } @@ -226,7 +273,7 @@ public class ViewResolutionResultHandlerTests { ByteBuffer byteBuffer = dataBuffer.asByteBuffer(); final byte[] bytes = new byte[byteBuffer.remaining()]; byteBuffer.get(bytes); - return new String(bytes, UTF_8); + return new String(bytes, Charset.forName("UTF-8")); } @@ -310,6 +357,45 @@ public class ViewResolutionResultHandlerTests { public Single handleSingleView() { return null; } + + public Model handleModel() { + return null; + } + + public Map handleMap() { + return null; + } + + @ModelAttribute("name") + public String handleModelAttributeAnnotation() { + return null; + } + + public TestBean handleTestBean() { + return null; + } + + public int handleInteger() { + return 0; + } + } + + private static class TestBean { + + private final String name; + + public TestBean(String name) { + this.name = name; + } + + public String getName() { + return this.name; + } + + @Override + public String toString() { + return "TestBean[name=" + this.name + "]"; + } } } \ No newline at end of file From 97155f1a30f9ead064bed07341b155f178cb9b83 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Mon, 30 May 2016 15:09:37 +0200 Subject: [PATCH 370/498] Rename ReactiveHttpOutputMessage.setBody() to writeWith() --- .../http/ReactiveHttpOutputMessage.java | 11 ++-- .../http/ZeroCopyHttpOutputMessage.java | 6 +-- .../reactive/ClientHttpRequestFactory.java | 2 +- .../reactive/ReactorClientHttpRequest.java | 2 +- .../reactive/RxNettyClientHttpRequest.java | 2 +- .../reactive/CodecHttpMessageConverter.java | 2 +- .../ResourceHttpMessageConverter.java | 2 +- .../reactive/AbstractServerHttpResponse.java | 8 +-- .../reactive/ReactorServerHttpResponse.java | 4 +- .../reactive/RxNettyServerHttpResponse.java | 4 +- .../reactive/ServletServerHttpResponse.java | 2 +- .../reactive/UndertowServerHttpResponse.java | 4 +- .../reactive/DefaultHttpRequestBuilder.java | 2 +- .../view/ViewResolutionResultHandler.java | 4 +- .../reactive/AsyncIntegrationTests.java | 2 +- .../reactive/EchoHandlerIntegrationTests.java | 2 +- .../reactive/MockServerHttpRequest.java | 4 +- .../reactive/MockServerHttpResponse.java | 2 +- .../RandomHandlerIntegrationTests.java | 2 +- .../reactive/ServerHttpResponseTests.java | 52 +++++++++---------- .../reactive/ZeroCopyIntegrationTests.java | 2 +- .../reactive/DispatcherHandlerErrorTests.java | 4 +- .../result/WebHandlerIntegrationTests.java | 4 +- 23 files changed, 67 insertions(+), 62 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java b/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java index 686a780583..7f139545e6 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java @@ -31,6 +31,7 @@ import org.springframework.core.io.buffer.DataBufferFactory; * on the server-side. * * @author Arjen Poutsma + * @author Sebastien Deleuze */ public interface ReactiveHttpOutputMessage extends HttpMessage { @@ -41,18 +42,20 @@ public interface ReactiveHttpOutputMessage extends HttpMessage { void beforeCommit(Supplier> action); /** - * Set the body of the message to the given {@link Publisher} which will be - * used to write to the underlying HTTP layer. + * Use the given {@link Publisher} to write the body of the message to the underlying + * HTTP layer, and flush the data when the complete signal is received (data could be + * flushed before depending on the configuration, the HTTP engine and the amount of + * data sent). * * @param body the body content publisher * @return a publisher that indicates completion or error. */ - Mono setBody(Publisher body); + Mono writeWith(Publisher body); /** * Returns a {@link DataBufferFactory} that can be used for creating the body. * @return a buffer factory - * @see #setBody(Publisher) + * @see #writeWith(Publisher) */ DataBufferFactory bufferFactory(); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/ZeroCopyHttpOutputMessage.java b/spring-web-reactive/src/main/java/org/springframework/http/ZeroCopyHttpOutputMessage.java index 9499c8d81f..91df2452e8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/ZeroCopyHttpOutputMessage.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/ZeroCopyHttpOutputMessage.java @@ -30,13 +30,13 @@ import reactor.core.publisher.Mono; public interface ZeroCopyHttpOutputMessage extends ReactiveHttpOutputMessage { /** - * Set the body of the message to the given {@link File} which will be - * used to write to the underlying HTTP layer. + * Use the given {@link File} to write the body of the message to the underlying + * HTTP layer. * @param file the file to transfer * @param position the position within the file from which the transfer is to begin * @param count the number of bytes to be transferred * @return a publisher that indicates completion or error. */ - Mono setBody(File file, long position, long count); + Mono writeWith(File file, long position, long count); } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ClientHttpRequestFactory.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ClientHttpRequestFactory.java index af8c437825..ce8b5b7e17 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ClientHttpRequestFactory.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ClientHttpRequestFactory.java @@ -31,7 +31,7 @@ public interface ClientHttpRequestFactory { /** * Create a new {@link ClientHttpRequest} for the specified HTTP method, URI and headers - *

    The returned request can be {@link ClientHttpRequest#setBody(Publisher) written to}, + *

    The returned request can be {@link ClientHttpRequest#writeWith(Publisher) written to}, * and then executed by calling {@link ClientHttpRequest#execute()} * * @param httpMethod the HTTP method to execute diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java index fff7f7a485..4bec8778a1 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java @@ -89,7 +89,7 @@ public class ReactorClientHttpRequest extends AbstractClientHttpRequest { * @see #execute() */ @Override - public Mono setBody(Publisher body) { + public Mono writeWith(Publisher body) { this.body = Flux.from(body).map(this::toByteBuf); return Mono.empty(); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpRequest.java index aa45ed7404..e0804c07a8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpRequest.java @@ -77,7 +77,7 @@ public class RxNettyClientHttpRequest extends AbstractClientHttpRequest { * @see #execute() */ @Override - public Mono setBody(Publisher body) { + public Mono writeWith(Publisher body) { this.body = RxJava1ObservableConverter.from(Flux.from(body) .map(b -> dataBufferFactory.wrap(b.asByteBuffer()).getNativeBuffer())); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/CodecHttpMessageConverter.java b/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/CodecHttpMessageConverter.java index e48716b794..207fbf506a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/CodecHttpMessageConverter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/CodecHttpMessageConverter.java @@ -131,6 +131,6 @@ public class CodecHttpMessageConverter implements HttpMessageConverter { DataBufferFactory dataBufferFactory = outputMessage.bufferFactory(); Flux body = this.encoder.encode(inputStream, dataBufferFactory, type, contentType); - return outputMessage.setBody(body); + return outputMessage.writeWith(body); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/ResourceHttpMessageConverter.java b/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/ResourceHttpMessageConverter.java index 79392a518d..158ef29847 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/ResourceHttpMessageConverter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/ResourceHttpMessageConverter.java @@ -93,7 +93,7 @@ public class ResourceHttpMessageConverter extends CodecHttpMessageConverter setBody(Publisher publisher) { + public Mono writeWith(Publisher publisher) { return new ChannelSendOperator<>(publisher, writePublisher -> - applyBeforeCommit().then(() -> setBodyInternal(writePublisher))); + applyBeforeCommit().then(() -> writeWithInternal(writePublisher))); } protected Mono applyBeforeCommit() { @@ -128,9 +128,9 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse { /** * Implement this method to write to the underlying the response. - * @param publisher the publisher to write with + * @param body the publisher to write with */ - protected abstract Mono setBodyInternal(Publisher publisher); + protected abstract Mono writeWithInternal(Publisher body); @Override public void beforeCommit(Supplier> action) { diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java index a132f635aa..1f293f6fac 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java @@ -65,7 +65,7 @@ public class ReactorServerHttpResponse extends AbstractServerHttpResponse } @Override - protected Mono setBodyInternal(Publisher publisher) { + protected Mono writeWithInternal(Publisher publisher) { return this.channel.send(Flux.from(publisher).map(this::toByteBuf)); } @@ -105,7 +105,7 @@ public class ReactorServerHttpResponse extends AbstractServerHttpResponse } @Override - public Mono setBody(File file, long position, long count) { + public Mono writeWith(File file, long position, long count) { return applyBeforeCommit().then(() -> { return this.channel.sendFile(file, position, count); }); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java index bb6f571526..936b4c97be 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java @@ -63,7 +63,7 @@ public class RxNettyServerHttpResponse extends AbstractServerHttpResponse { } @Override - protected Mono setBodyInternal(Publisher publisher) { + protected Mono writeWithInternal(Publisher publisher) { Observable content = RxJava1ObservableConverter.from(publisher).map(this::toByteBuf); Observable completion = this.response.write(content); @@ -114,7 +114,7 @@ public class RxNettyServerHttpResponse extends AbstractServerHttpResponse { @Override - public Mono setBody(File file, long position, long count) { + public Mono writeWith(File file, long position, long count) { Channel channel = this.response.unsafeNettyChannel(); HttpResponse httpResponse = diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java index 59e3ea26d5..926315c204 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java @@ -68,7 +68,7 @@ public class ServletServerHttpResponse extends AbstractServerHttpResponse { } @Override - protected Mono setBodyInternal(Publisher publisher) { + protected Mono writeWithInternal(Publisher publisher) { return this.responseBodyWriter.apply(publisher); } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java index dd7507a78e..7ce3a5d89a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java @@ -79,12 +79,12 @@ public class UndertowServerHttpResponse extends AbstractServerHttpResponse } @Override - protected Mono setBodyInternal(Publisher publisher) { + protected Mono writeWithInternal(Publisher publisher) { return this.responseBodyWriter.apply(publisher); } @Override - public Mono setBody(File file, long position, long count) { + public Mono writeWith(File file, long position, long count) { writeHeaders(); writeCookies(); try { diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultHttpRequestBuilder.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultHttpRequestBuilder.java index 5293508c93..cfff9a257d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultHttpRequestBuilder.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultHttpRequestBuilder.java @@ -148,7 +148,7 @@ public class DefaultHttpRequestBuilder implements HttpRequestBuilder { .findFirst(); if (messageEncoder.isPresent()) { - request.setBody(messageEncoder.get() + request.writeWith(messageEncoder.get() .encode(this.contentPublisher, request.bufferFactory(), requestBodyType, mediaType)); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java index d46e127ad6..52c3676c89 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java @@ -185,7 +185,7 @@ public class ViewResolutionResultHandler implements HandlerResultHandler, Ordere return viewMono.then(returnValue -> { if (returnValue instanceof View) { Flux body = ((View) returnValue).render(result, null, exchange); - return exchange.getResponse().setBody(body); + return exchange.getResponse().writeWith(body); } else if (returnValue instanceof CharSequence) { String viewName = returnValue.toString(); @@ -196,7 +196,7 @@ public class ViewResolutionResultHandler implements HandlerResultHandler, Ordere .otherwiseIfEmpty(handleUnresolvedViewName(viewName)) .then(view -> { Flux body = view.render(result, null, exchange); - return exchange.getResponse().setBody(body); + return exchange.getResponse().writeWith(body); }); } else { diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java index 54c922baec..bf7561160d 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java @@ -65,7 +65,7 @@ public class AsyncIntegrationTests extends AbstractHttpHandlerIntegrationTests { @Override public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { - return response.setBody(Flux.just("h", "e", "l", "l", "o") + return response.writeWith(Flux.just("h", "e", "l", "l", "o") .useTimer(Timer.global()) .delay(Duration.ofMillis(100)) .publishOn(asyncGroup) diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/EchoHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/EchoHandlerIntegrationTests.java index 00c030795e..dd0a4a5ea1 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/EchoHandlerIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/EchoHandlerIntegrationTests.java @@ -67,7 +67,7 @@ public class EchoHandlerIntegrationTests extends AbstractHttpHandlerIntegrationT @Override public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { - return response.setBody(request.getBody()); + return response.writeWith(request.getBody()); } } } diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpRequest.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpRequest.java index 3f7864144b..805734aced 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpRequest.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpRequest.java @@ -19,6 +19,7 @@ import java.net.URI; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpCookie; @@ -96,7 +97,8 @@ public class MockServerHttpRequest implements ServerHttpRequest { return this.body; } - public void setBody(Publisher body) { + public Mono writeWith(Publisher body) { this.body = Flux.from(body); + return this.body.then(); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java index 3a6ea38952..10292ad6f7 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java @@ -71,7 +71,7 @@ public class MockServerHttpResponse implements ServerHttpResponse { } @Override - public Mono setBody(Publisher body) { + public Mono writeWith(Publisher body) { this.body = body; return Flux.from(this.body).then(); } diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/RandomHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/RandomHandlerIntegrationTests.java index 8ab8aa7917..095c8f86e0 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/RandomHandlerIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/RandomHandlerIntegrationTests.java @@ -131,7 +131,7 @@ public class RandomHandlerIntegrationTests extends AbstractHttpHandlerIntegratio }); response.getHeaders().setContentLength(RESPONSE_SIZE); - return response.setBody(multipleChunks()); + return response.writeWith(multipleChunks()); } private Publisher singleChunk() { diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java index 049e5f5d4e..9628e3cd9e 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java @@ -43,28 +43,28 @@ public class ServerHttpResponseTests { @Test - public void setBody() throws Exception { + public void writeWith() throws Exception { TestServerHttpResponse response = new TestServerHttpResponse(); - response.setBody(Flux.just(wrap("a"), wrap("b"), wrap("c"))).get(); + response.writeWith(Flux.just(wrap("a"), wrap("b"), wrap("c"))).get(); assertTrue(response.headersWritten); assertTrue(response.cookiesWritten); - assertEquals(3, response.content.size()); - assertEquals("a", new String(response.content.get(0).asByteBuffer().array(), UTF_8)); - assertEquals("b", new String(response.content.get(1).asByteBuffer().array(), UTF_8)); - assertEquals("c", new String(response.content.get(2).asByteBuffer().array(), UTF_8)); + assertEquals(3, response.body.size()); + assertEquals("a", new String(response.body.get(0).asByteBuffer().array(), UTF_8)); + assertEquals("b", new String(response.body.get(1).asByteBuffer().array(), UTF_8)); + assertEquals("c", new String(response.body.get(2).asByteBuffer().array(), UTF_8)); } @Test - public void setBodyWithError() throws Exception { + public void writeWithWithComplete() throws Exception { TestServerHttpResponse response = new TestServerHttpResponse(); IllegalStateException error = new IllegalStateException("boo"); - response.setBody(Flux.error(error)).otherwise(ex -> Mono.empty()).get(); + response.writeWith(Flux.error(error)).otherwise(ex -> Mono.empty()).get(); assertFalse(response.headersWritten); assertFalse(response.cookiesWritten); - assertTrue(response.content.isEmpty()); + assertTrue(response.body.isEmpty()); } @Test @@ -74,27 +74,27 @@ public class ServerHttpResponseTests { assertTrue(response.headersWritten); assertTrue(response.cookiesWritten); - assertTrue(response.content.isEmpty()); + assertTrue(response.body.isEmpty()); } @Test - public void beforeCommitWithSetBody() throws Exception { + public void beforeCommitWithComplete() throws Exception { ResponseCookie cookie = ResponseCookie.from("ID", "123").build(); TestServerHttpResponse response = new TestServerHttpResponse(); response.beforeCommit(() -> { response.getCookies().add(cookie.getName(), cookie); return Mono.empty(); }); - response.setBody(Flux.just(wrap("a"), wrap("b"), wrap("c"))).get(); + response.writeWith(Flux.just(wrap("a"), wrap("b"), wrap("c"))).get(); assertTrue(response.headersWritten); assertTrue(response.cookiesWritten); assertSame(cookie, response.getCookies().getFirst("ID")); - assertEquals(3, response.content.size()); - assertEquals("a", new String(response.content.get(0).asByteBuffer().array(), UTF_8)); - assertEquals("b", new String(response.content.get(1).asByteBuffer().array(), UTF_8)); - assertEquals("c", new String(response.content.get(2).asByteBuffer().array(), UTF_8)); + assertEquals(3, response.body.size()); + assertEquals("a", new String(response.body.get(0).asByteBuffer().array(), UTF_8)); + assertEquals("b", new String(response.body.get(1).asByteBuffer().array(), UTF_8)); + assertEquals("c", new String(response.body.get(2).asByteBuffer().array(), UTF_8)); } @Test @@ -102,16 +102,16 @@ public class ServerHttpResponseTests { TestServerHttpResponse response = new TestServerHttpResponse(); IllegalStateException error = new IllegalStateException("boo"); response.beforeCommit(() -> Mono.error(error)); - response.setBody(Flux.just(wrap("a"), wrap("b"), wrap("c"))).get(); + response.writeWith(Flux.just(wrap("a"), wrap("b"), wrap("c"))).get(); assertTrue("beforeCommit action errors should be ignored", response.headersWritten); assertTrue("beforeCommit action errors should be ignored", response.cookiesWritten); assertNull(response.getCookies().get("ID")); - assertEquals(3, response.content.size()); - assertEquals("a", new String(response.content.get(0).asByteBuffer().array(), UTF_8)); - assertEquals("b", new String(response.content.get(1).asByteBuffer().array(), UTF_8)); - assertEquals("c", new String(response.content.get(2).asByteBuffer().array(), UTF_8)); + assertEquals(3, response.body.size()); + assertEquals("a", new String(response.body.get(0).asByteBuffer().array(), UTF_8)); + assertEquals("b", new String(response.body.get(1).asByteBuffer().array(), UTF_8)); + assertEquals("c", new String(response.body.get(2).asByteBuffer().array(), UTF_8)); } @Test @@ -126,7 +126,7 @@ public class ServerHttpResponseTests { assertTrue(response.headersWritten); assertTrue(response.cookiesWritten); - assertTrue(response.content.isEmpty()); + assertTrue(response.body.isEmpty()); assertSame(cookie, response.getCookies().getFirst("ID")); } @@ -143,7 +143,7 @@ public class ServerHttpResponseTests { private boolean cookiesWritten; - private final List content = new ArrayList<>(); + private final List body = new ArrayList<>(); public TestServerHttpResponse() { super(new DefaultDataBufferFactory()); @@ -166,9 +166,9 @@ public class ServerHttpResponseTests { } @Override - protected Mono setBodyInternal(Publisher publisher) { - return Flux.from(publisher).map(b -> { - this.content.add(b); + protected Mono writeWithInternal(Publisher body) { + return Flux.from(body).map(b -> { + this.body.add(b); return b; }).then(); } diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ZeroCopyIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ZeroCopyIntegrationTests.java index bee5462c1a..1b1e8c7aa0 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ZeroCopyIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ZeroCopyIntegrationTests.java @@ -84,7 +84,7 @@ public class ZeroCopyIntegrationTests extends AbstractHttpHandlerIntegrationTest File logoFile = logo.getFile(); zeroCopyResponse.getHeaders().setContentType(MediaType.IMAGE_PNG); zeroCopyResponse.getHeaders().setContentLength(logoFile.length()); - return zeroCopyResponse.setBody(logoFile, 0, logoFile.length()); + return zeroCopyResponse.writeWith(logoFile, 0, logoFile.length()); } catch (Throwable ex) { diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java index f014b1fcee..c46460627c 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java @@ -162,7 +162,7 @@ public class DispatcherHandlerErrorTests { this.request.getHeaders().setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); DataBuffer buffer = new DefaultDataBufferFactory().allocateBuffer() .write("body".getBytes("UTF-8")); - this.request.setBody(Mono.just(buffer)); + this.request.writeWith(Mono.just(buffer)); Mono publisher = this.dispatcherHandler.handle(this.exchange); Throwable ex = awaitErrorSignal(publisher); @@ -173,7 +173,7 @@ public class DispatcherHandlerErrorTests { @Test public void requestBodyError() throws Exception { this.request.setUri(new URI("/request-body")); - this.request.setBody(Mono.error(EXCEPTION)); + this.request.writeWith(Mono.error(EXCEPTION)); Mono publisher = this.dispatcherHandler.handle(this.exchange); Throwable ex = awaitErrorSignal(publisher); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/WebHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/WebHandlerIntegrationTests.java index 5445bf9b9f..86624f9981 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/WebHandlerIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/WebHandlerIntegrationTests.java @@ -150,7 +150,7 @@ public class WebHandlerIntegrationTests extends AbstractHttpHandlerIntegrationTe @Override public Mono handle(ServerWebExchange exchange) { DataBuffer buffer = asDataBuffer("foo"); - return exchange.getResponse().setBody(Flux.just(buffer)); + return exchange.getResponse().writeWith(Flux.just(buffer)); } } @@ -159,7 +159,7 @@ public class WebHandlerIntegrationTests extends AbstractHttpHandlerIntegrationTe @Override public Mono handle(ServerWebExchange exchange) { DataBuffer buffer = asDataBuffer("bar"); - return exchange.getResponse().setBody(Flux.just(buffer)); + return exchange.getResponse().writeWith(Flux.just(buffer)); } } From 3f80b4ab2e1bf2bd63ee1e5ff7a4910aa0c3621a Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Mon, 30 May 2016 15:26:55 +0200 Subject: [PATCH 371/498] Polish --- .../http/server/reactive/ServerHttpResponseTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java index 9628e3cd9e..e30181f73a 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java @@ -57,7 +57,7 @@ public class ServerHttpResponseTests { } @Test - public void writeWithWithComplete() throws Exception { + public void writeWithError() throws Exception { TestServerHttpResponse response = new TestServerHttpResponse(); IllegalStateException error = new IllegalStateException("boo"); response.writeWith(Flux.error(error)).otherwise(ex -> Mono.empty()).get(); From 7fc3031c7aa40a1edf9981094aadeb216e086df1 Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Tue, 31 May 2016 11:11:38 +0100 Subject: [PATCH 372/498] update after Schedulers change --- .../http/server/reactive/AsyncIntegrationTests.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java index bf7561160d..9405c7f656 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java @@ -21,11 +21,10 @@ import java.time.Duration; import org.hamcrest.Matchers; import org.junit.Test; -import reactor.core.publisher.Computations; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.scheduler.Scheduler; -import reactor.core.scheduler.Timer; +import reactor.core.scheduler.Schedulers; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DefaultDataBufferFactory; @@ -42,7 +41,7 @@ import static org.junit.Assert.assertThat; */ public class AsyncIntegrationTests extends AbstractHttpHandlerIntegrationTests { - private final Scheduler asyncGroup = Computations.parallel(); + private final Scheduler asyncGroup = Schedulers.parallel(); private final DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory(); @@ -66,7 +65,7 @@ public class AsyncIntegrationTests extends AbstractHttpHandlerIntegrationTests { @Override public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { return response.writeWith(Flux.just("h", "e", "l", "l", "o") - .useTimer(Timer.global()) + .useTimer(Schedulers.timer()) .delay(Duration.ofMillis(100)) .publishOn(asyncGroup) .collect(dataBufferFactory::allocateBuffer, (buffer, str) -> buffer.write(str.getBytes()))); From 3f7178ba9f56f66407f504411df19d448c69c713 Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Tue, 31 May 2016 13:00:51 +0100 Subject: [PATCH 373/498] Sync Reactor --- .../annotation/SessionAttributeMethodArgumentResolver.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/SessionAttributeMethodArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/SessionAttributeMethodArgumentResolver.java index b6eded3b22..153a6166da 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/SessionAttributeMethodArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/SessionAttributeMethodArgumentResolver.java @@ -58,7 +58,7 @@ public class SessionAttributeMethodArgumentResolver extends AbstractNamedValueMe @Override protected Mono resolveName(String name, MethodParameter parameter, ServerWebExchange exchange){ return exchange.getSession().map(session -> session.getAttribute(name)) - .where(Optional::isPresent).map(Optional::get); + .filter(Optional::isPresent).map(Optional::get); } @Override From 1b308cffbf5a6d6b4e71b8b991ae698f822ab5f1 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Sun, 29 May 2016 22:28:13 -0400 Subject: [PATCH 374/498] Introduce base class for ResponseBodyResultHandler The ContentNegotiatingResultHandlerSupport base class encapsulates the logic for content negotiation needed for both @ResponseBody and view resolution result handling. --- ...ontentNegotiatingResultHandlerSupport.java | 148 ++++++++++++++++++ .../annotation/ResponseBodyResultHandler.java | 144 +++-------------- 2 files changed, 173 insertions(+), 119 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/ContentNegotiatingResultHandlerSupport.java diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/ContentNegotiatingResultHandlerSupport.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/ContentNegotiatingResultHandlerSupport.java new file mode 100644 index 0000000000..fd0b0fb4b7 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/ContentNegotiatingResultHandlerSupport.java @@ -0,0 +1,148 @@ +/* + * 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.web.reactive.result; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import org.springframework.core.Ordered; +import org.springframework.core.convert.ConversionService; +import org.springframework.http.MediaType; +import org.springframework.util.Assert; +import org.springframework.web.reactive.HandlerMapping; +import org.springframework.web.reactive.accept.RequestedContentTypeResolver; +import org.springframework.web.server.ServerWebExchange; + +/** + * Base class for {@link org.springframework.web.reactive.HandlerResultHandler + * HandlerResultHandler} implementations that perform content negotiation. + * + * @author Rossen Stoyanchev + */ +public abstract class ContentNegotiatingResultHandlerSupport implements Ordered { + + private static final MediaType MEDIA_TYPE_APPLICATION_ALL = new MediaType("application"); + + + private final ConversionService conversionService; + + private final RequestedContentTypeResolver contentTypeResolver; + + private int order = LOWEST_PRECEDENCE; + + + protected ContentNegotiatingResultHandlerSupport(ConversionService conversionService, + RequestedContentTypeResolver contentTypeResolver) { + + Assert.notNull(conversionService, "'conversionService' is required."); + Assert.notNull(contentTypeResolver, "'contentTypeResolver' is required."); + this.conversionService = conversionService; + this.contentTypeResolver = contentTypeResolver; + } + + + /** + * Return the configured {@link ConversionService}. + */ + public ConversionService getConversionService() { + return this.conversionService; + } + + /** + * Return the configured {@link RequestedContentTypeResolver}. + */ + public RequestedContentTypeResolver getContentTypeResolver() { + return this.contentTypeResolver; + } + + /** + * Set the order for this result handler relative to others. + *

    By default set to {@link Ordered#LOWEST_PRECEDENCE}, however see + * Javadoc of sub-classes which may change this default. + * @param order the order + */ + public void setOrder(int order) { + this.order = order; + } + + @Override + public int getOrder() { + return this.order; + } + + + /** + * Select the best media type for the current request through a content + * negotiation algorithm. + * @param exchange the current request + * @param producibleTypes the media types that can be produced for the current request + * @return the selected media type or {@code null} + */ + protected MediaType selectMediaType(ServerWebExchange exchange, List producibleTypes) { + + List acceptableTypes = getAcceptableTypes(exchange); + producibleTypes = getProducibleTypes(exchange, producibleTypes); + + Set compatibleMediaTypes = new LinkedHashSet<>(); + for (MediaType acceptable : acceptableTypes) { + for (MediaType producible : producibleTypes) { + if (acceptable.isCompatibleWith(producible)) { + compatibleMediaTypes.add(selectMoreSpecificMediaType(acceptable, producible)); + } + } + } + + List result = new ArrayList<>(compatibleMediaTypes); + MediaType.sortBySpecificityAndQuality(result); + + for (MediaType mediaType : compatibleMediaTypes) { + if (mediaType.isConcrete()) { + return mediaType; + } + else if (mediaType.equals(MediaType.ALL) || mediaType.equals(MEDIA_TYPE_APPLICATION_ALL)) { + return MediaType.APPLICATION_OCTET_STREAM; + } + } + + return null; + } + + private List getAcceptableTypes(ServerWebExchange exchange) { + List mediaTypes = this.contentTypeResolver.resolveMediaTypes(exchange); + return (mediaTypes.isEmpty() ? Collections.singletonList(MediaType.ALL) : mediaTypes); + } + + private List getProducibleTypes(ServerWebExchange exchange, List mediaTypes) { + Optional optional = exchange.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE); + if (optional.isPresent()) { + Set set = (Set) optional.get(); + return new ArrayList<>(set); + } + return mediaTypes; + } + + private MediaType selectMoreSpecificMediaType(MediaType acceptable, MediaType producible) { + producible = producible.copyQualityValue(acceptable); + Comparator comparator = MediaType.SPECIFICITY_COMPARATOR; + return (comparator.compare(acceptable, producible) <= 0 ? acceptable : producible); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java index 8a64f6c86c..a2107aa77d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java @@ -16,13 +16,8 @@ package org.springframework.web.reactive.result.method.annotation; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.LinkedHashSet; import java.util.List; import java.util.Optional; -import java.util.Set; import java.util.stream.Collectors; import org.reactivestreams.Publisher; @@ -39,11 +34,11 @@ import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.util.Assert; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.method.HandlerMethod; -import org.springframework.web.reactive.HandlerMapping; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.HandlerResultHandler; import org.springframework.web.reactive.accept.HeaderContentTypeResolver; import org.springframework.web.reactive.accept.RequestedContentTypeResolver; +import org.springframework.web.reactive.result.ContentNegotiatingResultHandlerSupport; import org.springframework.web.server.NotAcceptableStatusException; import org.springframework.web.server.ServerWebExchange; @@ -53,25 +48,21 @@ import org.springframework.web.server.ServerWebExchange; * with {@code @ResponseBody} writing to the body of the request or response with * an {@link HttpMessageConverter}. * + *

    By default the order for the result handler is set to 0. It is generally + * safe and expected it will be ordered ahead of other result handlers since it + * only gets involved based on the presence of an {@code @ResponseBody} + * annotation. + * * @author Rossen Stoyanchev * @author Stephane Maldini * @author Sebastien Deleuze * @author Arjen Poutsma */ -public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered { - - private static final MediaType MEDIA_TYPE_APPLICATION_ALL = new MediaType("application"); +public class ResponseBodyResultHandler extends ContentNegotiatingResultHandlerSupport + implements HandlerResultHandler, Ordered { private final List> messageConverters; - private final ConversionService conversionService; - - private final RequestedContentTypeResolver contentTypeResolver; - - private final List supportedMediaTypes; - - private int order = 0; - /** * Constructor with message converters and a {@code ConversionService} only @@ -91,46 +82,18 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered * Constructor with message converters, a {@code ConversionService}, and a * {@code RequestedContentTypeResolver}. * - * @param messageConverters converters for writing the response body with + * @param converters converters for writing the response body with * @param conversionService for converting other reactive types (e.g. * rx.Observable, rx.Single, etc.) to Flux or Mono + * @param contentTypeResolver for resolving the requested content type */ - public ResponseBodyResultHandler(List> messageConverters, + public ResponseBodyResultHandler(List> converters, ConversionService conversionService, RequestedContentTypeResolver contentTypeResolver) { - Assert.notEmpty(messageConverters, "At least one message converter is required."); - Assert.notNull(conversionService, "'conversionService' is required."); - Assert.notNull(contentTypeResolver, "'contentTypeResolver' is required."); - - this.messageConverters = messageConverters; - this.conversionService = conversionService; - this.contentTypeResolver = contentTypeResolver; - this.supportedMediaTypes = initSupportedMediaTypes(messageConverters); - } - - private static List initSupportedMediaTypes(List> converters) { - Set set = new LinkedHashSet<>(); - converters.forEach(converter -> set.addAll(converter.getWritableMediaTypes())); - List result = new ArrayList<>(set); - MediaType.sortBySpecificity(result); - return Collections.unmodifiableList(result); - } - - - /** - * Set the order for this result handler relative to others. - *

    By default this is set to 0 and is generally save to be ahead of other - * result handlers since it only gets involved if the method (or class) is - * annotated with {@code @ResponseBody}. - * @param order the order - */ - public void setOrder(int order) { - this.order = order; - } - - @Override - public int getOrder() { - return this.order; + super(conversionService, contentTypeResolver); + Assert.notEmpty(converters, "At least one message converter is required."); + this.messageConverters = converters; + setOrder(0); } @@ -154,10 +117,10 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered ResolvableType elementType; ResolvableType returnType = result.getReturnValueType(); - if (this.conversionService.canConvert(returnType.getRawClass(), Publisher.class)) { + if (getConversionService().canConvert(returnType.getRawClass(), Publisher.class)) { Optional optionalValue = result.getReturnValue(); if (optionalValue.isPresent()) { - publisher = this.conversionService.convert(optionalValue.get(), Publisher.class); + publisher = getConversionService().convert(optionalValue.get(), Publisher.class); } else { publisher = Mono.empty(); @@ -172,16 +135,9 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered elementType = returnType; } - List compatibleMediaTypes = getCompatibleMediaTypes(exchange, elementType); - if (compatibleMediaTypes.isEmpty()) { - if (result.getReturnValue().isPresent()) { - List mediaTypes = getProducibleMediaTypes(exchange, elementType); - return Mono.error(new NotAcceptableStatusException(mediaTypes)); - } - return Mono.empty(); - } + List producibleTypes = getProducibleMediaTypes(elementType); + MediaType bestMediaType = selectMediaType(exchange, producibleTypes); - MediaType bestMediaType = selectBestMediaType(compatibleMediaTypes); if (bestMediaType != null) { for (HttpMessageConverter converter : this.messageConverters) { if (converter.canWrite(elementType, bestMediaType)) { @@ -191,64 +147,14 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered } } - return Mono.error(new NotAcceptableStatusException(this.supportedMediaTypes)); + return Mono.error(new NotAcceptableStatusException(producibleTypes)); } - private List getCompatibleMediaTypes(ServerWebExchange exchange, - ResolvableType elementType) { - - List acceptableMediaTypes = getAcceptableMediaTypes(exchange); - List producibleMediaTypes = getProducibleMediaTypes(exchange, elementType); - - Set compatibleMediaTypes = new LinkedHashSet<>(); - for (MediaType acceptable : acceptableMediaTypes) { - for (MediaType producible : producibleMediaTypes) { - if (acceptable.isCompatibleWith(producible)) { - compatibleMediaTypes.add(selectMoreSpecificMediaType(acceptable, producible)); - } - } - } - - List result = new ArrayList<>(compatibleMediaTypes); - MediaType.sortBySpecificityAndQuality(result); - return result; - } - - private List getAcceptableMediaTypes(ServerWebExchange exchange) { - List mediaTypes = this.contentTypeResolver.resolveMediaTypes(exchange); - return (mediaTypes.isEmpty() ? Collections.singletonList(MediaType.ALL) : mediaTypes); - } - - private List getProducibleMediaTypes(ServerWebExchange exchange, ResolvableType type) { - Optional optional = exchange.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE); - if (optional.isPresent()) { - Set mediaTypes = (Set) optional.get(); - return new ArrayList<>(mediaTypes); - } - else { - return this.messageConverters.stream() - .filter(converter -> converter.canWrite(type, null)) - .flatMap(converter -> converter.getWritableMediaTypes().stream()) - .collect(Collectors.toList()); - } - } - - private MediaType selectMoreSpecificMediaType(MediaType acceptable, MediaType producible) { - producible = producible.copyQualityValue(acceptable); - Comparator comparator = MediaType.SPECIFICITY_COMPARATOR; - return (comparator.compare(acceptable, producible) <= 0 ? acceptable : producible); - } - - private MediaType selectBestMediaType(List compatibleMediaTypes) { - for (MediaType mediaType : compatibleMediaTypes) { - if (mediaType.isConcrete()) { - return mediaType; - } - else if (mediaType.equals(MediaType.ALL) || mediaType.equals(MEDIA_TYPE_APPLICATION_ALL)) { - return MediaType.APPLICATION_OCTET_STREAM; - } - } - return null; + private List getProducibleMediaTypes(ResolvableType type) { + return this.messageConverters.stream() + .filter(converter -> converter.canWrite(type, null)) + .flatMap(converter -> converter.getWritableMediaTypes().stream()) + .collect(Collectors.toList()); } } From f8a7024b736a42a6663a7c7470f14ea92051a3d2 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Sun, 29 May 2016 22:51:10 -0400 Subject: [PATCH 375/498] Polish SimpleResultHandler --- .../reactive/result/SimpleResultHandler.java | 23 +++++------- .../result/SimpleResultHandlerTests.java | 36 +++---------------- .../result/WebHandlerIntegrationTests.java | 31 +++++++++++++--- 3 files changed, 39 insertions(+), 51 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/SimpleResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/SimpleResultHandler.java index 1b9c7b1d61..7f24effc08 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/SimpleResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/SimpleResultHandler.java @@ -47,9 +47,6 @@ public class SimpleResultHandler implements Ordered, HandlerResultHandler { private int order = Ordered.LOWEST_PRECEDENCE; - public SimpleResultHandler() { - } - public SimpleResultHandler(ConversionService conversionService) { Assert.notNull(conversionService, "'conversionService' is required."); this.conversionService = conversionService; @@ -76,18 +73,14 @@ public class SimpleResultHandler implements Ordered, HandlerResultHandler { @Override public boolean supports(HandlerResult result) { ResolvableType type = result.getReturnValueType(); - return (type != null && (Void.TYPE.equals(type.getRawClass()) || isConvertibleToVoidPublisher(type))); - } - - private boolean isConvertibleToVoidPublisher(ResolvableType type) { - return (isConvertibleToPublisher(type) && - Void.class.isAssignableFrom(type.getGeneric(0).getRawClass())); - } - - private boolean isConvertibleToPublisher(ResolvableType type) { - Class clazz = type.getRawClass(); - return (Publisher.class.isAssignableFrom(clazz) || - ((this.conversionService != null) && this.conversionService.canConvert(clazz, Publisher.class))); + if (Void.TYPE.equals(type.getRawClass())) { + return true; + } + if (this.conversionService.canConvert(type.getRawClass(), Publisher.class)) { + Class clazz = result.getReturnValueType().getGeneric(0).getRawClass(); + return Void.class.equals(clazz); + } + return false; } @SuppressWarnings("unchecked") diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/SimpleResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/SimpleResultHandlerTests.java index c58f189ca5..52ad7271dd 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/SimpleResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/SimpleResultHandlerTests.java @@ -24,6 +24,7 @@ import reactor.core.publisher.Flux; import rx.Observable; import org.springframework.core.ResolvableType; +import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter; import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter; @@ -35,48 +36,19 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; /** + * Unit tests for {@link SimpleResultHandler}. * @author Sebastien Deleuze + * @author Rossen Stoyanchev */ public class SimpleResultHandlerTests { - @Test - public void supports() throws NoSuchMethodException { - - SimpleResultHandler resultHandler = new SimpleResultHandler(); - TestController controller = new TestController(); - - HandlerMethod hm = new HandlerMethod(controller, TestController.class.getMethod("voidReturnValue")); - ResolvableType type = ResolvableType.forMethodParameter(hm.getReturnType()); - assertTrue(resultHandler.supports(createHandlerResult(hm, type))); - - hm = new HandlerMethod(controller, TestController.class.getMethod("publisherString")); - type = ResolvableType.forMethodParameter(hm.getReturnType()); - assertFalse(resultHandler.supports(createHandlerResult(hm, type))); - - hm = new HandlerMethod(controller, TestController.class.getMethod("publisherVoid")); - type = ResolvableType.forMethodParameter(hm.getReturnType()); - assertTrue(resultHandler.supports(createHandlerResult(hm, type))); - - hm = new HandlerMethod(controller, TestController.class.getMethod("streamVoid")); - type = ResolvableType.forMethodParameter(hm.getReturnType()); - // Reactor Flux is a Publisher - assertTrue(resultHandler.supports(createHandlerResult(hm, type))); - - hm = new HandlerMethod(controller, TestController.class.getMethod("observableVoid")); - type = ResolvableType.forMethodParameter(hm.getReturnType()); - assertFalse(resultHandler.supports(createHandlerResult(hm, type))); - - hm = new HandlerMethod(controller, TestController.class.getMethod("completableFutureVoid")); - type = ResolvableType.forMethodParameter(hm.getReturnType()); - assertFalse(resultHandler.supports(createHandlerResult(hm, type))); - } - @Test public void supportsWithConversionService() throws NoSuchMethodException { GenericConversionService conversionService = new GenericConversionService(); conversionService.addConverter(new ReactiveStreamsToCompletableFutureConverter()); conversionService.addConverter(new ReactiveStreamsToRxJava1Converter()); + SimpleResultHandler resultHandler = new SimpleResultHandler(conversionService); TestController controller = new TestController(); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/WebHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/WebHandlerIntegrationTests.java index 86624f9981..0f45e8826a 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/WebHandlerIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/WebHandlerIntegrationTests.java @@ -26,7 +26,12 @@ import org.junit.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import org.springframework.beans.MutablePropertyValues; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.context.support.StaticApplicationContext; +import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.HttpStatus; @@ -60,10 +65,8 @@ public class WebHandlerIntegrationTests extends AbstractHttpHandlerIntegrationTe @Override protected HttpHandler createHttpHandler() { - StaticApplicationContext wac = new StaticApplicationContext(); - wac.registerSingleton("handlerMapping", TestSimpleUrlHandlerMapping.class); - wac.registerSingleton("handlerAdapter", SimpleHandlerAdapter.class); - wac.registerSingleton("resultHandler", SimpleResultHandler.class); + AnnotationConfigApplicationContext wac = new AnnotationConfigApplicationContext(); + wac.register(WebConfig.class); wac.refresh(); DispatcherHandler dispatcherHandler = new DispatcherHandler(); @@ -172,4 +175,24 @@ public class WebHandlerIntegrationTests extends AbstractHttpHandlerIntegrationTe } } + @Configuration + static class WebConfig { + + @Bean + public TestSimpleUrlHandlerMapping handlerMapping() { + return new TestSimpleUrlHandlerMapping(); + } + + @Bean + public SimpleHandlerAdapter handlerAdapter() { + return new SimpleHandlerAdapter(); + } + + @Bean + public SimpleResultHandler resultHandler() { + return new SimpleResultHandler(new DefaultConversionService()); + } + + } + } From a37b2e3a847daac1bebd3bdf0f556fcb3d5b9d08 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 31 May 2016 16:27:06 -0400 Subject: [PATCH 376/498] Refactor View contract View now returns Mono rather than Flux which aligns more closely with the reactive HttpMessageConverter vs the Encoder. The change was prompted by the upcoming implementation of a View that delegates to an existing HttpMessageConverter e.g. for JSON, XML. The resulting change also brings the reactive View closer in spirit to the View from spring-webmvc which returns void. --- .../web/reactive/result/view/AbstractView.java | 8 +++++--- .../web/reactive/result/view/View.java | 5 +++-- .../result/view/ViewResolutionResultHandler.java | 9 ++------- .../result/view/freemarker/FreeMarkerView.java | 9 +++++---- .../result/view/UrlBasedViewResolverTests.java | 4 ++-- .../result/view/ViewResolutionResultHandlerTests.java | 6 ++++-- .../result/view/freemarker/FreeMarkerViewTests.java | 11 +++++------ 7 files changed, 26 insertions(+), 26 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/AbstractView.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/AbstractView.java index ecf8ab2c62..7e882596f7 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/AbstractView.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/AbstractView.java @@ -24,6 +24,7 @@ import java.util.Map; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; @@ -111,10 +112,10 @@ public abstract class AbstractView implements View, ApplicationContextAware { * @param contentType the content type selected to render with which should * match one of the {@link #getSupportedMediaTypes() supported media types}. * @param exchange the current exchange - * @return + * @return {@code Mono} to represent when and if rendering succeeds */ @Override - public Flux render(HandlerResult result, MediaType contentType, + public Mono render(HandlerResult result, MediaType contentType, ServerWebExchange exchange) { if (logger.isTraceEnabled()) { @@ -151,8 +152,9 @@ public abstract class AbstractView implements View, ApplicationContextAware { * @param renderAttributes combined output Map (never {@code null}), * with dynamic values taking precedence over static attributes * @param exchange current exchange + * @return {@code Mono} to represent when and if rendering succeeds */ - protected abstract Flux renderInternal(Map renderAttributes, + protected abstract Mono renderInternal(Map renderAttributes, ServerWebExchange exchange); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/View.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/View.java index cfc9866ba5..a1689b00ef 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/View.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/View.java @@ -19,6 +19,7 @@ import java.util.List; import java.util.Optional; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.MediaType; @@ -53,8 +54,8 @@ public interface View { * @param contentType the content type selected to render with which should * match one of the {@link #getSupportedMediaTypes() supported media types}. * @param exchange the current exchange - * @return the output stream + * @return {@code Mono} to represent when and if rendering succeeds */ - Flux render(HandlerResult result, MediaType contentType, ServerWebExchange exchange); + Mono render(HandlerResult result, MediaType contentType, ServerWebExchange exchange); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java index 52c3676c89..07ca656298 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java @@ -34,7 +34,6 @@ import org.springframework.core.Ordered; import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.convert.ConversionService; -import org.springframework.core.io.buffer.DataBuffer; import org.springframework.ui.Model; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -184,8 +183,7 @@ public class ViewResolutionResultHandler implements HandlerResultHandler, Ordere return viewMono.then(returnValue -> { if (returnValue instanceof View) { - Flux body = ((View) returnValue).render(result, null, exchange); - return exchange.getResponse().writeWith(body); + return ((View) returnValue).render(result, null, exchange); } else if (returnValue instanceof CharSequence) { String viewName = returnValue.toString(); @@ -194,10 +192,7 @@ public class ViewResolutionResultHandler implements HandlerResultHandler, Ordere .concatMap(resolver -> resolver.resolveViewName(viewName, locale)) .next() .otherwiseIfEmpty(handleUnresolvedViewName(viewName)) - .then(view -> { - Flux body = view.render(result, null, exchange); - return exchange.getResponse().writeWith(body); - }); + .then(view -> view.render(result, null, exchange)); } else { // Should not happen diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerView.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerView.java index 4ed672ece3..61841172c5 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerView.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerView.java @@ -30,6 +30,7 @@ import freemarker.template.SimpleHash; import freemarker.template.Template; import freemarker.template.Version; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactoryUtils; @@ -156,7 +157,7 @@ public class FreeMarkerView extends AbstractUrlBasedView { } @Override - protected Flux renderInternal(Map renderAttributes, ServerWebExchange exchange) { + protected Mono renderInternal(Map renderAttributes, ServerWebExchange exchange) { // Expose all standard FreeMarker hash models. SimpleHash freeMarkerModel = getTemplateModel(renderAttributes, exchange); if (logger.isDebugEnabled()) { @@ -170,12 +171,12 @@ public class FreeMarkerView extends AbstractUrlBasedView { } catch (IOException ex) { String message = "Could not load FreeMarker template for URL [" + getUrl() + "]"; - return Flux.error(new IllegalStateException(message, ex)); + return Mono.error(new IllegalStateException(message, ex)); } catch (Throwable ex) { - return Flux.error(ex); + return Mono.error(ex); } - return Flux.just(dataBuffer); + return exchange.getResponse().writeWith(Flux.just(dataBuffer)); } /** diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/UrlBasedViewResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/UrlBasedViewResolverTests.java index c913427e6e..d521a6e7ef 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/UrlBasedViewResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/UrlBasedViewResolverTests.java @@ -63,8 +63,8 @@ public class UrlBasedViewResolverTests { } @Override - protected Flux renderInternal(Map attributes, ServerWebExchange exchange) { - return Flux.empty(); + protected Mono renderInternal(Map attributes, ServerWebExchange exchange) { + return Mono.empty(); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java index 492a1b4a55..35132680ea 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java @@ -48,6 +48,7 @@ import org.springframework.http.MediaType; import org.springframework.http.server.reactive.MockServerHttpRequest; import org.springframework.http.server.reactive.MockServerHttpResponse; import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.ui.ExtendedModelMap; import org.springframework.ui.Model; import org.springframework.ui.ModelMap; @@ -324,10 +325,11 @@ public class ViewResolutionResultHandlerTests { } @Override - public Flux render(HandlerResult result, MediaType mediaType, ServerWebExchange exchange) { + public Mono render(HandlerResult result, MediaType mediaType, ServerWebExchange exchange) { String value = this.name + ": " + result.getModel().toString(); assertNotNull(value); - return Flux.just(asDataBuffer(value)); + ServerHttpResponse response = exchange.getResponse(); + return response.writeWith(Flux.just(asDataBuffer(value))); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerViewTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerViewTests.java index bb28907238..3ea29a6819 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerViewTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerViewTests.java @@ -19,14 +19,12 @@ import java.net.URI; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.util.Locale; -import java.util.Optional; import freemarker.template.Configuration; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; -import reactor.core.publisher.Flux; import reactor.core.test.TestSubscriber; import org.springframework.context.ApplicationContextException; @@ -46,7 +44,6 @@ import org.springframework.web.server.session.WebSessionManager; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; /** * @author Rossen Stoyanchev @@ -60,6 +57,8 @@ public class FreeMarkerViewTests { private ServerWebExchange exchange; + private MockServerHttpResponse response; + private GenericApplicationContext context; private Configuration freeMarkerConfig; @@ -83,7 +82,7 @@ public class FreeMarkerViewTests { fv.setApplicationContext(this.context); MockServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/path")); - MockServerHttpResponse response = new MockServerHttpResponse(); + this.response = new MockServerHttpResponse(); WebSessionManager manager = new DefaultWebSessionManager(); this.exchange = new DefaultServerWebExchange(request, response, manager); } @@ -127,10 +126,10 @@ public class FreeMarkerViewTests { ModelMap model = new ExtendedModelMap(); model.addAttribute("hello", "hi FreeMarker"); HandlerResult result = new HandlerResult(new Object(), "", ResolvableType.NONE, model); - Flux flux = view.render(result, null, this.exchange); + view.render(result, null, this.exchange); TestSubscriber subscriber = new TestSubscriber<>(); - subscriber.bindTo(flux).assertValuesWith(dataBuffer -> + subscriber.bindTo(this.response.getBody()).assertValuesWith(dataBuffer -> assertEquals("hi FreeMarker", asString(dataBuffer))); } From 5db1a54ff09e7baa5668161ae75acf5b181b2c85 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 31 May 2016 17:49:21 -0400 Subject: [PATCH 377/498] Add HttpMessageConverterView --- .../result/view/HttpMessageConverterView.java | 153 +++++++++++++++ .../view/HttpMessageConverterViewTests.java | 180 ++++++++++++++++++ 2 files changed, 333 insertions(+) create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/HttpMessageConverterView.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/HttpMessageConverterViewTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/HttpMessageConverterView.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/HttpMessageConverterView.java new file mode 100644 index 0000000000..b95dfe3f60 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/HttpMessageConverterView.java @@ -0,0 +1,153 @@ +/* + * 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.web.reactive.result.view; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.reactivestreams.Publisher; +import reactor.core.publisher.Mono; + +import org.springframework.core.ResolvableType; +import org.springframework.http.MediaType; +import org.springframework.http.converter.reactive.HttpMessageConverter; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.ui.ModelMap; +import org.springframework.util.Assert; +import org.springframework.web.reactive.HandlerResult; +import org.springframework.web.server.ServerWebExchange; + + +/** + * A {@link View} that delegates to an {@link HttpMessageConverter}. + * + * @author Rossen Stoyanchev + */ +public class HttpMessageConverterView implements View { + + private final HttpMessageConverter converter; + + private final Set modelKeys = new HashSet<>(4); + + private final List mediaTypes; + + + public HttpMessageConverterView(HttpMessageConverter converter) { + Assert.notNull(converter, "'converter' is required."); + this.converter = converter; + this.mediaTypes = converter.getWritableMediaTypes(); + } + + + public HttpMessageConverter getConverter() { + return this.converter; + } + + /** + * By default model attributes are filtered with + * {@link HttpMessageConverter#canWrite} to find the ones that can be + * rendered. Use this property to further narrow the list and consider only + * attribute(s) under specific model key(s). + *

    If more than one matching attribute is found, than a Map is rendered, + * or if the {@code Encoder} does not support rendering a {@code Map} then + * an exception is raised. + */ + public void setModelKeys(Set modelKeys) { + this.modelKeys.clear(); + if (modelKeys != null) { + this.modelKeys.addAll(modelKeys); + } + } + + /** + * Return the configured model keys. + */ + public final Set getModelKeys() { + return this.modelKeys; + } + + @Override + public List getSupportedMediaTypes() { + return this.mediaTypes; + } + + @Override + public Mono render(HandlerResult result, MediaType contentType, ServerWebExchange exchange) { + Object value = extractObjectToRender(result); + return applyConverter(value, contentType, exchange); + } + + protected Object extractObjectToRender(HandlerResult result) { + ModelMap model = result.getModel(); + Map map = new HashMap<>(model.size()); + for (Map.Entry entry : model.entrySet()) { + if (isEligibleAttribute(entry.getKey(), entry.getValue())) { + map.put(entry.getKey(), entry.getValue()); + } + } + if (map.isEmpty()) { + return null; + } + else if (map.size() == 1) { + return map.values().iterator().next(); + } + else if (getConverter().canWrite(ResolvableType.forClass(Map.class), null)) { + return map; + } + else { + throw new IllegalStateException( + "Multiple matching attributes found: " + map + ". " + + "However Map rendering is not supported by " + getConverter()); + } + } + + /** + * Whether the given model attribute key-value pair is eligible for encoding. + *

    The default implementation checks against the configured + * {@link #setModelKeys model keys} and whether the Encoder supports the + * value type. + */ + protected boolean isEligibleAttribute(String attributeName, Object attributeValue) { + ResolvableType type = ResolvableType.forClass(attributeValue.getClass()); + if (getModelKeys().isEmpty()) { + return getConverter().canWrite(type, null); + } + if (getModelKeys().contains(attributeName)) { + if (getConverter().canWrite(type, null)) { + return true; + } + throw new IllegalStateException( + "Model object [" + attributeValue + "] retrieved via key " + + "[" + attributeName + "] is not supported by " + getConverter()); + } + return false; + } + + @SuppressWarnings("unchecked") + private Mono applyConverter(Object value, MediaType contentType, ServerWebExchange exchange) { + if (value == null) { + return Mono.empty(); + } + Publisher stream = Mono.just((T) value); + ResolvableType type = ResolvableType.forClass(value.getClass()); + ServerHttpResponse response = exchange.getResponse(); + return ((HttpMessageConverter) getConverter()).write(stream, type, contentType, response); + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/HttpMessageConverterViewTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/HttpMessageConverterViewTests.java new file mode 100644 index 0000000000..f1b4fa1b82 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/HttpMessageConverterViewTests.java @@ -0,0 +1,180 @@ +/* + * 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.web.reactive.result.view; + + +import java.net.URI; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import reactor.core.test.TestSubscriber; + +import org.springframework.core.ResolvableType; +import org.springframework.core.codec.support.JacksonJsonEncoder; +import org.springframework.core.codec.support.Jaxb2Encoder; +import org.springframework.core.codec.support.Pojo; +import org.springframework.core.codec.support.StringEncoder; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.support.DataBufferTestUtils; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.converter.reactive.CodecHttpMessageConverter; +import org.springframework.http.converter.reactive.HttpMessageConverter; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.ui.ExtendedModelMap; +import org.springframework.ui.ModelMap; +import org.springframework.util.MimeType; +import org.springframework.web.reactive.HandlerResult; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.DefaultWebSessionManager; +import org.springframework.web.server.session.WebSessionManager; + +import static junit.framework.TestCase.assertTrue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + + +/** + * Unit tests for {@link HttpMessageConverterView}. + * @author Rossen Stoyanchev + */ +public class HttpMessageConverterViewTests { + + private HttpMessageConverterView view; + + private HandlerResult result; + + private ModelMap model; + + + @Before + public void setup() throws Exception { + HttpMessageConverter converter = new CodecHttpMessageConverter<>(new JacksonJsonEncoder()); + this.view = new HttpMessageConverterView(converter); + this.model = new ExtendedModelMap(); + this.result = new HandlerResult(new Object(), null, ResolvableType.NONE, model); + } + + + @Test + public void supportedMediaTypes() throws Exception { + List mimeTypes = Arrays.asList( + new MimeType("application", "json", StandardCharsets.UTF_8), + new MimeType("application", "*+json", StandardCharsets.UTF_8)); + + assertEquals(mimeTypes, this.view.getSupportedMediaTypes()); + } + + @Test + public void extractObject() throws Exception { + this.view.setModelKeys(Collections.singleton("foo2")); + this.model.addAttribute("foo1", "bar1"); + this.model.addAttribute("foo2", "bar2"); + this.model.addAttribute("foo3", "bar3"); + + assertEquals("bar2", this.view.extractObjectToRender(this.result)); + } + + @Test + public void extractObjectNoMatch() throws Exception { + this.view.setModelKeys(Collections.singleton("foo2")); + this.model.addAttribute("foo1", "bar1"); + + assertNull(this.view.extractObjectToRender(this.result)); + } + + @Test + public void extractObjectMultipleMatches() throws Exception { + this.view.setModelKeys(new HashSet<>(Arrays.asList("foo1", "foo2"))); + this.model.addAttribute("foo1", "bar1"); + this.model.addAttribute("foo2", "bar2"); + this.model.addAttribute("foo3", "bar3"); + + Object value = this.view.extractObjectToRender(this.result); + assertNotNull(value); + assertEquals(HashMap.class, value.getClass()); + + Map map = (Map) value; + assertEquals(2, map.size()); + assertEquals("bar1", map.get("foo1")); + assertEquals("bar2", map.get("foo2")); + } + + @Test + public void extractObjectMultipleMatchesNotSupported() throws Exception { + HttpMessageConverter converter = new CodecHttpMessageConverter<>(new StringEncoder()); + HttpMessageConverterView view = new HttpMessageConverterView(converter); + view.setModelKeys(new HashSet<>(Arrays.asList("foo1", "foo2"))); + this.model.addAttribute("foo1", "bar1"); + this.model.addAttribute("foo2", "bar2"); + + try { + view.extractObjectToRender(this.result); + fail(); + } + catch (IllegalStateException ex) { + String message = ex.getMessage(); + assertTrue(message, message.contains("Map rendering is not supported")); + } + } + + @Test + public void extractObjectNotSupported() throws Exception { + HttpMessageConverter converter = new CodecHttpMessageConverter<>(new Jaxb2Encoder()); + HttpMessageConverterView view = new HttpMessageConverterView(converter); + view.setModelKeys(new HashSet<>(Collections.singletonList("foo1"))); + this.model.addAttribute("foo1", "bar1"); + + try { + view.extractObjectToRender(this.result); + fail(); + } + catch (IllegalStateException ex) { + String message = ex.getMessage(); + assertTrue(message, message.contains("[foo1] is not supported")); + } + } + + @Test + public void render() throws Exception { + this.model.addAttribute("pojo", new Pojo("foo", "bar")); + this.view.setModelKeys(Collections.singleton("pojo")); + + MockServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/path")); + MockServerHttpResponse response = new MockServerHttpResponse(); + WebSessionManager manager = new DefaultWebSessionManager(); + ServerWebExchange exchange = new DefaultServerWebExchange(request, response, manager); + + this.view.render(result, MediaType.APPLICATION_JSON, exchange); + + new TestSubscriber().bindTo(response.getBody()) + .assertValuesWith(buf -> assertEquals("{\"foo\":\"foo\",\"bar\":\"bar\"}", + DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); + } + +} From 8cc72b320bc86e5acb380f3588d1ec926ed1e681 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 31 May 2016 21:51:24 -0400 Subject: [PATCH 378/498] View resolution with content negotiation ViewResolutionResultHandler and ResponseBodyResultHandler now share a common base class ContentNegotiatingResultHandlerSupport that supports content negotiation. For view resolution we compare against the supported media types of resolved View instances, which may include default View's delegating to an HttpMessageConverter (e.g. JSON, XML, rendering). --- .../web/reactive/result/view/View.java | 2 +- .../view/ViewResolutionResultHandler.java | 110 ++++++++++++------ .../ViewResolutionResultHandlerTests.java | 72 ++++++++++-- 3 files changed, 139 insertions(+), 45 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/View.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/View.java index a1689b00ef..a6e82eb517 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/View.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/View.java @@ -43,7 +43,7 @@ import org.springframework.web.server.ServerWebExchange; public interface View { /** - * Return the list of media types this encoder supports. + * Return the list of media types this View supports, or an empty list. */ List getSupportedMediaTypes(); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java index 07ca656298..716e5fca99 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java @@ -34,17 +34,20 @@ import org.springframework.core.Ordered; import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.convert.ConversionService; +import org.springframework.http.MediaType; import org.springframework.ui.Model; -import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.HandlerResultHandler; +import org.springframework.web.reactive.accept.HeaderContentTypeResolver; +import org.springframework.web.reactive.accept.RequestedContentTypeResolver; +import org.springframework.web.reactive.result.ContentNegotiatingResultHandlerSupport; +import org.springframework.web.server.NotAcceptableStatusException; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.util.HttpRequestPathHelper; - /** * {@code HandlerResultHandler} that encapsulates the view resolution algorithm * supporting the following return types: @@ -63,32 +66,46 @@ import org.springframework.web.util.HttpRequestPathHelper; * If a view is left unspecified (e.g. by returning {@code null} or a * model-related return value), a default view name is selected. * - *

    This result handler should be ordered late relative to other result - * handlers. See {@link #setOrder(int)} for more details. + *

    By default this resolver is ordered at {@link Ordered#LOWEST_PRECEDENCE} + * and generally needs to be late in the order since it interprets any String + * return value as a view name while others may interpret the same otherwise + * based on annotations (e.g. for {@code @ResponseBody}). * * @author Rossen Stoyanchev */ -public class ViewResolutionResultHandler implements HandlerResultHandler, Ordered { +public class ViewResolutionResultHandler extends ContentNegotiatingResultHandlerSupport + implements HandlerResultHandler, Ordered { private final List viewResolvers = new ArrayList<>(4); - private final ConversionService conversionService; - - private int order = Ordered.LOWEST_PRECEDENCE; + private final List defaultViews = new ArrayList<>(4); private final HttpRequestPathHelper pathHelper = new HttpRequestPathHelper(); + /** + * Constructor with {@code ViewResolver}s and a {@code ConversionService} only + * and creating a {@link HeaderContentTypeResolver}, i.e. using Accept header + * to determine the requested content type. + * @param resolvers the resolver to use + * @param conversionService for converting other reactive types (e.g. rx.Single) to Mono + */ + public ViewResolutionResultHandler(List resolvers, ConversionService conversionService) { + this(resolvers, conversionService, new HeaderContentTypeResolver()); + } + /** * Constructor with {@code ViewResolver}s tand a {@code ConversionService}. * @param resolvers the resolver to use - * @param service for converting other reactive types (e.g. rx.Single) to Mono + * @param conversionService for converting other reactive types (e.g. rx.Single) to Mono + * @param contentTypeResolver for resolving the requested content type */ - public ViewResolutionResultHandler(List resolvers, ConversionService service) { - Assert.notNull(service, "'conversionService' is required."); + public ViewResolutionResultHandler(List resolvers, ConversionService conversionService, + RequestedContentTypeResolver contentTypeResolver) { + + super(conversionService, contentTypeResolver); this.viewResolvers.addAll(resolvers); AnnotationAwareOrderComparator.sort(this.viewResolvers); - this.conversionService = service; } @@ -100,20 +117,18 @@ public class ViewResolutionResultHandler implements HandlerResultHandler, Ordere } /** - * Set the order for this result handler relative to others. - *

    By default this is set to {@link Ordered#LOWEST_PRECEDENCE} and - * generally needs to be used late in the order since it interprets any - * String return value as a view name while others may interpret the same - * otherwise based on annotations (e.g. for {@code @ResponseBody}). - * @param order the order + * Set the default views to consider always when resolving view names and + * trying to satisfy the best matching content type. */ - public void setOrder(int order) { - this.order = order; + public void setDefaultViews(List defaultViews) { + this.defaultViews.clear(); + if (defaultViews != null) { + this.defaultViews.addAll(defaultViews); + } } - @Override - public int getOrder() { - return this.order; + public List getDefaultViews() { + return this.defaultViews; } @Override @@ -125,7 +140,7 @@ public class ViewResolutionResultHandler implements HandlerResultHandler, Ordere if (isSupportedType(clazz)) { return true; } - if (this.conversionService.canConvert(clazz, Mono.class)) { + if (getConversionService().canConvert(clazz, Mono.class)) { clazz = result.getReturnValueType().getGeneric(0).getRawClass(); return isSupportedType(clazz); } @@ -155,10 +170,10 @@ public class ViewResolutionResultHandler implements HandlerResultHandler, Ordere ResolvableType elementType; ResolvableType returnType = result.getReturnValueType(); - if (this.conversionService.canConvert(returnType.getRawClass(), Mono.class)) { + if (getConversionService().canConvert(returnType.getRawClass(), Mono.class)) { Optional optionalValue = result.getReturnValue(); if (optionalValue.isPresent()) { - Mono converted = this.conversionService.convert(optionalValue.get(), Mono.class); + Mono converted = getConversionService().convert(optionalValue.get(), Mono.class); valueMono = converted.map(o -> o); } else { @@ -188,11 +203,8 @@ public class ViewResolutionResultHandler implements HandlerResultHandler, Ordere else if (returnValue instanceof CharSequence) { String viewName = returnValue.toString(); Locale locale = Locale.getDefault(); // TODO - return Flux.fromIterable(getViewResolvers()) - .concatMap(resolver -> resolver.resolveViewName(viewName, locale)) - .next() - .otherwiseIfEmpty(handleUnresolvedViewName(viewName)) - .then(view -> view.render(result, null, exchange)); + return resolveViewAndRender(viewName, locale, result, exchange); + } else { // Should not happen @@ -281,9 +293,39 @@ public class ViewResolutionResultHandler implements HandlerResultHandler, Ordere } } - private Mono handleUnresolvedViewName(String viewName) { - return Mono.error(new IllegalStateException( - "Could not resolve view with name '" + viewName + "'.")); + private Mono resolveViewAndRender(String viewName, Locale locale, + HandlerResult result, ServerWebExchange exchange) { + + return Flux.fromIterable(getViewResolvers()) + .concatMap(resolver -> resolver.resolveViewName(viewName, locale)) + .switchIfEmpty(Mono.error( + new IllegalStateException( + "Could not resolve view with name '" + viewName + "'."))) + .asList() + .then(views -> { + views.addAll(getDefaultViews()); + + List producibleTypes = getProducibleMediaTypes(views); + MediaType bestMediaType = selectMediaType(exchange, producibleTypes); + + if (bestMediaType != null) { + for (View view : views) { + for (MediaType supported : view.getSupportedMediaTypes()) { + if (supported.isCompatibleWith(bestMediaType)) { + return view.render(result, bestMediaType, exchange); + } + } + } + } + + return Mono.error(new NotAcceptableStatusException(producibleTypes)); + }); + } + + private List getProducibleMediaTypes(List views) { + List result = new ArrayList<>(); + views.forEach(view -> result.addAll(view.getSupportedMediaTypes())); + return result; } } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java index 35132680ea..6e1d59607d 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java @@ -47,7 +47,6 @@ import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.http.server.reactive.MockServerHttpRequest; import org.springframework.http.server.reactive.MockServerHttpResponse; -import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.ui.ExtendedModelMap; import org.springframework.ui.Model; @@ -55,7 +54,7 @@ import org.springframework.ui.ModelMap; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerResult; -import org.springframework.web.reactive.HandlerResultHandler; +import org.springframework.web.server.NotAcceptableStatusException; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange; import org.springframework.web.server.session.DefaultWebSessionManager; @@ -73,6 +72,8 @@ import static org.mockito.Mockito.mock; */ public class ViewResolutionResultHandlerTests { + private MockServerHttpRequest request; + private MockServerHttpResponse response; private ModelMap model; @@ -81,6 +82,8 @@ public class ViewResolutionResultHandlerTests { @Before public void setUp() throws Exception { this.model = new ExtendedModelMap().addAttribute("id", "123"); + this.request = new MockServerHttpRequest(HttpMethod.GET, new URI("/path")); + this.response = new MockServerHttpResponse(); } @@ -101,8 +104,8 @@ public class ViewResolutionResultHandlerTests { @Test public void order() throws Exception { - TestViewResolver resolver1 = new TestViewResolver(); - TestViewResolver resolver2 = new TestViewResolver(); + TestViewResolver resolver1 = new TestViewResolver(new String[] {}); + TestViewResolver resolver2 = new TestViewResolver(new String[] {}); resolver1.setOrder(2); resolver2.setOrder(1); @@ -226,6 +229,36 @@ public class ViewResolutionResultHandlerTests { .assertValuesWith(buf -> assertEquals("account: {id=123, testBean=TestBean[name=Joe]}", asString(buf))); } + @Test + public void selectBestMediaType() throws Exception { + TestView htmlView = new TestView("account"); + htmlView.setMediaTypes(Collections.singletonList(MediaType.TEXT_HTML)); + + TestView jsonView = new TestView("defaultView"); + jsonView.setMediaTypes(Collections.singletonList(MediaType.APPLICATION_JSON)); + + this.request.getHeaders().setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); + + handle("/account", "account", "handleString", + Collections.singletonList(new TestViewResolver(htmlView)), + Collections.singletonList(jsonView)); + + assertEquals(MediaType.APPLICATION_JSON, this.response.getHeaders().getContentType()); + new TestSubscriber().bindTo(this.response.getBody()) + .assertValuesWith(buf -> assertEquals("defaultView: {id=123}", asString(buf))); + } + + @Test + public void selectBestMediaTypeNotAcceptable() throws Exception { + TestView htmlView = new TestView("account"); + htmlView.setMediaTypes(Collections.singletonList(MediaType.TEXT_HTML)); + + this.request.getHeaders().setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); + + handle("/account", "account", "handleString", new TestViewResolver(htmlView)) + .assertError(NotAcceptableStatusException.class); + + } private void testSupports(String methodName, boolean supports) throws NoSuchMethodException { Method method = TestController.class.getMethod(methodName); @@ -246,18 +279,24 @@ public class ViewResolutionResultHandlerTests { private TestSubscriber handle(String path, Object value, String methodName, ViewResolver... resolvers) throws Exception { - List resolverList = Arrays.asList(resolvers); + return handle(path, value, methodName, Arrays.asList(resolvers), Collections.emptyList()); + } + + private TestSubscriber handle(String path, Object value, String methodName, + List resolvers, List defaultViews) throws Exception { + ConversionService conversionService = new DefaultConversionService(); - HandlerResultHandler handler = new ViewResolutionResultHandler(resolverList, conversionService); + ViewResolutionResultHandler handler = new ViewResolutionResultHandler(resolvers, conversionService); + handler.setDefaultViews(defaultViews); + Method method = TestController.class.getMethod(methodName); HandlerMethod handlerMethod = new HandlerMethod(new TestController(), method); ResolvableType type = ResolvableType.forMethodReturnType(method); HandlerResult handlerResult = new HandlerResult(handlerMethod, value, type, this.model); - ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI(path)); - this.response = new MockServerHttpResponse(); + this.request.setUri(new URI(path)); WebSessionManager sessionManager = new DefaultWebSessionManager(); - ServerWebExchange exchange = new DefaultServerWebExchange(request, this.response, sessionManager); + ServerWebExchange exchange = new DefaultServerWebExchange(this.request, this.response, sessionManager); Mono mono = handler.handleResult(exchange, handlerResult); @@ -289,6 +328,10 @@ public class ViewResolutionResultHandlerTests { Arrays.stream(viewNames).forEach(name -> this.views.put(name, new TestView(name))); } + public TestViewResolver(TestView... views) { + Arrays.stream(views).forEach(view -> this.views.put(view.getName(), view)); + } + public void setOrder(int order) { this.order = order; } @@ -310,6 +353,8 @@ public class ViewResolutionResultHandlerTests { private final String name; + private List mediaTypes = Collections.singletonList(MediaType.TEXT_PLAIN); + public TestView(String name) { this.name = name; @@ -319,9 +364,13 @@ public class ViewResolutionResultHandlerTests { return this.name; } + public void setMediaTypes(List mediaTypes) { + this.mediaTypes = mediaTypes; + } + @Override public List getSupportedMediaTypes() { - return null; + return this.mediaTypes; } @Override @@ -329,6 +378,9 @@ public class ViewResolutionResultHandlerTests { String value = this.name + ": " + result.getModel().toString(); assertNotNull(value); ServerHttpResponse response = exchange.getResponse(); + if (mediaType != null) { + response.getHeaders().setContentType(mediaType); + } return response.writeWith(Flux.just(asDataBuffer(value))); } } From a40a8b06bfba0f31d9a6e9ab209f84d8b58464aa Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 31 May 2016 22:14:17 -0400 Subject: [PATCH 379/498] Polish view resolution --- .../view/ViewResolutionResultHandler.java | 43 ++++++----- .../ViewResolutionResultHandlerTests.java | 75 ++++++++++--------- 2 files changed, 64 insertions(+), 54 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java index 716e5fca99..336037876e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java @@ -127,6 +127,9 @@ public class ViewResolutionResultHandler extends ContentNegotiatingResultHandler } } + /** + * Return the configured default {@code View}'s. + */ public List getDefaultViews() { return this.defaultViews; } @@ -187,46 +190,48 @@ public class ViewResolutionResultHandler extends ContentNegotiatingResultHandler } Mono viewMono; - if (isViewReturnType(result, elementType)) { - viewMono = valueMono.otherwiseIfEmpty(selectDefaultViewName(exchange, result)); + if (isViewNameOrReference(elementType, result)) { + Mono viewName = getDefaultViewNameMono(exchange, result); + viewMono = valueMono.otherwiseIfEmpty(viewName); } else { - viewMono = valueMono.map(value -> updateModel(result, value)) + viewMono = valueMono.map(value -> updateModel(value, result)) .defaultIfEmpty(result.getModel()) - .then(model -> selectDefaultViewName(exchange, result)); + .then(model -> getDefaultViewNameMono(exchange, result)); } - return viewMono.then(returnValue -> { - if (returnValue instanceof View) { - return ((View) returnValue).render(result, null, exchange); + return viewMono.then(view -> { + if (view instanceof View) { + return ((View) view).render(result, null, exchange); } - else if (returnValue instanceof CharSequence) { - String viewName = returnValue.toString(); + else if (view instanceof CharSequence) { + String viewName = view.toString(); Locale locale = Locale.getDefault(); // TODO - return resolveViewAndRender(viewName, locale, result, exchange); + return resolveAndRender(viewName, locale, result, exchange); } else { // Should not happen - return Mono.error(new IllegalStateException("Unexpected return value")); + return Mono.error(new IllegalStateException("Unexpected view type")); } }); } - private boolean isViewReturnType(HandlerResult result, ResolvableType elementType) { + private boolean isViewNameOrReference(ResolvableType elementType, HandlerResult result) { Class clazz = elementType.getRawClass(); return (View.class.isAssignableFrom(clazz) || (CharSequence.class.isAssignableFrom(clazz) && !hasModelAttributeAnnotation(result))); } - private Mono selectDefaultViewName(ServerWebExchange exchange, HandlerResult result) { - String defaultViewName = getDefaultViewName(exchange, result); + private Mono getDefaultViewNameMono(ServerWebExchange exchange, HandlerResult result) { + String defaultViewName = getDefaultViewName(result, exchange); if (defaultViewName != null) { return Mono.just(defaultViewName); } else { - return Mono.error(new IllegalStateException("Handler [" + result.getHandler() + "] " + - "neither returned a view name nor a View object")); + return Mono.error(new IllegalStateException( + "Handler [" + result.getHandler() + "] " + + "neither returned a view name nor a View object")); } } @@ -239,7 +244,7 @@ public class ViewResolutionResultHandler extends ContentNegotiatingResultHandler * processing will result in an IllegalStateException. */ @SuppressWarnings("UnusedParameters") - protected String getDefaultViewName(ServerWebExchange exchange, HandlerResult result) { + protected String getDefaultViewName(HandlerResult result, ServerWebExchange exchange) { String path = this.pathHelper.getLookupPathForRequest(exchange); if (path.startsWith("/")) { path = path.substring(1); @@ -250,7 +255,7 @@ public class ViewResolutionResultHandler extends ContentNegotiatingResultHandler return StringUtils.stripFilenameExtension(path); } - private Object updateModel(HandlerResult result, Object value) { + private Object updateModel(Object value, HandlerResult result) { if (value instanceof Model) { result.getModel().addAllAttributes(((Model) value).asMap()); } @@ -293,7 +298,7 @@ public class ViewResolutionResultHandler extends ContentNegotiatingResultHandler } } - private Mono resolveViewAndRender(String viewName, Locale locale, + private Mono resolveAndRender(String viewName, Locale locale, HandlerResult result, ServerWebExchange exchange) { return Flux.fromIterable(getViewResolvers()) diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java index 6e1d59607d..8742bc6c71 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java @@ -43,6 +43,7 @@ import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DefaultDataBufferFactory; +import org.springframework.core.io.buffer.support.DataBufferTestUtils; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.http.server.reactive.MockServerHttpRequest; @@ -88,7 +89,7 @@ public class ViewResolutionResultHandlerTests { @Test - public void supportsWithNullReturnValue() throws Exception { + public void supports() throws Exception { testSupports("handleString", true); testSupports("handleView", true); testSupports("handleMonoString", true); @@ -120,7 +121,8 @@ public class ViewResolutionResultHandlerTests { handle("/path", value, "handleView"); new TestSubscriber().bindTo(this.response.getBody()) - .assertValuesWith(buf -> assertEquals("account: {id=123}", asString(buf))); + .assertValuesWith(buf -> assertEquals("account: {id=123}", + DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); } @Test @@ -129,7 +131,8 @@ public class ViewResolutionResultHandlerTests { handle("/path", value, "handleMonoView"); new TestSubscriber().bindTo(this.response.getBody()) - .assertValuesWith(buf -> assertEquals("account: {id=123}", asString(buf))); + .assertValuesWith(buf -> assertEquals("account: {id=123}", + DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); } @Test @@ -139,16 +142,18 @@ public class ViewResolutionResultHandlerTests { TestSubscriber subscriber = new TestSubscriber<>(); subscriber.bindTo(this.response.getBody()) - .assertValuesWith(buf -> assertEquals("account: {id=123}", asString(buf))); + .assertValuesWith(buf -> assertEquals("account: {id=123}", + DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); } @Test - public void viewNameInMono() throws Exception { + public void viewNameMono() throws Exception { Object value = Mono.just("account"); handle("/path", value, "handleMonoString", new TestViewResolver("account")); new TestSubscriber().bindTo(this.response.getBody()) - .assertValuesWith(buf -> assertEquals("account: {id=123}", asString(buf))); + .assertValuesWith(buf -> assertEquals("account: {id=123}", + DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); } @Test @@ -158,7 +163,8 @@ public class ViewResolutionResultHandlerTests { new TestViewResolver("account"), new TestViewResolver("profile")); new TestSubscriber().bindTo(this.response.getBody()) - .assertValuesWith(buf -> assertEquals("profile: {id=123}", asString(buf))); + .assertValuesWith(buf -> assertEquals("profile: {id=123}", + DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); } @Test @@ -173,15 +179,18 @@ public class ViewResolutionResultHandlerTests { handle("/account", null, "handleString", resolver); new TestSubscriber().bindTo(this.response.getBody()) - .assertValuesWith(buf -> assertEquals("account: {id=123}", asString(buf))); + .assertValuesWith(buf -> assertEquals("account: {id=123}", + DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); handle("/account/", null, "handleString", resolver); new TestSubscriber().bindTo(this.response.getBody()) - .assertValuesWith(buf -> assertEquals("account: {id=123}", asString(buf))); + .assertValuesWith(buf -> assertEquals("account: {id=123}", + DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); handle("/account.123", null, "handleString", resolver); new TestSubscriber().bindTo(this.response.getBody()) - .assertValuesWith(buf -> assertEquals("account: {id=123}", asString(buf))); + .assertValuesWith(buf -> assertEquals("account: {id=123}", + DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); } @Test @@ -190,47 +199,52 @@ public class ViewResolutionResultHandlerTests { handle("/account", value, "handleMonoString", new TestViewResolver("account")); new TestSubscriber().bindTo(this.response.getBody()) - .assertValuesWith(buf -> assertEquals("account: {id=123}", asString(buf))); + .assertValuesWith(buf -> assertEquals("account: {id=123}", + DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); } @Test - public void model() throws Exception { + public void modelReturnValue() throws Exception { Model value = new ExtendedModelMap().addAttribute("name", "Joe"); handle("/account", value, "handleModel", new TestViewResolver("account")); new TestSubscriber().bindTo(this.response.getBody()) - .assertValuesWith(buf -> assertEquals("account: {id=123, name=Joe}", asString(buf))); + .assertValuesWith(buf -> assertEquals("account: {id=123, name=Joe}", + DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); } @Test - public void map() throws Exception { + public void mapReturnValue() throws Exception { Map value = Collections.singletonMap("name", "Joe"); handle("/account", value, "handleMap", new TestViewResolver("account")); new TestSubscriber().bindTo(this.response.getBody()) - .assertValuesWith(buf -> assertEquals("account: {id=123, name=Joe}", asString(buf))); + .assertValuesWith(buf -> assertEquals("account: {id=123, name=Joe}", + DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); } @Test - public void modelAttributeAnnotation() throws Exception { + public void modelAttributeAnnotationReturnValue() throws Exception { String value = "Joe"; handle("/account", value, "handleModelAttributeAnnotation", new TestViewResolver("account")); new TestSubscriber().bindTo(this.response.getBody()) - .assertValuesWith(buf -> assertEquals("account: {id=123, name=Joe}", asString(buf))); + .assertValuesWith(buf -> assertEquals("account: {id=123, name=Joe}", + DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); } @Test - public void testBean() throws Exception { + public void objectReturnValue() throws Exception { Object value = new TestBean("Joe"); handle("/account", value, "handleTestBean", new TestViewResolver("account")); new TestSubscriber().bindTo(this.response.getBody()) - .assertValuesWith(buf -> assertEquals("account: {id=123, testBean=TestBean[name=Joe]}", asString(buf))); + .assertValuesWith(buf -> assertEquals("account: {id=123, testBean=TestBean[name=Joe]}", + DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); } @Test - public void selectBestMediaType() throws Exception { + public void contentNegotiation() throws Exception { TestView htmlView = new TestView("account"); htmlView.setMediaTypes(Collections.singletonList(MediaType.TEXT_HTML)); @@ -245,11 +259,12 @@ public class ViewResolutionResultHandlerTests { assertEquals(MediaType.APPLICATION_JSON, this.response.getHeaders().getContentType()); new TestSubscriber().bindTo(this.response.getBody()) - .assertValuesWith(buf -> assertEquals("defaultView: {id=123}", asString(buf))); + .assertValuesWith(buf -> assertEquals("defaultView: {id=123}", + DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); } @Test - public void selectBestMediaTypeNotAcceptable() throws Exception { + public void contentNegotiationNotAcceptable() throws Exception { TestView htmlView = new TestView("account"); htmlView.setMediaTypes(Collections.singletonList(MediaType.TEXT_HTML)); @@ -304,18 +319,6 @@ public class ViewResolutionResultHandlerTests { return subscriber.bindTo(mono).await(Duration.ofSeconds(1)); } - private static DataBuffer asDataBuffer(String value) { - ByteBuffer byteBuffer = ByteBuffer.wrap(value.getBytes(Charset.forName("UTF-8"))); - return new DefaultDataBufferFactory().wrap(byteBuffer); - } - - private static String asString(DataBuffer dataBuffer) { - ByteBuffer byteBuffer = dataBuffer.asByteBuffer(); - final byte[] bytes = new byte[byteBuffer.remaining()]; - byteBuffer.get(bytes); - return new String(bytes, Charset.forName("UTF-8")); - } - private static class TestViewResolver implements ViewResolver, Ordered { @@ -381,7 +384,9 @@ public class ViewResolutionResultHandlerTests { if (mediaType != null) { response.getHeaders().setContentType(mediaType); } - return response.writeWith(Flux.just(asDataBuffer(value))); + ByteBuffer byteBuffer = ByteBuffer.wrap(value.getBytes(Charset.forName("UTF-8"))); + DataBuffer dataBuffer = new DefaultDataBufferFactory().wrap(byteBuffer); + return response.writeWith(Flux.just(dataBuffer)); } } From 65246f8cfdcda92288e96209edbbdad3a4321a80 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 1 Jun 2016 10:17:12 -0400 Subject: [PATCH 380/498] Remove FIXME on retain in ReactorServerHttpRequest The converters release so this is the right thing to do, not a temporary fix. --- .../http/server/reactive/ReactorServerHttpRequest.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java index bdd587c0ca..8defae9cb6 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java @@ -89,9 +89,7 @@ public class ReactorServerHttpRequest extends AbstractServerHttpRequest { @Override public Flux getBody() { - return this.channel.receive() - .retain() //FIXME Rogue reference holding - .map(dataBufferFactory::wrap); + return this.channel.receive().retain().map(this.dataBufferFactory::wrap); } } From 8cf5ea91f8f764229cac2fde619b37f01adf08d3 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Wed, 1 Jun 2016 17:55:53 +0200 Subject: [PATCH 381/498] Sync with TestSubscriber changes in reactor-core --- .../codec/support/ByteBufferDecoderTests.java | 9 +- .../codec/support/ByteBufferEncoderTests.java | 4 +- .../support/JacksonJsonDecoderTests.java | 5 +- .../support/JacksonJsonEncoderTests.java | 10 +- .../core/codec/support/Jaxb2DecoderTests.java | 72 ++++++-------- .../core/codec/support/Jaxb2EncoderTests.java | 5 +- .../codec/support/JsonObjectDecoderTests.java | 16 ++-- .../codec/support/ResourceDecoderTests.java | 10 +- .../codec/support/ResourceEncoderTests.java | 6 +- .../codec/support/StringDecoderTests.java | 26 +++--- .../codec/support/StringEncoderTests.java | 5 +- .../codec/support/XmlEventDecoderTests.java | 21 ++--- .../buffer/support/DataBufferUtilsTests.java | 43 +++++---- .../reactive/WebClientIntegrationTests.java | 93 ++++++++++--------- .../method/HandlerMethodMappingTests.java | 7 +- ...RequestMappingInfoHandlerMappingTests.java | 18 ++-- ...equestAttributesArgumentResolverTests.java | 6 +- ...ookieValueMethodArgumentResolverTests.java | 6 +- ...thVariableMethodArgumentResolverTests.java | 6 +- ...tAttributeMethodArgumentResolverTests.java | 6 +- ...uestHeaderMethodArgumentResolverTests.java | 6 +- ...questParamMethodArgumentResolverTests.java | 6 +- ...nAttributeMethodArgumentResolverTests.java | 6 +- .../view/HttpMessageConverterViewTests.java | 3 +- .../ViewResolutionResultHandlerTests.java | 33 ++++--- .../view/freemarker/FreeMarkerViewTests.java | 6 +- 26 files changed, 217 insertions(+), 217 deletions(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ByteBufferDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ByteBufferDecoderTests.java index 9408471bc2..cc59004367 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ByteBufferDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ByteBufferDecoderTests.java @@ -56,9 +56,10 @@ public class ByteBufferDecoderTests extends AbstractDataBufferAllocatingTestCase Flux output = this.decoder.decode(source, ResolvableType.forClassWithGenerics(Publisher.class, ByteBuffer.class), null); - TestSubscriber testSubscriber = new TestSubscriber<>(); - testSubscriber.bindTo(output).assertNoError().assertComplete() - .assertValues(ByteBuffer.wrap("foo".getBytes()), - ByteBuffer.wrap("bar".getBytes())); + TestSubscriber + .subscribe(output) + .assertNoError() + .assertComplete() + .assertValues(ByteBuffer.wrap("foo".getBytes()), ByteBuffer.wrap("bar".getBytes())); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ByteBufferEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ByteBufferEncoderTests.java index 611662dad9..60458231c6 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ByteBufferEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ByteBufferEncoderTests.java @@ -64,8 +64,8 @@ public class ByteBufferEncoderTests extends AbstractDataBufferAllocatingTestCase Flux output = this.encoder.encode(source, this.dataBufferFactory, ResolvableType.forClassWithGenerics(Publisher.class, ByteBuffer.class), null); - TestSubscriber testSubscriber = new TestSubscriber<>(); - testSubscriber.bindTo(output) + TestSubscriber + .subscribe(output) .assertValuesWith(b -> { byte[] buf = new byte[3]; b.read(buf); diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonDecoderTests.java index e8721702d4..788d610629 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonDecoderTests.java @@ -47,8 +47,9 @@ public class JacksonJsonDecoderTests extends AbstractDataBufferAllocatingTestCas Flux.just(stringBuffer("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}")); Flux output = this.decoder.decode(source, ResolvableType.forClass(Pojo.class), null); - TestSubscriber testSubscriber = new TestSubscriber<>(); - testSubscriber.bindTo(output).assertValues(new Pojo("foofoo", "barbar")); + TestSubscriber + .subscribe(output) + .assertValues(new Pojo("foofoo", "barbar")); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonEncoderTests.java index 2523899e57..7c2b7deee0 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonEncoderTests.java @@ -53,11 +53,11 @@ public class JacksonJsonEncoderTests extends AbstractDataBufferAllocatingTestCas Flux output = this.encoder.encode(source, this.dataBufferFactory, null, null); - TestSubscriber testSubscriber = new TestSubscriber<>(); - testSubscriber.bindTo(output). - assertComplete(). - assertNoError(). - assertValuesWith(stringConsumer("["), + TestSubscriber + .subscribe(output) + .assertComplete() + .assertNoError() + .assertValuesWith(stringConsumer("["), stringConsumer("{\"foo\":\"foofoo\",\"bar\":\"barbar\"}"), stringConsumer(","), stringConsumer("{\"foo\":\"foofoofoo\",\"bar\":\"barbarbar\"}"), diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2DecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2DecoderTests.java index ff5d60041e..afe3194fa9 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2DecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2DecoderTests.java @@ -88,11 +88,11 @@ public class Jaxb2DecoderTests extends AbstractDataBufferAllocatingTestCase { .decode(Flux.just(stringBuffer(POJO_ROOT)), null, null); Flux> result = this.decoder.split(xmlEvents, new QName("pojo")); - TestSubscriber> resultSubscriber = new TestSubscriber<>(); - resultSubscriber.bindTo(result). - assertNoError(). - assertComplete(). - assertValuesWith(events -> { + TestSubscriber + .subscribe(result) + .assertNoError() + .assertComplete() + .assertValuesWith(events -> { assertEquals(8, events.size()); assertStartElement(events.get(0), "pojo"); assertStartElement(events.get(1), "foo"); @@ -113,11 +113,11 @@ public class Jaxb2DecoderTests extends AbstractDataBufferAllocatingTestCase { .decode(Flux.just(stringBuffer(POJO_CHILD)), null, null); Flux> result = this.decoder.split(xmlEvents, new QName("pojo")); - TestSubscriber> resultSubscriber = new TestSubscriber<>(); - resultSubscriber.bindTo(result). - assertNoError(). - assertComplete(). - assertValuesWith(events -> { + TestSubscriber + .subscribe(result) + .assertNoError() + .assertComplete() + .assertValuesWith(events -> { assertEquals(8, events.size()); assertStartElement(events.get(0), "pojo"); assertStartElement(events.get(1), "foo"); @@ -161,14 +161,11 @@ public class Jaxb2DecoderTests extends AbstractDataBufferAllocatingTestCase { Flux output = this.decoder.decode(source, ResolvableType.forClass(Pojo.class), null); - TestSubscriber testSubscriber = new TestSubscriber<>(); - - testSubscriber.bindTo(output). - assertNoError(). - assertComplete(). - assertValues(new Pojo("foofoo", "barbar") - - ); + TestSubscriber + .subscribe(output) + .assertNoError() + .assertComplete() + .assertValues(new Pojo("foofoo", "barbar")); } @Test @@ -177,14 +174,11 @@ public class Jaxb2DecoderTests extends AbstractDataBufferAllocatingTestCase { Flux output = this.decoder .decode(source, ResolvableType.forClass(TypePojo.class), null); - TestSubscriber testSubscriber = new TestSubscriber<>(); - - testSubscriber.bindTo(output). - assertNoError(). - assertComplete(). - assertValues(new TypePojo("foofoo", "barbar") - - ); + TestSubscriber + .subscribe(output) + .assertNoError() + .assertComplete() + .assertValues(new TypePojo("foofoo", "barbar")); } @Test @@ -193,14 +187,11 @@ public class Jaxb2DecoderTests extends AbstractDataBufferAllocatingTestCase { Flux output = this.decoder.decode(source, ResolvableType.forClass(Pojo.class), null); - TestSubscriber testSubscriber = new TestSubscriber<>(); - - testSubscriber.bindTo(output). - assertNoError(). - assertComplete(). - assertValues(new Pojo("foo", "bar"), new Pojo("foofoo", "barbar") - - ); + TestSubscriber + .subscribe(output) + .assertNoError() + .assertComplete() + .assertValues(new Pojo("foo", "bar"), new Pojo("foofoo", "barbar")); } @Test @@ -209,14 +200,11 @@ public class Jaxb2DecoderTests extends AbstractDataBufferAllocatingTestCase { Flux output = this.decoder .decode(source, ResolvableType.forClass(TypePojo.class), null); - TestSubscriber testSubscriber = new TestSubscriber<>(); - - testSubscriber.bindTo(output). - assertNoError(). - assertComplete(). - assertValues(new TypePojo("foo", "bar"), new TypePojo("foofoo", "barbar") - - ); + TestSubscriber + .subscribe(output) + .assertNoError() + .assertComplete() + .assertValues(new TypePojo("foo", "bar"), new TypePojo("foofoo", "barbar")); } @Test diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2EncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2EncoderTests.java index 8d956741c0..dddeee0759 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2EncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2EncoderTests.java @@ -73,8 +73,9 @@ public class Jaxb2EncoderTests extends AbstractDataBufferAllocatingTestCase { Flux output = this.encoder.encode(source, this.dataBufferFactory, ResolvableType.forClass(Pojo.class), MediaType.APPLICATION_XML); - TestSubscriber testSubscriber = new TestSubscriber<>(); - testSubscriber.bindTo(output).assertValuesWith(dataBuffer -> { + TestSubscriber + .subscribe(output) + .assertValuesWith(dataBuffer -> { try { String s = DataBufferTestUtils .dumpString(dataBuffer, StandardCharsets.UTF_8); diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JsonObjectDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JsonObjectDecoderTests.java index 92aef3a8fe..21eddc7afe 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JsonObjectDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JsonObjectDecoderTests.java @@ -38,8 +38,8 @@ public class JsonObjectDecoderTests extends AbstractDataBufferAllocatingTestCase Flux.just(stringBuffer("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}")); Flux output = decoder.decode(source, null, null).map(JsonObjectDecoderTests::toString); - TestSubscriber testSubscriber = new TestSubscriber<>(); - testSubscriber.bindTo(output) + TestSubscriber + .subscribe(output) .assertValues("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}"); } @@ -50,8 +50,8 @@ public class JsonObjectDecoderTests extends AbstractDataBufferAllocatingTestCase stringBuffer(", \"bar\": \"barbar\"}")); Flux output = decoder.decode(source, null, null).map(JsonObjectDecoderTests::toString); - TestSubscriber testSubscriber = new TestSubscriber<>(); - testSubscriber.bindTo(output) + TestSubscriber + .subscribe(output) .assertValues("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}"); } @@ -62,8 +62,8 @@ public class JsonObjectDecoderTests extends AbstractDataBufferAllocatingTestCase "[{\"foo\": \"foofoo\", \"bar\": \"barbar\"},{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}]")); Flux output = decoder.decode(source, null, null).map(JsonObjectDecoderTests::toString); - TestSubscriber testSubscriber = new TestSubscriber<>(); - testSubscriber.bindTo(output) + TestSubscriber + .subscribe(output) .assertValues("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}", "{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}"); } @@ -76,8 +76,8 @@ public class JsonObjectDecoderTests extends AbstractDataBufferAllocatingTestCase ": \"barbar\"},{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}]")); Flux output = decoder.decode(source, null, null).map(JsonObjectDecoderTests::toString); - TestSubscriber testSubscriber = new TestSubscriber<>(); - testSubscriber.bindTo(output) + TestSubscriber + .subscribe(output) .assertValues("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}", "{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}"); } diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ResourceDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ResourceDecoderTests.java index 7688a1d036..d9372f2163 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ResourceDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ResourceDecoderTests.java @@ -64,11 +64,11 @@ public class ResourceDecoderTests extends AbstractDataBufferAllocatingTestCase { Flux result = this.decoder .decode(source, ResolvableType.forClass(Resource.class), null); - TestSubscriber testSubscriber = new TestSubscriber<>(); - testSubscriber.bindTo(result). - assertNoError(). - assertComplete(). - assertValuesWith(resource -> { + TestSubscriber + .subscribe(result) + .assertNoError() + .assertComplete() + .assertValuesWith(resource -> { try { byte[] bytes = StreamUtils.copyToByteArray(resource.getInputStream()); diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ResourceEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ResourceEncoderTests.java index 19b4c3bec9..9e28305c87 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ResourceEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ResourceEncoderTests.java @@ -66,8 +66,10 @@ public class ResourceEncoderTests extends AbstractDataBufferAllocatingTestCase { ResolvableType.forClass(Resource.class), null); - TestSubscriber testSubscriber = new TestSubscriber<>(); - testSubscriber.bindTo(output).assertNoError().assertComplete() + TestSubscriber + .subscribe(output) + .assertNoError() + .assertComplete() .assertValuesWith(stringConsumer(s)); } diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringDecoderTests.java index 34a8b0ae24..2178eab29c 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringDecoderTests.java @@ -65,11 +65,11 @@ public class StringDecoderTests extends AbstractDataBufferAllocatingTestCase { Flux.just(stringBuffer("foo"), stringBuffer("bar"), stringBuffer("baz")); Flux output = this.decoder.decode(source, ResolvableType.forClass(String.class), null); - TestSubscriber testSubscriber = new TestSubscriber<>(); - testSubscriber.bindTo(output). - assertNoError(). - assertComplete(). - assertValues("foo", "bar", "baz"); + TestSubscriber + .subscribe(output) + .assertNoError() + .assertComplete() + .assertValues("foo", "bar", "baz"); } @Test @@ -79,21 +79,21 @@ public class StringDecoderTests extends AbstractDataBufferAllocatingTestCase { Flux source = Flux.just(fooBar, baz); Flux output = decoder.decode(source, ResolvableType.forClass(String.class), null); - TestSubscriber testSubscriber = new TestSubscriber<>(); - testSubscriber.bindTo(output). - assertNoError(). - assertComplete(). - assertValues("foo", "bar", "baz"); + TestSubscriber + .subscribe(output) + .assertNoError() + .assertComplete() + .assertValues("foo", "bar", "baz"); } - @Test public void decodeEmpty() throws InterruptedException { Flux source = Flux.just(stringBuffer("")); Flux output = this.decoder.decode(source, ResolvableType.forClass(String.class), null); - TestSubscriber testSubscriber = new TestSubscriber<>(); - testSubscriber.bindTo(output).assertValues(""); + TestSubscriber + .subscribe(output) + .assertValues(""); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringEncoderTests.java index eb3062e8bc..296b562e75 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringEncoderTests.java @@ -66,8 +66,9 @@ public class StringEncoderTests extends AbstractDataBufferAllocatingTestCase { DataBufferUtils.release(chunk); return new String(b, StandardCharsets.UTF_8); }); - TestSubscriber testSubscriber = new TestSubscriber<>(); - testSubscriber.bindTo(output).assertValues("foo"); + TestSubscriber + .subscribe(output) + .assertValues("foo"); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/XmlEventDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/XmlEventDecoderTests.java index e1e0185b3a..430355d200 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/XmlEventDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/XmlEventDecoderTests.java @@ -46,11 +46,11 @@ public class XmlEventDecoderTests extends AbstractDataBufferAllocatingTestCase { Flux events = this.decoder.decode(Flux.just(stringBuffer(XML)), null, null); - TestSubscriber testSubscriber = new TestSubscriber<>(); - testSubscriber.bindTo(events). - assertNoError(). - assertComplete(). - assertValuesWith(e -> assertTrue(e.isStartDocument()), + TestSubscriber + .subscribe(events) + .assertNoError() + .assertComplete() + .assertValuesWith(e -> assertTrue(e.isStartDocument()), e -> assertStartElement(e, "pojo"), e -> assertStartElement(e, "foo"), e -> assertCharacters(e, "foofoo"), @@ -68,11 +68,11 @@ public class XmlEventDecoderTests extends AbstractDataBufferAllocatingTestCase { Flux events = this.decoder.decode(Flux.just(stringBuffer(XML)), null, null); - TestSubscriber testSubscriber = new TestSubscriber<>(); - testSubscriber.bindTo(events). - assertNoError(). - assertComplete(). - assertValuesWith(e -> assertTrue(e.isStartDocument()), + TestSubscriber + .subscribe(events) + .assertNoError() + .assertComplete() + .assertValuesWith(e -> assertTrue(e.isStartDocument()), e -> assertStartElement(e, "pojo"), e -> assertStartElement(e, "foo"), e -> assertCharacters(e, "foofoo"), @@ -98,5 +98,4 @@ public class XmlEventDecoderTests extends AbstractDataBufferAllocatingTestCase { assertEquals(expectedData, event.asCharacters().getData()); } - } \ No newline at end of file diff --git a/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/support/DataBufferUtilsTests.java b/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/support/DataBufferUtilsTests.java index 22e123cbf3..1a2cba08ec 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/support/DataBufferUtilsTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/support/DataBufferUtilsTests.java @@ -47,11 +47,12 @@ public class DataBufferUtilsTests extends AbstractDataBufferAllocatingTestCase { Flux flux = DataBufferUtils.read(channel, this.dataBufferFactory, 4); - TestSubscriber testSubscriber = new TestSubscriber<>(); - testSubscriber.bindTo(flux). - assertNoError(). - assertComplete(). - assertValuesWith(stringConsumer("foo\n"), stringConsumer("bar\n"), + TestSubscriber + .subscribe(flux) + .assertNoError() + .assertComplete() + .assertValuesWith( + stringConsumer("foo\n"), stringConsumer("bar\n"), stringConsumer("baz\n"), stringConsumer("qux\n")); assertFalse(channel.isOpen()); @@ -65,11 +66,12 @@ public class DataBufferUtilsTests extends AbstractDataBufferAllocatingTestCase { Flux flux = DataBufferUtils.read(channel, this.dataBufferFactory, 3); - TestSubscriber testSubscriber = new TestSubscriber<>(); - testSubscriber.bindTo(flux). - assertNoError(). - assertComplete(). - assertValuesWith(stringConsumer("foo"), stringConsumer("\nba"), + TestSubscriber + .subscribe(flux) + .assertNoError() + .assertComplete() + .assertValuesWith( + stringConsumer("foo"), stringConsumer("\nba"), stringConsumer("r\nb"), stringConsumer("az\n"), stringConsumer("qux"), stringConsumer("\n")); @@ -83,11 +85,12 @@ public class DataBufferUtilsTests extends AbstractDataBufferAllocatingTestCase { Flux flux = DataBufferUtils.read(is, this.dataBufferFactory, 4); - TestSubscriber testSubscriber = new TestSubscriber<>(); - testSubscriber.bindTo(flux). - assertNoError(). - assertComplete(). - assertValuesWith(stringConsumer("foo\n"), stringConsumer("bar\n"), + TestSubscriber + .subscribe(flux) + .assertNoError() + .assertComplete() + .assertValuesWith( + stringConsumer("foo\n"), stringConsumer("bar\n"), stringConsumer("baz\n"), stringConsumer("qux\n")); } @@ -100,11 +103,11 @@ public class DataBufferUtilsTests extends AbstractDataBufferAllocatingTestCase { Flux result = DataBufferUtils.takeUntilByteCount(flux, 5L); - TestSubscriber testSubscriber = new TestSubscriber<>(); - testSubscriber.bindTo(result). - assertNoError(). - assertComplete(). - assertValuesWith(stringConsumer("foo"), stringConsumer("ba")); + TestSubscriber + .subscribe(result) + .assertNoError() + .assertComplete() + .assertValuesWith(stringConsumer("foo"), stringConsumer("ba")); release(baz); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/client/reactive/WebClientIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/client/reactive/WebClientIntegrationTests.java index 7557e84082..0006c773d2 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/client/reactive/WebClientIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/client/reactive/WebClientIntegrationTests.java @@ -65,14 +65,14 @@ public class WebClientIntegrationTests { .perform(get(baseUrl.toString())) .extract(headers()); - TestSubscriber ts = new TestSubscriber<>(); - result.subscribe(ts); - ts.awaitAndAssertNextValuesWith( - httpHeaders -> { - assertEquals(MediaType.TEXT_PLAIN, httpHeaders.getContentType()); - assertEquals(13L, httpHeaders.getContentLength()); - } - ).assertComplete(); + TestSubscriber + .subscribe(result) + .awaitAndAssertNextValuesWith( + httpHeaders -> { + assertEquals(MediaType.TEXT_PLAIN, httpHeaders.getContentType()); + assertEquals(13L, httpHeaders.getContentLength()); + }) + .assertComplete(); RecordedRequest request = server.takeRequest(); assertEquals(1, server.getRequestCount()); @@ -92,9 +92,10 @@ public class WebClientIntegrationTests { .extract(body(String.class)); - TestSubscriber ts = new TestSubscriber<>(); - result.subscribe(ts); - ts.awaitAndAssertNextValues("Hello Spring!").assertComplete(); + TestSubscriber + .subscribe(result) + .awaitAndAssertNextValues("Hello Spring!") + .assertComplete(); RecordedRequest request = server.takeRequest(); assertEquals(1, server.getRequestCount()); @@ -114,12 +115,12 @@ public class WebClientIntegrationTests { .accept(MediaType.TEXT_PLAIN)) .extract(response(String.class)); - TestSubscriber> ts = new TestSubscriber<>(); - result.subscribe(ts); - ts.awaitAndAssertNextValuesWith((Consumer>) response -> { - assertEquals(200, response.getStatusCode().value()); - assertEquals(MediaType.TEXT_PLAIN, response.getHeaders().getContentType()); - assertEquals("Hello Spring!", response.getBody()); + TestSubscriber + .subscribe(result) + .awaitAndAssertNextValuesWith((Consumer>) response -> { + assertEquals(200, response.getStatusCode().value()); + assertEquals(MediaType.TEXT_PLAIN, response.getHeaders().getContentType()); + assertEquals("Hello Spring!", response.getBody()); }); RecordedRequest request = server.takeRequest(); assertEquals(1, server.getRequestCount()); @@ -140,9 +141,10 @@ public class WebClientIntegrationTests { .accept(MediaType.APPLICATION_JSON)) .extract(body(String.class)); - TestSubscriber ts = new TestSubscriber<>(); - result.subscribe(ts); - ts.awaitAndAssertNextValues(content).assertComplete(); + TestSubscriber + .subscribe(result) + .awaitAndAssertNextValues(content) + .assertComplete(); RecordedRequest request = server.takeRequest(); assertEquals(1, server.getRequestCount()); assertEquals("/json", request.getPath()); @@ -161,9 +163,10 @@ public class WebClientIntegrationTests { .accept(MediaType.APPLICATION_JSON)) .extract(body(Pojo.class)); - TestSubscriber ts = new TestSubscriber<>(); - result.subscribe(ts); - ts.awaitAndAssertNextValuesWith(p -> assertEquals("barbar", p.getBar())).assertComplete(); + TestSubscriber + .subscribe(result) + .awaitAndAssertNextValuesWith(p -> assertEquals("barbar", p.getBar())) + .assertComplete(); RecordedRequest request = server.takeRequest(); assertEquals(1, server.getRequestCount()); assertEquals("/pojo", request.getPath()); @@ -182,12 +185,13 @@ public class WebClientIntegrationTests { .accept(MediaType.APPLICATION_JSON)) .extract(bodyStream(Pojo.class)); - TestSubscriber ts = new TestSubscriber<>(); - result.subscribe(ts); - ts.awaitAndAssertNextValuesWith( - p -> assertThat(p.getBar(), Matchers.is("bar1")), - p -> assertThat(p.getBar(), Matchers.is("bar2")) - ).assertValueCount(2).assertComplete(); + TestSubscriber + .subscribe(result) + .awaitAndAssertNextValuesWith( + p -> assertThat(p.getBar(), Matchers.is("bar1")), + p -> assertThat(p.getBar(), Matchers.is("bar2"))) + .assertValueCount(2) + .assertComplete(); RecordedRequest request = server.takeRequest(); assertEquals(1, server.getRequestCount()); assertEquals("/pojos", request.getPath()); @@ -206,14 +210,14 @@ public class WebClientIntegrationTests { .accept(MediaType.APPLICATION_JSON)) .extract(responseStream(Pojo.class)); - TestSubscriber>> ts = new TestSubscriber<>(); - result.subscribe(ts); - ts.awaitAndAssertNextValuesWith( - response -> { - assertEquals(200, response.getStatusCode().value()); - assertEquals(MediaType.APPLICATION_JSON, response.getHeaders().getContentType()); - } - ).assertComplete(); + TestSubscriber + .subscribe(result) + .awaitAndAssertNextValuesWith( + response -> { + assertEquals(200, response.getStatusCode().value()); + assertEquals(MediaType.APPLICATION_JSON, response.getHeaders().getContentType()); + }) + .assertComplete(); RecordedRequest request = server.takeRequest(); assertEquals(1, server.getRequestCount()); assertEquals("/pojos", request.getPath()); @@ -234,9 +238,10 @@ public class WebClientIntegrationTests { .accept(MediaType.APPLICATION_JSON)) .extract(body(Pojo.class)); - TestSubscriber ts = new TestSubscriber<>(); - result.subscribe(ts); - ts.awaitAndAssertNextValuesWith(p -> assertEquals("BARBAR", p.getBar())).assertComplete(); + TestSubscriber + .subscribe(result) + .awaitAndAssertNextValuesWith(p -> assertEquals("BARBAR", p.getBar())) + .assertComplete(); RecordedRequest request = server.takeRequest(); assertEquals(1, server.getRequestCount()); @@ -257,11 +262,11 @@ public class WebClientIntegrationTests { .perform(get(baseUrl.toString())) .extract(body(String.class)); - - TestSubscriber ts = new TestSubscriber<>(); - result.subscribe(ts); // TODO: error message should be converted to a ClientException - ts.await().assertError(); + TestSubscriber + .subscribe(result) + .await() + .assertError(); RecordedRequest request = server.takeRequest(); assertEquals(1, server.getRequestCount()); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/HandlerMethodMappingTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/HandlerMethodMappingTests.java index 0bcda250d9..87b005f2a9 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/HandlerMethodMappingTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/HandlerMethodMappingTests.java @@ -38,7 +38,6 @@ import org.springframework.util.AntPathMatcher; import org.springframework.util.PathMatcher; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.method.HandlerMethod; -import org.springframework.web.reactive.result.method.AbstractHandlerMethodMapping; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange; import org.springframework.web.server.session.WebSessionManager; @@ -103,9 +102,9 @@ public class HandlerMethodMappingTests { this.mapping.registerMapping("/fo?", this.handler, this.method2); Mono result = this.mapping.getHandler(createExchange(HttpMethod.GET, "/foo")); - TestSubscriber subscriber = new TestSubscriber<>(); - result.subscribeWith(subscriber); - subscriber.assertError(IllegalStateException.class); + TestSubscriber + .subscribe(result) + .assertError(IllegalStateException.class); } @Test diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java index ada1c7a058..31f4c3b00c 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java @@ -160,9 +160,9 @@ public class RequestMappingInfoHandlerMappingTests { this.handlerMapping.registerHandler(new UserController()); Mono mono = this.handlerMapping.getHandler(exchange); - TestSubscriber subscriber = new TestSubscriber<>(); - mono.subscribeWith(subscriber); - subscriber.assertError(NotAcceptableStatusException.class); + TestSubscriber + .subscribe(mono) + .assertError(NotAcceptableStatusException.class); } // SPR-8462 @@ -357,12 +357,12 @@ public class RequestMappingInfoHandlerMappingTests { @SuppressWarnings("unchecked") private void assertError(Mono mono, final Class exceptionClass, final Consumer consumer) { - TestSubscriber subscriber = new TestSubscriber<>(); - mono.subscribeWith(subscriber); - subscriber.assertErrorWith(ex -> { - assertEquals(exceptionClass, ex.getClass()); - consumer.accept((T) ex); - }); + TestSubscriber + .subscribe(mono) + .assertErrorWith(ex -> { + assertEquals(exceptionClass, ex.getClass()); + consumer.accept((T) ex); + }); } @SuppressWarnings("ConstantConditions") diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/AbstractRequestAttributesArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/AbstractRequestAttributesArgumentResolverTests.java index ae1b4b9f25..6e12aea212 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/AbstractRequestAttributesArgumentResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/AbstractRequestAttributesArgumentResolverTests.java @@ -96,9 +96,9 @@ public abstract class AbstractRequestAttributesArgumentResolverTests { public void resolve() throws Exception { MethodParameter param = initMethodParameter(0); Mono mono = this.resolver.resolveArgument(param, null, this.exchange); - TestSubscriber subscriber = new TestSubscriber<>(); - mono.subscribeWith(subscriber); - subscriber.assertError(ServerWebInputException.class); + TestSubscriber + .subscribe(mono) + .assertError(ServerWebInputException.class); Foo foo = new Foo(); this.exchange.getAttributes().put("foo", foo); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/CookieValueMethodArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/CookieValueMethodArgumentResolverTests.java index e31b056e62..4951337b20 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/CookieValueMethodArgumentResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/CookieValueMethodArgumentResolverTests.java @@ -116,9 +116,9 @@ public class CookieValueMethodArgumentResolverTests { @Test public void notFound() { Mono mono = resolver.resolveArgument(this.cookieParameter, null, this.exchange); - TestSubscriber subscriber = new TestSubscriber<>(); - mono.subscribeWith(subscriber); - subscriber.assertError(ServerWebInputException.class); + TestSubscriber + .subscribe(mono) + .assertError(ServerWebInputException.class); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/PathVariableMethodArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/PathVariableMethodArgumentResolverTests.java index f79d713013..065c624903 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/PathVariableMethodArgumentResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/PathVariableMethodArgumentResolverTests.java @@ -98,9 +98,9 @@ public class PathVariableMethodArgumentResolverTests { @Test public void handleMissingValue() throws Exception { Mono mono = this.resolver.resolveArgument(this.paramNamedString, new ModelMap(), this.exchange); - TestSubscriber subscriber = new TestSubscriber<>(); - mono.subscribeWith(subscriber); - subscriber.assertError(ServerErrorException.class); + TestSubscriber + .subscribe(mono) + .assertError(ServerErrorException.class); } @SuppressWarnings("unused") diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestAttributeMethodArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestAttributeMethodArgumentResolverTests.java index 3d82d8e04e..000e695cbb 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestAttributeMethodArgumentResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestAttributeMethodArgumentResolverTests.java @@ -90,9 +90,9 @@ public class RequestAttributeMethodArgumentResolverTests { public void resolve() throws Exception { MethodParameter param = initMethodParameter(0); Mono mono = this.resolver.resolveArgument(param, null, this.exchange); - TestSubscriber subscriber = new TestSubscriber<>(); - mono.subscribeWith(subscriber); - subscriber.assertError(ServerWebInputException.class); + TestSubscriber + .subscribe(mono) + .assertError(ServerWebInputException.class); Foo foo = new Foo(); this.exchange.getAttributes().put("foo", foo); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolverTests.java index 14f93ef363..5b22d56eb5 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolverTests.java @@ -184,9 +184,9 @@ public class RequestHeaderMethodArgumentResolverTests { @Test public void notFound() throws Exception { Mono mono = resolver.resolveArgument(paramNamedValueStringArray, null, this.exchange); - TestSubscriber subscriber = new TestSubscriber<>(); - mono.subscribeWith(subscriber); - subscriber.assertError(ServerWebInputException.class); + TestSubscriber + .subscribe(mono) + .assertError(ServerWebInputException.class); } @Test diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestParamMethodArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestParamMethodArgumentResolverTests.java index 4e6979dfcc..b407348403 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestParamMethodArgumentResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestParamMethodArgumentResolverTests.java @@ -149,9 +149,9 @@ public class RequestParamMethodArgumentResolverTests { @Test public void missingRequestParam() throws Exception { Mono mono = this.resolver.resolveArgument(paramNamedStringArray, null, this.exchange); - TestSubscriber subscriber = new TestSubscriber<>(); - mono.subscribeWith(subscriber); - subscriber.assertError(ServerWebInputException.class); + TestSubscriber + .subscribe(mono) + .assertError(ServerWebInputException.class); } @Test diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SessionAttributeMethodArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SessionAttributeMethodArgumentResolverTests.java index 1bdbc9ce4a..a85187a085 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SessionAttributeMethodArgumentResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SessionAttributeMethodArgumentResolverTests.java @@ -97,9 +97,9 @@ public class SessionAttributeMethodArgumentResolverTests { public void resolve() throws Exception { MethodParameter param = initMethodParameter(0); Mono mono = this.resolver.resolveArgument(param, null, this.exchange); - TestSubscriber subscriber = new TestSubscriber<>(); - mono.subscribeWith(subscriber); - subscriber.assertError(ServerWebInputException.class); + TestSubscriber + .subscribe(mono) + .assertError(ServerWebInputException.class); Foo foo = new Foo(); when(this.session.getAttribute("foo")).thenReturn(Optional.of(foo)); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/HttpMessageConverterViewTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/HttpMessageConverterViewTests.java index f1b4fa1b82..0747d986f5 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/HttpMessageConverterViewTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/HttpMessageConverterViewTests.java @@ -172,7 +172,8 @@ public class HttpMessageConverterViewTests { this.view.render(result, MediaType.APPLICATION_JSON, exchange); - new TestSubscriber().bindTo(response.getBody()) + TestSubscriber + .subscribe(response.getBody()) .assertValuesWith(buf -> assertEquals("{\"foo\":\"foo\",\"bar\":\"bar\"}", DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java index 8742bc6c71..700ae0efed 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java @@ -120,7 +120,7 @@ public class ViewResolutionResultHandlerTests { Object value = new TestView("account"); handle("/path", value, "handleView"); - new TestSubscriber().bindTo(this.response.getBody()) + TestSubscriber.subscribe(this.response.getBody()) .assertValuesWith(buf -> assertEquals("account: {id=123}", DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); } @@ -130,7 +130,7 @@ public class ViewResolutionResultHandlerTests { Object value = Mono.just(new TestView("account")); handle("/path", value, "handleMonoView"); - new TestSubscriber().bindTo(this.response.getBody()) + TestSubscriber.subscribe(this.response.getBody()) .assertValuesWith(buf -> assertEquals("account: {id=123}", DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); } @@ -140,8 +140,8 @@ public class ViewResolutionResultHandlerTests { Object value = "account"; handle("/path", value, "handleString", new TestViewResolver("account")); - TestSubscriber subscriber = new TestSubscriber<>(); - subscriber.bindTo(this.response.getBody()) + TestSubscriber + .subscribe(this.response.getBody()) .assertValuesWith(buf -> assertEquals("account: {id=123}", DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); } @@ -151,7 +151,7 @@ public class ViewResolutionResultHandlerTests { Object value = Mono.just("account"); handle("/path", value, "handleMonoString", new TestViewResolver("account")); - new TestSubscriber().bindTo(this.response.getBody()) + TestSubscriber.subscribe(this.response.getBody()) .assertValuesWith(buf -> assertEquals("account: {id=123}", DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); } @@ -162,7 +162,7 @@ public class ViewResolutionResultHandlerTests { handle("/path", value, "handleString", new TestViewResolver("account"), new TestViewResolver("profile")); - new TestSubscriber().bindTo(this.response.getBody()) + TestSubscriber.subscribe(this.response.getBody()) .assertValuesWith(buf -> assertEquals("profile: {id=123}", DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); } @@ -178,17 +178,17 @@ public class ViewResolutionResultHandlerTests { ViewResolver resolver = new TestViewResolver("account"); handle("/account", null, "handleString", resolver); - new TestSubscriber().bindTo(this.response.getBody()) + TestSubscriber.subscribe(this.response.getBody()) .assertValuesWith(buf -> assertEquals("account: {id=123}", DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); handle("/account/", null, "handleString", resolver); - new TestSubscriber().bindTo(this.response.getBody()) + TestSubscriber.subscribe(this.response.getBody()) .assertValuesWith(buf -> assertEquals("account: {id=123}", DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); handle("/account.123", null, "handleString", resolver); - new TestSubscriber().bindTo(this.response.getBody()) + TestSubscriber.subscribe(this.response.getBody()) .assertValuesWith(buf -> assertEquals("account: {id=123}", DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); } @@ -198,7 +198,7 @@ public class ViewResolutionResultHandlerTests { Object value = Mono.empty(); handle("/account", value, "handleMonoString", new TestViewResolver("account")); - new TestSubscriber().bindTo(this.response.getBody()) + TestSubscriber.subscribe(this.response.getBody()) .assertValuesWith(buf -> assertEquals("account: {id=123}", DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); } @@ -208,7 +208,7 @@ public class ViewResolutionResultHandlerTests { Model value = new ExtendedModelMap().addAttribute("name", "Joe"); handle("/account", value, "handleModel", new TestViewResolver("account")); - new TestSubscriber().bindTo(this.response.getBody()) + TestSubscriber.subscribe(this.response.getBody()) .assertValuesWith(buf -> assertEquals("account: {id=123, name=Joe}", DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); } @@ -218,7 +218,7 @@ public class ViewResolutionResultHandlerTests { Map value = Collections.singletonMap("name", "Joe"); handle("/account", value, "handleMap", new TestViewResolver("account")); - new TestSubscriber().bindTo(this.response.getBody()) + TestSubscriber.subscribe(this.response.getBody()) .assertValuesWith(buf -> assertEquals("account: {id=123, name=Joe}", DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); } @@ -228,7 +228,7 @@ public class ViewResolutionResultHandlerTests { String value = "Joe"; handle("/account", value, "handleModelAttributeAnnotation", new TestViewResolver("account")); - new TestSubscriber().bindTo(this.response.getBody()) + TestSubscriber.subscribe(this.response.getBody()) .assertValuesWith(buf -> assertEquals("account: {id=123, name=Joe}", DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); } @@ -238,7 +238,7 @@ public class ViewResolutionResultHandlerTests { Object value = new TestBean("Joe"); handle("/account", value, "handleTestBean", new TestViewResolver("account")); - new TestSubscriber().bindTo(this.response.getBody()) + TestSubscriber.subscribe(this.response.getBody()) .assertValuesWith(buf -> assertEquals("account: {id=123, testBean=TestBean[name=Joe]}", DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); } @@ -258,7 +258,7 @@ public class ViewResolutionResultHandlerTests { Collections.singletonList(jsonView)); assertEquals(MediaType.APPLICATION_JSON, this.response.getHeaders().getContentType()); - new TestSubscriber().bindTo(this.response.getBody()) + TestSubscriber.subscribe(this.response.getBody()) .assertValuesWith(buf -> assertEquals("defaultView: {id=123}", DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); } @@ -315,8 +315,7 @@ public class ViewResolutionResultHandlerTests { Mono mono = handler.handleResult(exchange, handlerResult); - TestSubscriber subscriber = new TestSubscriber<>(); - return subscriber.bindTo(mono).await(Duration.ofSeconds(1)); + return TestSubscriber.subscribe(mono).await(Duration.ofSeconds(1)); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerViewTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerViewTests.java index 3ea29a6819..5407b2822b 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerViewTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerViewTests.java @@ -128,13 +128,13 @@ public class FreeMarkerViewTests { HandlerResult result = new HandlerResult(new Object(), "", ResolvableType.NONE, model); view.render(result, null, this.exchange); - TestSubscriber subscriber = new TestSubscriber<>(); - subscriber.bindTo(this.response.getBody()).assertValuesWith(dataBuffer -> + TestSubscriber + .subscribe(this.response.getBody()) + .assertValuesWith(dataBuffer -> assertEquals("hi FreeMarker", asString(dataBuffer))); } - private static String asString(DataBuffer dataBuffer) { ByteBuffer byteBuffer = dataBuffer.asByteBuffer(); final byte[] bytes = new byte[byteBuffer.remaining()]; From 431fedccc7ca95d48adedc6d9a7401b1fdbd3034 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 1 Jun 2016 17:32:05 -0400 Subject: [PATCH 382/498] Upgrade to RxJava 1.1.5 After the fix for ReactiveX/RxNetty#509 we can remove the workaround to concatenate with an empty Observable for the request body and upgrade to the latest RxJava 1.1.5. Issues: #103 --- spring-web-reactive/build.gradle | 2 +- .../http/server/reactive/RxNettyServerHttpRequest.java | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index b6811b7eff..7e18f1e4e1 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -30,7 +30,7 @@ ext { springVersion = '4.3.0.BUILD-SNAPSHOT' reactorVersion = '2.5.0.BUILD-SNAPSHOT' reactorNettyVersion = '2.5.0.BUILD-SNAPSHOT' - rxJavaVersion = '1.1.1' // See issue #103 + rxJavaVersion = '1.1.5' tomcatVersion = '8.5.2' jettyVersion = '9.3.8.v20160314' nettyVersion = '4.1.0.CR6' diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java index cd07391ade..fd5a5de1ef 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java @@ -93,9 +93,7 @@ public class RxNettyServerHttpRequest extends AbstractServerHttpRequest { @Override public Flux getBody() { - Observable content = - this.request.getContent().map(dataBufferFactory::wrap); - content = content.concatWith(Observable.empty()); // See GH issue #58 + Observable content = this.request.getContent().map(dataBufferFactory::wrap); return RxJava1ObservableConverter.from(content); } From 03a997c9d4ce6815d3a07c4d4b20b3f18554ed7c Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 1 Jun 2016 18:07:54 -0400 Subject: [PATCH 383/498] Remove outdated DataBufferFactory properties Removed in favor of accessing the factory from the response. --- .../reactive/result/view/AbstractView.java | 23 ------------------- .../result/view/UrlBasedViewResolver.java | 1 - .../result/view/ViewResolverSupport.java | 21 ----------------- .../view/freemarker/FreeMarkerView.java | 2 +- .../RequestMappingIntegrationTests.java | 13 ++++------- 5 files changed, 6 insertions(+), 54 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/AbstractView.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/AbstractView.java index 7e882596f7..b56f721a59 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/AbstractView.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/AbstractView.java @@ -23,14 +23,10 @@ import java.util.Map; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; -import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferFactory; -import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.MediaType; import org.springframework.ui.ModelMap; import org.springframework.util.Assert; @@ -49,8 +45,6 @@ public abstract class AbstractView implements View, ApplicationContextAware { private final List mediaTypes = new ArrayList<>(4); - private DataBufferFactory bufferAllocator = new DefaultDataBufferFactory(); - private ApplicationContext applicationContext; @@ -79,23 +73,6 @@ public abstract class AbstractView implements View, ApplicationContextAware { return this.mediaTypes; } - /** - * Configure the {@link DataBufferFactory} to use for write I/O. - *

    By default this is set to {@link DefaultDataBufferFactory}. - * @param bufferAllocator the factory to use - */ - public void setBufferAllocator(DataBufferFactory bufferAllocator) { - Assert.notNull(bufferAllocator, "'bufferAllocator' is required."); - this.bufferAllocator = bufferAllocator; - } - - /** - * Return the configured buffer factory, never {@code null}. - */ - public DataBufferFactory getBufferAllocator() { - return this.bufferAllocator; - } - @Override public void setApplicationContext(ApplicationContext applicationContext) { this.applicationContext = applicationContext; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/UrlBasedViewResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/UrlBasedViewResolver.java index 651205f385..510886fdac 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/UrlBasedViewResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/UrlBasedViewResolver.java @@ -193,7 +193,6 @@ public class UrlBasedViewResolver extends ViewResolverSupport implements ViewRes protected AbstractUrlBasedView createUrlBasedView(String viewName) { AbstractUrlBasedView view = (AbstractUrlBasedView) BeanUtils.instantiateClass(getViewClass()); view.setSupportedMediaTypes(getSupportedMediaTypes()); - view.setBufferAllocator(getBufferAllocator()); view.setUrl(getPrefix() + viewName + getSuffix()); return view; } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolverSupport.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolverSupport.java index 9d334dbaba..7c38a1fc12 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolverSupport.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolverSupport.java @@ -22,8 +22,6 @@ import java.util.List; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.core.Ordered; -import org.springframework.core.io.buffer.DataBufferFactory; -import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.MediaType; import org.springframework.util.Assert; @@ -40,8 +38,6 @@ public abstract class ViewResolverSupport implements ApplicationContextAware, Or private List mediaTypes = new ArrayList<>(4); - private DataBufferFactory bufferAllocator = new DefaultDataBufferFactory(); - private ApplicationContext applicationContext; private int order = Integer.MAX_VALUE; @@ -71,23 +67,6 @@ public abstract class ViewResolverSupport implements ApplicationContextAware, Or return this.mediaTypes; } - /** - * Configure the {@link DataBufferFactory} to use for write I/O. - *

    By default this is set to {@link DefaultDataBufferFactory}. - * @param bufferAllocator the factory to use - */ - public void setBufferAllocator(DataBufferFactory bufferAllocator) { - Assert.notNull(bufferAllocator, "'bufferAllocator' is required."); - this.bufferAllocator = bufferAllocator; - } - - /** - * Return the configured buffer factory, never {@code null}. - */ - public DataBufferFactory getBufferAllocator() { - return this.bufferAllocator; - } - @Override public void setApplicationContext(ApplicationContext applicationContext) { this.applicationContext = applicationContext; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerView.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerView.java index 61841172c5..8818d87355 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerView.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerView.java @@ -164,7 +164,7 @@ public class FreeMarkerView extends AbstractUrlBasedView { logger.debug("Rendering FreeMarker template [" + getUrl() + "]."); } Locale locale = Locale.getDefault(); // TODO - DataBuffer dataBuffer = getBufferAllocator().allocateBuffer(); + DataBuffer dataBuffer = exchange.getResponse().bufferFactory().allocateBuffer(); try { Writer writer = new OutputStreamWriter(dataBuffer.asOutputStream()); getTemplate(locale).process(freeMarkerModel, writer); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java index 561d407395..7ff5e36a67 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java @@ -82,7 +82,9 @@ import org.springframework.web.reactive.result.view.freemarker.FreeMarkerConfigu import org.springframework.web.reactive.result.view.freemarker.FreeMarkerViewResolver; import org.springframework.web.server.adapter.WebHttpHandlerBuilder; -import static org.junit.Assert.*; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; /** @@ -382,9 +384,6 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @SuppressWarnings("unused") static class FrameworkConfig { - private DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory(); - - @Bean public RequestMappingHandlerMapping handlerMapping() { return new RequestMappingHandlerMapping(); @@ -429,9 +428,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @Bean public ViewResolver freeMarkerViewResolver() { - FreeMarkerViewResolver viewResolver = new FreeMarkerViewResolver("", ".ftl"); - viewResolver.setBufferAllocator(this.dataBufferFactory); - return viewResolver; + return new FreeMarkerViewResolver("", ".ftl"); } @Bean @@ -564,7 +561,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @RequestMapping("/stream-create") public Publisher streamCreate(@RequestBody Flux personStream) { - return personStream.toList().doOnSuccess(persons::addAll).then(); + return personStream.asList().doOnSuccess(persons::addAll).then(); } @RequestMapping("/person-capitalize") From b20ea75766d9bb75883d35c109bf60be25f28340 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 3 Jun 2016 17:55:14 -0400 Subject: [PATCH 384/498] Update to latest Reactor snapshot --- .../web/reactive/result/view/ViewResolutionResultHandler.java | 2 +- .../reactive/result/method/InvocableHandlerMethodTests.java | 4 ++-- .../method/annotation/RequestMappingIntegrationTests.java | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java index 336037876e..bc28fbc543 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java @@ -306,7 +306,7 @@ public class ViewResolutionResultHandler extends ContentNegotiatingResultHandler .switchIfEmpty(Mono.error( new IllegalStateException( "Could not resolve view with name '" + viewName + "'."))) - .asList() + .collectList() .then(views -> { views.addAll(getDefaultViews()); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/InvocableHandlerMethodTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/InvocableHandlerMethodTests.java index 939c67e217..44a5302c36 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/InvocableHandlerMethodTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/InvocableHandlerMethodTests.java @@ -75,7 +75,7 @@ public class InvocableHandlerMethodTests { InvocableHandlerMethod hm = createHandlerMethod("noArgs"); Mono mono = hm.invokeForRequest(this.exchange, this.model); - HandlerResult value = mono.get(); + HandlerResult value = mono.block(); assertNotNull(value); assertEquals("success", value.getReturnValue().get()); @@ -204,7 +204,7 @@ public class InvocableHandlerMethodTests { } private Throwable awaitErrorSignal(Publisher publisher) throws Exception { - Signal signal = Flux.from(publisher).materialize().toList().get().get(0); + Signal signal = Flux.from(publisher).materialize().collectList().block().get(0); assertEquals("Unexpected signal: " + signal, SignalKind.onError, signal.getType()); return signal.getThrowable(); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java index 7ff5e36a67..c85b85acb7 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java @@ -561,7 +561,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @RequestMapping("/stream-create") public Publisher streamCreate(@RequestBody Flux personStream) { - return personStream.asList().doOnSuccess(persons::addAll).then(); + return personStream.collectList().doOnSuccess(persons::addAll).then(); } @RequestMapping("/person-capitalize") From a163938758a2a76dee538428d67cb39d4db0cf59 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 3 Jun 2016 17:42:54 -0400 Subject: [PATCH 385/498] Message converters are configurable --- .../RequestMappingHandlerAdapter.java | 50 +++++++++++-------- ...essionAttributeMethodArgumentResolver.java | 1 - .../view/freemarker/FreeMarkerView.java | 1 + .../reactive/DispatcherHandlerErrorTests.java | 9 ++-- .../RequestMappingIntegrationTests.java | 21 ++++++-- 5 files changed, 51 insertions(+), 31 deletions(-) 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 b6c0ec6848..a049b0dfc7 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 @@ -17,9 +17,7 @@ package org.springframework.web.reactive.result.method.annotation; import java.lang.reflect.Method; -import java.nio.ByteBuffer; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -34,16 +32,9 @@ import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.core.codec.support.ByteBufferDecoder; -import org.springframework.core.codec.support.ByteBufferEncoder; -import org.springframework.core.codec.support.JacksonJsonDecoder; -import org.springframework.core.codec.support.JacksonJsonEncoder; -import org.springframework.core.codec.support.Jaxb2Decoder; -import org.springframework.core.codec.support.Jaxb2Encoder; -import org.springframework.core.codec.support.JsonObjectDecoder; import org.springframework.core.codec.support.StringDecoder; -import org.springframework.core.codec.support.StringEncoder; import org.springframework.core.convert.ConversionService; -import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.http.converter.reactive.CodecHttpMessageConverter; import org.springframework.http.converter.reactive.HttpMessageConverter; import org.springframework.ui.ExtendedModelMap; @@ -68,13 +59,20 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory private final List argumentResolvers = new ArrayList<>(); - private ConversionService conversionService = new DefaultConversionService(); + private final List> messageConverters = new ArrayList<>(); - private final Map, ExceptionHandlerMethodResolver> exceptionHandlerCache = - new ConcurrentHashMap<>(64); + private ConversionService conversionService = new DefaultFormattingConversionService(); private ConfigurableBeanFactory beanFactory; + private final Map, ExceptionHandlerMethodResolver> exceptionHandlerCache = new ConcurrentHashMap<>(64); + + + + public RequestMappingHandlerAdapter() { + this.messageConverters.add(new CodecHttpMessageConverter<>(new ByteBufferDecoder())); + this.messageConverters.add(new CodecHttpMessageConverter<>(new StringDecoder())); + } /** @@ -93,6 +91,18 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory return this.argumentResolvers; } + public void setMessageConverters(List> messageConverters) { + this.messageConverters.clear(); + this.messageConverters.addAll(messageConverters); + } + + /** + * Provide the message converters to use for argument resolution. + */ + public List> getMessageConverters() { + return this.messageConverters; + } + public void setConversionService(ConversionService conversionService) { this.conversionService = conversionService; } @@ -121,12 +131,12 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory public void afterPropertiesSet() throws Exception { if (ObjectUtils.isEmpty(this.argumentResolvers)) { - List> converters = Arrays.asList( - new CodecHttpMessageConverter(new ByteBufferEncoder(), new ByteBufferDecoder()), - new CodecHttpMessageConverter(new StringEncoder(), new StringDecoder()), - new CodecHttpMessageConverter(new Jaxb2Encoder(), new Jaxb2Decoder()), - new CodecHttpMessageConverter(new JacksonJsonEncoder(), - new JacksonJsonDecoder(new JsonObjectDecoder()))); +// List> converters = Arrays.asList( +// new CodecHttpMessageConverter(new ByteBufferEncoder(), new ByteBufferDecoder()), +// new CodecHttpMessageConverter(new StringEncoder(), new StringDecoder()), +// new CodecHttpMessageConverter(new Jaxb2Encoder(), new Jaxb2Decoder()), +// new CodecHttpMessageConverter(new JacksonJsonEncoder(), +// new JacksonJsonDecoder(new JsonObjectDecoder()))); // Annotation-based argument resolution ConversionService cs = getConversionService(); @@ -134,7 +144,7 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory this.argumentResolvers.add(new RequestParamMapMethodArgumentResolver()); this.argumentResolvers.add(new PathVariableMethodArgumentResolver(cs, getBeanFactory())); this.argumentResolvers.add(new PathVariableMapMethodArgumentResolver()); - this.argumentResolvers.add(new RequestBodyArgumentResolver(converters, cs)); + this.argumentResolvers.add(new RequestBodyArgumentResolver(getMessageConverters(), cs)); this.argumentResolvers.add(new RequestHeaderMethodArgumentResolver(cs, getBeanFactory())); this.argumentResolvers.add(new RequestHeaderMapMethodArgumentResolver()); this.argumentResolvers.add(new CookieValueMethodArgumentResolver(cs, getBeanFactory())); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/SessionAttributeMethodArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/SessionAttributeMethodArgumentResolver.java index 153a6166da..a48b3ddb03 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/SessionAttributeMethodArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/SessionAttributeMethodArgumentResolver.java @@ -48,7 +48,6 @@ public class SessionAttributeMethodArgumentResolver extends AbstractNamedValueMe return parameter.hasParameterAnnotation(SessionAttribute.class); } - @Override protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) { SessionAttribute annot = parameter.getParameterAnnotation(SessionAttribute.class); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerView.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerView.java index 8818d87355..8caaa1c9a0 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerView.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerView.java @@ -166,6 +166,7 @@ public class FreeMarkerView extends AbstractUrlBasedView { Locale locale = Locale.getDefault(); // TODO DataBuffer dataBuffer = exchange.getResponse().bufferFactory().allocateBuffer(); try { + // TODO: pass charset Writer writer = new OutputStreamWriter(dataBuffer.asOutputStream()); getTemplate(locale).process(freeMarkerModel, writer); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java index c46460627c..02a1cd75d1 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java @@ -32,6 +32,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.codec.support.StringDecoder; import org.springframework.core.codec.support.StringEncoder; +import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DefaultDataBufferFactory; @@ -230,11 +231,9 @@ public class DispatcherHandlerErrorTests { @Bean public ResponseBodyResultHandler resultHandler() { - List> converters = Collections.singletonList( - new CodecHttpMessageConverter<>(new StringEncoder(), - new StringDecoder())); - return new ResponseBodyResultHandler(converters, - new DefaultConversionService()); + HttpMessageConverter converter = new CodecHttpMessageConverter<>(new StringEncoder()); + ConversionService conversionService = new DefaultConversionService(); + return new ResponseBodyResultHandler(Collections.singletonList(converter), conversionService); } @Bean diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java index c85b85acb7..217c52a6c2 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java @@ -44,6 +44,9 @@ import org.springframework.core.codec.support.ByteBufferDecoder; import org.springframework.core.codec.support.ByteBufferEncoder; import org.springframework.core.codec.support.JacksonJsonDecoder; import org.springframework.core.codec.support.JacksonJsonEncoder; +import org.springframework.core.codec.support.Jaxb2Decoder; +import org.springframework.core.codec.support.Jaxb2Encoder; +import org.springframework.core.codec.support.JsonObjectDecoder; import org.springframework.core.codec.support.StringDecoder; import org.springframework.core.codec.support.StringEncoder; import org.springframework.core.convert.ConversionService; @@ -392,10 +395,20 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @Bean public RequestMappingHandlerAdapter handlerAdapter() { RequestMappingHandlerAdapter handlerAdapter = new RequestMappingHandlerAdapter(); + handlerAdapter.setMessageConverters(getDefaultMessageConverters()); handlerAdapter.setConversionService(conversionService()); return handlerAdapter; } + private List> getDefaultMessageConverters() { + return Arrays.asList( + new CodecHttpMessageConverter<>(new ByteBufferEncoder(), new ByteBufferDecoder()), + new CodecHttpMessageConverter<>(new StringEncoder(), new StringDecoder()), + new CodecHttpMessageConverter<>(new Jaxb2Encoder(), new Jaxb2Decoder()), + new CodecHttpMessageConverter<>(new JacksonJsonEncoder(), + new JacksonJsonDecoder(new JsonObjectDecoder()))); + } + @Bean public ConversionService conversionService() { // TODO: test failures with DefaultConversionService @@ -407,11 +420,9 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @Bean public ResponseBodyResultHandler responseBodyResultHandler() { - List> converters = Arrays.asList( - new ResourceHttpMessageConverter(), - new CodecHttpMessageConverter<>(new ByteBufferEncoder(), new ByteBufferDecoder()), - new CodecHttpMessageConverter<>(new StringEncoder(), new StringDecoder()), - new CodecHttpMessageConverter<>(new JacksonJsonEncoder(), new JacksonJsonDecoder())); + List> converters = new ArrayList<>(); + converters.add(new ResourceHttpMessageConverter()); + converters.addAll(getDefaultMessageConverters()); return new ResponseBodyResultHandler(converters, conversionService()); } From b45a48d0fcc0d208e079d80cc583131164be59fb Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 6 Jun 2016 09:30:00 -0400 Subject: [PATCH 386/498] Support for custom argument resolvers --- .../RequestMappingHandlerAdapter.java | 104 ++++++++++++------ .../annotation/ResponseBodyResultHandler.java | 8 ++ 2 files changed, 77 insertions(+), 35 deletions(-) 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 a049b0dfc7..2cbc579437 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 @@ -50,6 +50,8 @@ import org.springframework.web.server.ServerWebExchange; /** + * Supports the invocation of {@code @RequestMapping} methods. + * * @author Rossen Stoyanchev */ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactoryAware, InitializingBean { @@ -57,9 +59,11 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory private static Log logger = LogFactory.getLog(RequestMappingHandlerAdapter.class); - private final List argumentResolvers = new ArrayList<>(); + private List customArgumentResolvers; - private final List> messageConverters = new ArrayList<>(); + private List argumentResolvers; + + private final List> messageConverters = new ArrayList<>(10); private ConversionService conversionService = new DefaultFormattingConversionService(); @@ -75,13 +79,26 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory } + /** + * Provide custom argument resolvers without overriding the built-in ones. + */ + public void setCustomArgumentResolvers(List argumentResolvers) { + this.customArgumentResolvers = argumentResolvers; + } + + /** + * Return the custom argument resolvers. + */ + public List getCustomArgumentResolvers() { + return this.customArgumentResolvers; + } + /** * Configure the complete list of supported argument types thus overriding * the resolvers that would otherwise be configured by default. */ public void setArgumentResolvers(List resolvers) { - this.argumentResolvers.clear(); - this.argumentResolvers.addAll(resolvers); + this.argumentResolvers = new ArrayList<>(resolvers); } /** @@ -91,22 +108,35 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory return this.argumentResolvers; } + /** + * Configure message converters to read the request body with. + */ public void setMessageConverters(List> messageConverters) { this.messageConverters.clear(); this.messageConverters.addAll(messageConverters); } /** - * Provide the message converters to use for argument resolution. + * Return the configured message converters. */ public List> getMessageConverters() { return this.messageConverters; } + /** + * Configure a ConversionService for type conversion of controller method + * arguments as well as for converting from different async types to + * {@code Flux} and {@code Mono}. + * + * TODO: this may be replaced by DataBinder + */ public void setConversionService(ConversionService conversionService) { this.conversionService = conversionService; } + /** + * Return the configured ConversionService. + */ public ConversionService getConversionService() { return this.conversionService; } @@ -123,43 +153,47 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory } public ConfigurableBeanFactory getBeanFactory() { - return beanFactory; + return this.beanFactory; } @Override public void afterPropertiesSet() throws Exception { - if (ObjectUtils.isEmpty(this.argumentResolvers)) { - -// List> converters = Arrays.asList( -// new CodecHttpMessageConverter(new ByteBufferEncoder(), new ByteBufferDecoder()), -// new CodecHttpMessageConverter(new StringEncoder(), new StringDecoder()), -// new CodecHttpMessageConverter(new Jaxb2Encoder(), new Jaxb2Decoder()), -// new CodecHttpMessageConverter(new JacksonJsonEncoder(), -// new JacksonJsonDecoder(new JsonObjectDecoder()))); - - // Annotation-based argument resolution - ConversionService cs = getConversionService(); - this.argumentResolvers.add(new RequestParamMethodArgumentResolver(cs, getBeanFactory(), false)); - this.argumentResolvers.add(new RequestParamMapMethodArgumentResolver()); - this.argumentResolvers.add(new PathVariableMethodArgumentResolver(cs, getBeanFactory())); - this.argumentResolvers.add(new PathVariableMapMethodArgumentResolver()); - this.argumentResolvers.add(new RequestBodyArgumentResolver(getMessageConverters(), cs)); - this.argumentResolvers.add(new RequestHeaderMethodArgumentResolver(cs, getBeanFactory())); - this.argumentResolvers.add(new RequestHeaderMapMethodArgumentResolver()); - this.argumentResolvers.add(new CookieValueMethodArgumentResolver(cs, getBeanFactory())); - this.argumentResolvers.add(new ExpressionValueMethodArgumentResolver(cs, getBeanFactory())); - this.argumentResolvers.add(new SessionAttributeMethodArgumentResolver(cs, getBeanFactory())); - this.argumentResolvers.add(new RequestAttributeMethodArgumentResolver(cs , getBeanFactory())); - - // Type-based argument resolution - this.argumentResolvers.add(new ModelArgumentResolver()); - - // Catch-all - this.argumentResolvers.add(new RequestParamMethodArgumentResolver(cs, getBeanFactory(), true)); + if (this.argumentResolvers == null) { + this.argumentResolvers = initArgumentResolvers(); } } + protected List initArgumentResolvers() { + List resolvers = new ArrayList<>(); + + // Annotation-based argument resolution + ConversionService cs = getConversionService(); + resolvers.add(new RequestParamMethodArgumentResolver(cs, getBeanFactory(), false)); + resolvers.add(new RequestParamMapMethodArgumentResolver()); + resolvers.add(new PathVariableMethodArgumentResolver(cs, getBeanFactory())); + resolvers.add(new PathVariableMapMethodArgumentResolver()); + resolvers.add(new RequestBodyArgumentResolver(getMessageConverters(), cs)); + resolvers.add(new RequestHeaderMethodArgumentResolver(cs, getBeanFactory())); + resolvers.add(new RequestHeaderMapMethodArgumentResolver()); + resolvers.add(new CookieValueMethodArgumentResolver(cs, getBeanFactory())); + resolvers.add(new ExpressionValueMethodArgumentResolver(cs, getBeanFactory())); + resolvers.add(new SessionAttributeMethodArgumentResolver(cs, getBeanFactory())); + resolvers.add(new RequestAttributeMethodArgumentResolver(cs , getBeanFactory())); + + // Type-based argument resolution + resolvers.add(new ModelArgumentResolver()); + + // Custom resolvers + if (getCustomArgumentResolvers() != null) { + resolvers.addAll(getCustomArgumentResolvers()); + } + + // Catch-all + resolvers.add(new RequestParamMethodArgumentResolver(cs, getBeanFactory(), true)); + return resolvers; + } + @Override public boolean supports(Object handler) { return HandlerMethod.class.equals(handler.getClass()); @@ -169,7 +203,7 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory public Mono handle(ServerWebExchange exchange, Object handler) { HandlerMethod handlerMethod = (HandlerMethod) handler; InvocableHandlerMethod invocable = new InvocableHandlerMethod(handlerMethod); - invocable.setHandlerMethodArgumentResolvers(this.argumentResolvers); + invocable.setHandlerMethodArgumentResolvers(getArgumentResolvers()); ModelMap model = new ExtendedModelMap(); return invocable.invokeForRequest(exchange, model) .map(result -> result.setExceptionHandler(ex -> handleException(ex, handlerMethod, exchange))) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java index a2107aa77d..56f7886ffe 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java @@ -78,6 +78,14 @@ public class ResponseBodyResultHandler extends ContentNegotiatingResultHandlerSu this(converters, conversionService, new HeaderContentTypeResolver()); } + + /** + * Return the configured message converters. + */ + public List> getMessageConverters() { + return this.messageConverters; + } + /** * Constructor with message converters, a {@code ConversionService}, and a * {@code RequestedContentTypeResolver}. From 505569c9923c01102cf8afecde2ed1bf02e71f69 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 6 Jun 2016 09:35:10 -0400 Subject: [PATCH 387/498] Add Encoder constructor to HttpMessageConverterView --- .../result/view/HttpMessageConverterView.java | 18 ++++++++++++++++++ .../reactive/DispatcherHandlerErrorTests.java | 6 +++--- .../view/HttpMessageConverterViewTests.java | 12 +++--------- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/HttpMessageConverterView.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/HttpMessageConverterView.java index b95dfe3f60..e0ecd8df66 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/HttpMessageConverterView.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/HttpMessageConverterView.java @@ -25,7 +25,9 @@ import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; import org.springframework.core.ResolvableType; +import org.springframework.core.codec.Encoder; import org.springframework.http.MediaType; +import org.springframework.http.converter.reactive.CodecHttpMessageConverter; import org.springframework.http.converter.reactive.HttpMessageConverter; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.ui.ModelMap; @@ -48,6 +50,19 @@ public class HttpMessageConverterView implements View { private final List mediaTypes; + /** + * Create a {@code View} with the given {@code Encoder}. + * Internally this creates + * {@link CodecHttpMessageConverter#CodecHttpMessageConverter(Encoder) + * CodecHttpMessageConverter(Encoder)}. + */ + public HttpMessageConverterView(Encoder encoder) { + this(new CodecHttpMessageConverter<>(encoder)); + } + + /** + * Create a View that delegates to the given message converter. + */ public HttpMessageConverterView(HttpMessageConverter converter) { Assert.notNull(converter, "'converter' is required."); this.converter = converter; @@ -55,6 +70,9 @@ public class HttpMessageConverterView implements View { } + /** + * Return the configured message converter. + */ public HttpMessageConverter getConverter() { return this.converter; } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java index 02a1cd75d1..bbea4c62d1 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java @@ -18,7 +18,6 @@ package org.springframework.web.reactive; import java.net.URI; import java.util.Collections; -import java.util.List; import org.junit.Before; import org.junit.Test; @@ -30,7 +29,6 @@ import reactor.core.util.SignalKind; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.core.codec.support.StringDecoder; import org.springframework.core.codec.support.StringEncoder; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.DefaultConversionService; @@ -63,7 +61,9 @@ import org.springframework.web.server.handler.FilteringWebHandler; import org.springframework.web.server.session.WebSessionManager; import static org.hamcrest.CoreMatchers.startsWith; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThat; import static org.mockito.Mockito.mock; /** diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/HttpMessageConverterViewTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/HttpMessageConverterViewTests.java index 0747d986f5..d840a572bc 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/HttpMessageConverterViewTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/HttpMessageConverterViewTests.java @@ -35,12 +35,9 @@ import org.springframework.core.codec.support.JacksonJsonEncoder; import org.springframework.core.codec.support.Jaxb2Encoder; import org.springframework.core.codec.support.Pojo; import org.springframework.core.codec.support.StringEncoder; -import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.support.DataBufferTestUtils; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; -import org.springframework.http.converter.reactive.CodecHttpMessageConverter; -import org.springframework.http.converter.reactive.HttpMessageConverter; import org.springframework.http.server.reactive.MockServerHttpRequest; import org.springframework.http.server.reactive.MockServerHttpResponse; import org.springframework.ui.ExtendedModelMap; @@ -74,8 +71,7 @@ public class HttpMessageConverterViewTests { @Before public void setup() throws Exception { - HttpMessageConverter converter = new CodecHttpMessageConverter<>(new JacksonJsonEncoder()); - this.view = new HttpMessageConverterView(converter); + this.view = new HttpMessageConverterView(new JacksonJsonEncoder()); this.model = new ExtendedModelMap(); this.result = new HandlerResult(new Object(), null, ResolvableType.NONE, model); } @@ -127,8 +123,7 @@ public class HttpMessageConverterViewTests { @Test public void extractObjectMultipleMatchesNotSupported() throws Exception { - HttpMessageConverter converter = new CodecHttpMessageConverter<>(new StringEncoder()); - HttpMessageConverterView view = new HttpMessageConverterView(converter); + HttpMessageConverterView view = new HttpMessageConverterView(new StringEncoder()); view.setModelKeys(new HashSet<>(Arrays.asList("foo1", "foo2"))); this.model.addAttribute("foo1", "bar1"); this.model.addAttribute("foo2", "bar2"); @@ -145,8 +140,7 @@ public class HttpMessageConverterViewTests { @Test public void extractObjectNotSupported() throws Exception { - HttpMessageConverter converter = new CodecHttpMessageConverter<>(new Jaxb2Encoder()); - HttpMessageConverterView view = new HttpMessageConverterView(converter); + HttpMessageConverterView view = new HttpMessageConverterView(new Jaxb2Encoder()); view.setModelKeys(new HashSet<>(Collections.singletonList("foo1"))); this.model.addAttribute("foo1", "bar1"); From 03b474edfe6252c8cf34da95b8f908b88b08bbfd Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 6 Jun 2016 09:42:09 -0400 Subject: [PATCH 388/498] Add Web Reactive Java config --- .../reactive/config/PathMatchConfigurer.java | 110 ++++++ .../UrlBasedViewResolverRegistration.java | 79 +++++ .../reactive/config/ViewResolverRegistry.java | 146 ++++++++ .../config/WebReactiveConfiguration.java | 316 ++++++++++++++++++ .../web/reactive/config/package-info.java | 4 + .../config/ViewResolverRegistryTests.java | 90 +++++ .../config/WebReactiveConfigurationTests.java | 277 +++++++++++++++ .../RequestMappingIntegrationTests.java | 80 +---- 8 files changed, 1028 insertions(+), 74 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/config/PathMatchConfigurer.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/config/UrlBasedViewResolverRegistration.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/config/ViewResolverRegistry.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/config/package-info.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/config/ViewResolverRegistryTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/config/WebReactiveConfigurationTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/PathMatchConfigurer.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/PathMatchConfigurer.java new file mode 100644 index 0000000000..c45565372a --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/PathMatchConfigurer.java @@ -0,0 +1,110 @@ +/* + * 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.web.reactive.config; + +import org.springframework.util.PathMatcher; +import org.springframework.web.util.HttpRequestPathHelper; + +/** + * Assist with configuring {@code HandlerMapping}'s with path matching options. + * + * @author Rossen Stoyanchev + */ +public class PathMatchConfigurer { + + private Boolean suffixPatternMatch; + + private Boolean trailingSlashMatch; + + private Boolean registeredSuffixPatternMatch; + + private HttpRequestPathHelper pathHelper; + + private PathMatcher pathMatcher; + + + /** + * Whether to use suffix pattern match (".*") when matching patterns to + * requests. If enabled a method mapped to "/users" also matches to "/users.*". + *

    By default this is set to {@code true}. + * @see #registeredSuffixPatternMatch + */ + public PathMatchConfigurer setUseSuffixPatternMatch(Boolean suffixPatternMatch) { + this.suffixPatternMatch = suffixPatternMatch; + return this; + } + + /** + * Whether to match to URLs irrespective of the presence of a trailing slash. + * If enabled a method mapped to "/users" also matches to "/users/". + *

    The default value is {@code true}. + */ + public PathMatchConfigurer setUseTrailingSlashMatch(Boolean trailingSlashMatch) { + this.trailingSlashMatch = trailingSlashMatch; + return this; + } + + /** + * Whether suffix pattern matching should work only against path extensions + * that are explicitly registered. This is generally recommended to reduce + * ambiguity and to avoid issues such as when a "." (dot) appears in the path + * for other reasons. + *

    By default this is set to "true". + */ + public PathMatchConfigurer setUseRegisteredSuffixPatternMatch(Boolean registeredSuffixPatternMatch) { + this.registeredSuffixPatternMatch = registeredSuffixPatternMatch; + return this; + } + + /** + * Set a {@code HttpRequestPathHelper} for the resolution of lookup paths. + *

    Default is {@code HttpRequestPathHelper}. + */ + public PathMatchConfigurer setPathHelper(HttpRequestPathHelper pathHelper) { + this.pathHelper = pathHelper; + return this; + } + + /** + * Set the PathMatcher for matching URL paths against registered URL patterns. + *

    Default is {@link org.springframework.util.AntPathMatcher AntPathMatcher}. + */ + public PathMatchConfigurer setPathMatcher(PathMatcher pathMatcher) { + this.pathMatcher = pathMatcher; + return this; + } + + protected Boolean isUseSuffixPatternMatch() { + return this.suffixPatternMatch; + } + + protected Boolean isUseTrailingSlashMatch() { + return this.trailingSlashMatch; + } + + protected Boolean isUseRegisteredSuffixPatternMatch() { + return this.registeredSuffixPatternMatch; + } + + protected HttpRequestPathHelper getPathHelper() { + return this.pathHelper; + } + + protected PathMatcher getPathMatcher() { + return this.pathMatcher; + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/UrlBasedViewResolverRegistration.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/UrlBasedViewResolverRegistration.java new file mode 100644 index 0000000000..422449d753 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/UrlBasedViewResolverRegistration.java @@ -0,0 +1,79 @@ +/* + * 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.web.reactive.config; + +import org.springframework.util.Assert; +import org.springframework.web.reactive.result.view.UrlBasedViewResolver; + +/** + * Assist with configuring properties of a {@link UrlBasedViewResolver}. + * + * @author Rossen Stoyanchev + */ +public class UrlBasedViewResolverRegistration { + + private final UrlBasedViewResolver viewResolver; + + + public UrlBasedViewResolverRegistration(UrlBasedViewResolver viewResolver) { + Assert.notNull(viewResolver); + this.viewResolver = viewResolver; + } + + + /** + * Set the prefix that gets prepended to view names when building a URL. + * @see UrlBasedViewResolver#setPrefix + */ + public UrlBasedViewResolverRegistration prefix(String prefix) { + this.viewResolver.setPrefix(prefix); + return this; + } + + /** + * Set the suffix that gets appended to view names when building a URL. + * @see UrlBasedViewResolver#setSuffix + */ + public UrlBasedViewResolverRegistration suffix(String suffix) { + this.viewResolver.setSuffix(suffix); + return this; + } + + /** + * Set the view class that should be used to create views. + * @see UrlBasedViewResolver#setViewClass + */ + public UrlBasedViewResolverRegistration viewClass(Class viewClass) { + this.viewResolver.setViewClass(viewClass); + return this; + } + + /** + * Set the view names (or name patterns) that can be handled by this view + * resolver. View names can contain simple wildcards such that 'my*', '*Report' + * and '*Repo*' will all match the view name 'myReport'. + * @see UrlBasedViewResolver#setViewNames + */ + public UrlBasedViewResolverRegistration viewNames(String... viewNames) { + this.viewResolver.setViewNames(viewNames); + return this; + } + + protected UrlBasedViewResolver getViewResolver() { + return this.viewResolver; + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/ViewResolverRegistry.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/ViewResolverRegistry.java new file mode 100644 index 0000000000..ce5989002d --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/ViewResolverRegistry.java @@ -0,0 +1,146 @@ +/* + * 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.web.reactive.config; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.BeanInitializationException; +import org.springframework.context.ApplicationContext; +import org.springframework.core.Ordered; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.web.reactive.result.view.UrlBasedViewResolver; +import org.springframework.web.reactive.result.view.View; +import org.springframework.web.reactive.result.view.ViewResolver; +import org.springframework.web.reactive.result.view.freemarker.FreeMarkerConfigurer; +import org.springframework.web.reactive.result.view.freemarker.FreeMarkerViewResolver; + + +/** + * Assist with the configuration of a chain of {@link ViewResolver}'s supporting + * different template mechanisms. + * + *

    In addition, you can also configure {@link #defaultViews(View...) + * defaultViews} for rendering according to the requested content type, e.g. + * JSON, XML, etc. + * + * @author Rossen Stoyanchev + */ +public class ViewResolverRegistry { + + private final List viewResolvers = new ArrayList<>(4); + + private final List defaultViews = new ArrayList<>(4); + + private Integer order; + + private final ApplicationContext applicationContext; + + + public ViewResolverRegistry(ApplicationContext applicationContext) { + Assert.notNull(applicationContext); + this.applicationContext = applicationContext; + } + + + /** + * Register a {@code FreeMarkerViewResolver} with a ".ftl" suffix. + *

    Note that you must also configure FreeMarker by + * adding a {@link FreeMarkerConfigurer} bean. + */ + public UrlBasedViewResolverRegistration freeMarker() { + if (this.applicationContext != null && !hasBeanOfType(FreeMarkerConfigurer.class)) { + throw new BeanInitializationException("In addition to a FreeMarker view resolver " + + "there must also be a single FreeMarkerConfig bean in this web application context " + + "(or its parent): FreeMarkerConfigurer is the usual implementation. " + + "This bean may be given any name."); + } + FreeMarkerRegistration registration = new FreeMarkerRegistration(); + UrlBasedViewResolver resolver = registration.getViewResolver(); + resolver.setApplicationContext(this.applicationContext); + this.viewResolvers.add(resolver); + return registration; + } + + protected boolean hasBeanOfType(Class beanType) { + return !ObjectUtils.isEmpty(BeanFactoryUtils.beanNamesForTypeIncludingAncestors( + this.applicationContext, beanType, false, false)); + } + + /** + * Register a {@link ViewResolver} bean instance. This may be useful to + * configure a 3rd party resolver implementation or as an alternative to + * other registration methods in this class when they don't expose some + * more advanced property that needs to be set. + */ + public void viewResolver(ViewResolver viewResolver) { + this.viewResolvers.add(viewResolver); + } + + /** + * Set default views associated with any view name and selected based on the + * best match for the requested content type. + *

    Use {@link org.springframework.web.reactive.result.view.HttpMessageConverterView + * HttpMessageConverterView} to adapt and use any existing + * {@code HttpMessageConverter} (e.g. JSON, XML) as a {@code View}. + */ + public void defaultViews(View... defaultViews) { + this.defaultViews.addAll(Arrays.asList(defaultViews)); + } + + /** + * Whether any view resolvers have been registered. + */ + public boolean hasRegistrations() { + return (!this.viewResolvers.isEmpty()); + } + + /** + * Set the order for the + * {@link org.springframework.web.reactive.result.view.ViewResolutionResultHandler + * ViewResolutionResultHandler}. + *

    By default this property is not set, which means the result handler is + * ordered at {@link Ordered#LOWEST_PRECEDENCE}. + */ + public void order(int order) { + this.order = order; + } + + protected int getOrder() { + return (this.order != null ? this.order : Ordered.LOWEST_PRECEDENCE); + } + + protected List getViewResolvers() { + return this.viewResolvers; + } + + protected List getDefaultViews() { + return this.defaultViews; + } + + + private static class FreeMarkerRegistration extends UrlBasedViewResolverRegistration { + + public FreeMarkerRegistration() { + super(new FreeMarkerViewResolver()); + getViewResolver().setSuffix(".ftl"); + } + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java new file mode 100644 index 0000000000..9f4b0e495a --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java @@ -0,0 +1,316 @@ +/* + * 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.web.reactive.config; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import reactor.core.converter.DependencyUtils; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.codec.Decoder; +import org.springframework.core.codec.Encoder; +import org.springframework.core.codec.support.ByteBufferDecoder; +import org.springframework.core.codec.support.ByteBufferEncoder; +import org.springframework.core.codec.support.JacksonJsonDecoder; +import org.springframework.core.codec.support.JacksonJsonEncoder; +import org.springframework.core.codec.support.Jaxb2Decoder; +import org.springframework.core.codec.support.Jaxb2Encoder; +import org.springframework.core.codec.support.JsonObjectDecoder; +import org.springframework.core.codec.support.StringDecoder; +import org.springframework.core.codec.support.StringEncoder; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.ConverterRegistry; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter; +import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter; +import org.springframework.format.Formatter; +import org.springframework.http.MediaType; +import org.springframework.http.converter.reactive.CodecHttpMessageConverter; +import org.springframework.http.converter.reactive.HttpMessageConverter; +import org.springframework.http.converter.reactive.ResourceHttpMessageConverter; +import org.springframework.util.ClassUtils; +import org.springframework.web.reactive.accept.RequestedContentTypeResolver; +import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder; +import org.springframework.web.reactive.result.SimpleHandlerAdapter; +import org.springframework.web.reactive.result.SimpleResultHandler; +import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver; +import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter; +import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping; +import org.springframework.web.reactive.result.method.annotation.ResponseBodyResultHandler; +import org.springframework.web.reactive.result.view.ViewResolutionResultHandler; +import org.springframework.web.reactive.result.view.ViewResolver; + +/** + * The main class for Spring Web Reactive configuration. + * + *

    Import directly or extend and override protected methods to customize. + * + * @author Rossen Stoyanchev + */ +@Configuration @SuppressWarnings("unused") +public class WebReactiveConfiguration implements ApplicationContextAware { + + private static final ClassLoader classLoader = WebReactiveConfiguration.class.getClassLoader(); + + private static final boolean jackson2Present = + ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader) && + ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader); + + private static final boolean jaxb2Present = + ClassUtils.isPresent("javax.xml.bind.Binder", classLoader); + + + private PathMatchConfigurer pathMatchConfigurer; + + private List> messageConverters; + + private ApplicationContext applicationContext; + + + @Override + public void setApplicationContext(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + + @Bean + public RequestMappingHandlerMapping requestMappingHandlerMapping() { + RequestMappingHandlerMapping mapping = createRequestMappingHandlerMapping(); + mapping.setOrder(0); + mapping.setContentTypeResolver(mvcContentTypeResolver()); + + PathMatchConfigurer configurer = getPathMatchConfigurer(); + if (configurer.isUseSuffixPatternMatch() != null) { + mapping.setUseSuffixPatternMatch(configurer.isUseSuffixPatternMatch()); + } + if (configurer.isUseRegisteredSuffixPatternMatch() != null) { + mapping.setUseRegisteredSuffixPatternMatch(configurer.isUseRegisteredSuffixPatternMatch()); + } + if (configurer.isUseTrailingSlashMatch() != null) { + mapping.setUseTrailingSlashMatch(configurer.isUseTrailingSlashMatch()); + } + if (configurer.getPathMatcher() != null) { + mapping.setPathMatcher(configurer.getPathMatcher()); + } + if (configurer.getPathHelper() != null) { + mapping.setPathHelper(configurer.getPathHelper()); + } + + return mapping; + } + + /** + * Override to plug a sub-class of {@link RequestMappingHandlerMapping}. + */ + protected RequestMappingHandlerMapping createRequestMappingHandlerMapping() { + return new RequestMappingHandlerMapping(); + } + + @Bean + public RequestedContentTypeResolver mvcContentTypeResolver() { + RequestedContentTypeResolverBuilder builder = new RequestedContentTypeResolverBuilder(); + builder.mediaTypes(getDefaultMediaTypeMappings()); + configureRequestedContentTypeResolver(builder); + return builder.build(); + } + + /** + * Override to configure media type mappings. + * @see RequestedContentTypeResolverBuilder#mediaTypes(Map) + */ + protected Map getDefaultMediaTypeMappings() { + Map map = new HashMap<>(); + if (jackson2Present) { + map.put("json", MediaType.APPLICATION_JSON); + } + return map; + } + + /** + * Override to configure how the requested content type is resolved. + */ + protected void configureRequestedContentTypeResolver(RequestedContentTypeResolverBuilder builder) { + } + + /** + * Callback for building the {@link PathMatchConfigurer}. This method is + * final, use {@link #configurePathMatching} to customize path matching. + */ + protected final PathMatchConfigurer getPathMatchConfigurer() { + if (this.pathMatchConfigurer == null) { + this.pathMatchConfigurer = new PathMatchConfigurer(); + configurePathMatching(this.pathMatchConfigurer); + } + return this.pathMatchConfigurer; + } + + /** + * Override to configure path matching options. + */ + public void configurePathMatching(PathMatchConfigurer configurer) { + } + + @Bean + public RequestMappingHandlerAdapter requestMappingHandlerAdapter() { + RequestMappingHandlerAdapter adapter = createRequestMappingHandlerAdapter(); + + List resolvers = new ArrayList<>(); + addArgumentResolvers(resolvers); + if (!resolvers.isEmpty()) { + adapter.setCustomArgumentResolvers(resolvers); + } + + adapter.setMessageConverters(getMessageConverters()); + adapter.setConversionService(mvcConversionService()); + + return adapter; + } + + /** + * Override to plug a sub-class of {@link RequestMappingHandlerAdapter}. + */ + protected RequestMappingHandlerAdapter createRequestMappingHandlerAdapter() { + return new RequestMappingHandlerAdapter(); + } + + /** + * Provide custom argument resolvers without overriding the built-in ones. + */ + protected void addArgumentResolvers(List resolvers) { + } + + /** + * Main method to access message converters to use for decoding + * controller method arguments and encoding return values. + *

    Use {@link #configureMessageConverters} to configure the list or + * {@link #extendMessageConverters} to add in addition to the default ones. + */ + protected final List> getMessageConverters() { + if (this.messageConverters == null) { + this.messageConverters = new ArrayList<>(); + configureMessageConverters(this.messageConverters); + if (this.messageConverters.isEmpty()) { + addDefaultHttpMessageConverters(this.messageConverters); + } + extendMessageConverters(this.messageConverters); + } + return this.messageConverters; + } + + /** + * Override to configure the message converters to use for decoding + * controller method arguments and encoding return values. + *

    If no converters are specified, default will be added via + * {@link #addDefaultHttpMessageConverters}. + * @param converters a list to add converters to, initially an empty + */ + protected void configureMessageConverters(List> converters) { + } + + /** + * Adds default converters that sub-classes can call from + * {@link #configureMessageConverters(List)}. + */ + protected final void addDefaultHttpMessageConverters(List> converters) { + converters.add(converter(new ByteBufferEncoder(), new ByteBufferDecoder())); + converters.add(converter(new StringEncoder(), new StringDecoder())); + converters.add(new ResourceHttpMessageConverter()); + if (jaxb2Present) { + converters.add(converter(new Jaxb2Encoder(), new Jaxb2Decoder())); + } + if (jackson2Present) { + JsonObjectDecoder objectDecoder = new JsonObjectDecoder(); + converters.add(converter(new JacksonJsonEncoder(), new JacksonJsonDecoder(objectDecoder))); + } + } + + private static HttpMessageConverter converter(Encoder encoder, Decoder decoder) { + return new CodecHttpMessageConverter<>(encoder, decoder); + } + + /** + * Override this to modify the list of converters after it has been + * configured, for example to add some in addition to the default ones. + */ + protected void extendMessageConverters(List> converters) { + } + + // TODO: switch to DefaultFormattingConversionService + + @Bean + public GenericConversionService mvcConversionService() { + GenericConversionService service = new GenericConversionService(); + addFormatters(service); + return service; + } + + // TODO: switch to FormatterRegistry + + /** + * Override to add custom {@link Converter}s and {@link Formatter}s. + *

    By default this method method registers: + *

      + *
    • {@link ReactiveStreamsToCompletableFutureConverter} + *
    • {@link ReactiveStreamsToRxJava1Converter} + *
    + */ + protected void addFormatters(ConverterRegistry registry) { + registry.addConverter(new ReactiveStreamsToCompletableFutureConverter()); + if (DependencyUtils.hasRxJava1()) { + registry.addConverter(new ReactiveStreamsToRxJava1Converter()); + } + } + + @Bean + public SimpleHandlerAdapter simpleHandlerAdapter() { + return new SimpleHandlerAdapter(); + } + + @Bean + public ResponseBodyResultHandler responseBodyResultHandler() { + return new ResponseBodyResultHandler(getMessageConverters(), mvcConversionService()); + } + + @Bean + public SimpleResultHandler simpleResultHandler() { + return new SimpleResultHandler(mvcConversionService()); + } + + @Bean + public ViewResolutionResultHandler viewResolutionResultHandler() { + ViewResolverRegistry registry = new ViewResolverRegistry(this.applicationContext); + configureViewResolvers(registry); + List resolvers = registry.getViewResolvers(); + ViewResolutionResultHandler handler = new ViewResolutionResultHandler(resolvers, mvcConversionService()); + handler.setDefaultViews(registry.getDefaultViews()); + handler.setOrder(registry.getOrder()); + return handler; + + } + + /** + * Override this to configure view resolution. + */ + protected void configureViewResolvers(ViewResolverRegistry registry) { + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/package-info.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/package-info.java new file mode 100644 index 0000000000..8ada9a8cd4 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/package-info.java @@ -0,0 +1,4 @@ +/** + * Defines Spring Web Reactive configuration. + */ +package org.springframework.web.reactive.config; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/ViewResolverRegistryTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/ViewResolverRegistryTests.java new file mode 100644 index 0000000000..6e46b829b5 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/ViewResolverRegistryTests.java @@ -0,0 +1,90 @@ +/* + * 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.web.reactive.config; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.core.Ordered; +import org.springframework.core.codec.support.JacksonJsonEncoder; +import org.springframework.web.context.support.StaticWebApplicationContext; +import org.springframework.web.reactive.result.view.HttpMessageConverterView; +import org.springframework.web.reactive.result.view.UrlBasedViewResolver; +import org.springframework.web.reactive.result.view.View; +import org.springframework.web.reactive.result.view.freemarker.FreeMarkerConfigurer; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +/** + * Unit tests for {@link ViewResolverRegistry}. + * + * @author Rossen Stoyanchev + */ +public class ViewResolverRegistryTests { + + private ViewResolverRegistry registry; + + + @Before + public void setUp() { + StaticWebApplicationContext context = new StaticWebApplicationContext(); + context.registerSingleton("freeMarkerConfigurer", FreeMarkerConfigurer.class); + this.registry = new ViewResolverRegistry(context); + } + + @Test + public void order() { + assertEquals(Ordered.LOWEST_PRECEDENCE, this.registry.getOrder()); + } + + @Test + public void hasRegistrations() { + assertFalse(this.registry.hasRegistrations()); + + this.registry.freeMarker(); + assertTrue(this.registry.hasRegistrations()); + } + + @Test + public void noResolvers() { + assertNotNull(this.registry.getViewResolvers()); + assertEquals(0, this.registry.getViewResolvers().size()); + assertFalse(this.registry.hasRegistrations()); + } + + @Test + public void customViewResolver() { + UrlBasedViewResolver viewResolver = new UrlBasedViewResolver(); + this.registry.viewResolver(viewResolver); + + assertSame(viewResolver, this.registry.getViewResolvers().get(0)); + assertEquals(1, this.registry.getViewResolvers().size()); + } + + @Test + public void defaultViews() throws Exception { + View view = new HttpMessageConverterView(new JacksonJsonEncoder()); + this.registry.defaultViews(view); + + assertEquals(1, this.registry.getDefaultViews().size()); + assertSame(view, this.registry.getDefaultViews().get(0)); + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/WebReactiveConfigurationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/WebReactiveConfigurationTests.java new file mode 100644 index 0000000000..74ddf2f793 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/WebReactiveConfigurationTests.java @@ -0,0 +1,277 @@ +/* + * 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.web.reactive.config; + +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import org.junit.Before; +import org.junit.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import rx.Observable; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.ResolvableType; +import org.springframework.core.codec.support.JacksonJsonEncoder; +import org.springframework.core.codec.support.Jaxb2Decoder; +import org.springframework.core.codec.support.Jaxb2Encoder; +import org.springframework.core.codec.support.Pojo; +import org.springframework.core.codec.support.StringDecoder; +import org.springframework.core.codec.support.StringEncoder; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.converter.reactive.CodecHttpMessageConverter; +import org.springframework.http.converter.reactive.HttpMessageConverter; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.util.MimeType; +import org.springframework.util.MimeTypeUtils; +import org.springframework.web.reactive.accept.RequestedContentTypeResolver; +import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter; +import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping; +import org.springframework.web.reactive.result.method.annotation.ResponseBodyResultHandler; +import org.springframework.web.reactive.result.view.HttpMessageConverterView; +import org.springframework.web.reactive.result.view.View; +import org.springframework.web.reactive.result.view.ViewResolutionResultHandler; +import org.springframework.web.reactive.result.view.ViewResolver; +import org.springframework.web.reactive.result.view.freemarker.FreeMarkerConfigurer; +import org.springframework.web.reactive.result.view.freemarker.FreeMarkerViewResolver; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.WebSessionManager; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +/** + * Unit tests for {@link WebReactiveConfiguration}. + * @author Rossen Stoyanchev + */ +public class WebReactiveConfigurationTests { + + private MockServerHttpRequest request; + + private ServerWebExchange exchange; + + + @Before + public void setUp() throws Exception { + this.request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); + MockServerHttpResponse response = new MockServerHttpResponse(); + this.exchange = new DefaultServerWebExchange(this.request, response, mock(WebSessionManager.class)); + } + + + @Test + public void requestMappingHandlerMapping() throws Exception { + ApplicationContext context = loadConfig(WebReactiveConfiguration.class); + + String name = "requestMappingHandlerMapping"; + RequestMappingHandlerMapping mapping = context.getBean(name, RequestMappingHandlerMapping.class); + assertNotNull(mapping); + + assertEquals(0, mapping.getOrder()); + + assertTrue(mapping.useSuffixPatternMatch()); + assertTrue(mapping.useTrailingSlashMatch()); + assertTrue(mapping.useRegisteredSuffixPatternMatch()); + + name = "mvcContentTypeResolver"; + RequestedContentTypeResolver resolver = context.getBean(name, RequestedContentTypeResolver.class); + assertSame(resolver, mapping.getContentTypeResolver()); + + this.request.setUri(new URI("/path.json")); + List list = Collections.singletonList(MediaType.APPLICATION_JSON); + assertEquals(list, resolver.resolveMediaTypes(this.exchange)); + + this.request.setUri(new URI("/path.xml")); + assertEquals(Collections.emptyList(), resolver.resolveMediaTypes(this.exchange)); + } + + @Test + public void customPathMatchConfig() throws Exception { + ApplicationContext context = loadConfig(CustomPatchMatchConfig.class); + + String name = "requestMappingHandlerMapping"; + RequestMappingHandlerMapping mapping = context.getBean(name, RequestMappingHandlerMapping.class); + assertNotNull(mapping); + + assertFalse(mapping.useSuffixPatternMatch()); + assertFalse(mapping.useTrailingSlashMatch()); + } + + @Test + public void requestMappingHandlerAdapter() throws Exception { + ApplicationContext context = loadConfig(WebReactiveConfiguration.class); + + String name = "requestMappingHandlerAdapter"; + RequestMappingHandlerAdapter adapter = context.getBean(name, RequestMappingHandlerAdapter.class); + assertNotNull(adapter); + + List> converters = adapter.getMessageConverters(); + assertEquals(5, converters.size()); + + assertHasConverter(converters, ByteBuffer.class, MediaType.APPLICATION_OCTET_STREAM); + assertHasConverter(converters, String.class, MediaType.TEXT_PLAIN); + assertHasConverter(converters, Resource.class, MediaType.IMAGE_PNG); + assertHasConverter(converters, Pojo.class, MediaType.APPLICATION_XML); + assertHasConverter(converters, Pojo.class, MediaType.APPLICATION_JSON); + + name = "mvcConversionService"; + ConversionService service = context.getBean(name, ConversionService.class); + assertSame(service, adapter.getConversionService()); + } + + @Test + public void customMessageConverterConfig() throws Exception { + ApplicationContext context = loadConfig(CustomMessageConverterConfig.class); + + String name = "requestMappingHandlerAdapter"; + RequestMappingHandlerAdapter adapter = context.getBean(name, RequestMappingHandlerAdapter.class); + assertNotNull(adapter); + + List> converters = adapter.getMessageConverters(); + assertEquals(2, converters.size()); + + assertHasConverter(converters, String.class, MediaType.TEXT_PLAIN); + assertHasConverter(converters, Pojo.class, MediaType.APPLICATION_XML); + } + + @Test + public void mvcConversionService() throws Exception { + ApplicationContext context = loadConfig(WebReactiveConfiguration.class); + + String name = "mvcConversionService"; + ConversionService service = context.getBean(name, ConversionService.class); + assertNotNull(service); + + service.canConvert(CompletableFuture.class, Mono.class); + service.canConvert(Observable.class, Flux.class); + } + + + @Test + public void responseBodyResultHandler() throws Exception { + ApplicationContext context = loadConfig(WebReactiveConfiguration.class); + + String name = "responseBodyResultHandler"; + ResponseBodyResultHandler handler = context.getBean(name, ResponseBodyResultHandler.class); + assertNotNull(handler); + + assertEquals(0, handler.getOrder()); + + List> converters = handler.getMessageConverters(); + assertEquals(5, converters.size()); + + assertHasConverter(converters, ByteBuffer.class, MediaType.APPLICATION_OCTET_STREAM); + assertHasConverter(converters, String.class, MediaType.TEXT_PLAIN); + assertHasConverter(converters, Resource.class, MediaType.IMAGE_PNG); + assertHasConverter(converters, Pojo.class, MediaType.APPLICATION_XML); + assertHasConverter(converters, Pojo.class, MediaType.APPLICATION_JSON); + } + + @Test + public void viewResolutionResultHandler() throws Exception { + ApplicationContext context = loadConfig(CustomViewResolverConfig.class); + + String name = "viewResolutionResultHandler"; + ViewResolutionResultHandler handler = context.getBean(name, ViewResolutionResultHandler.class); + assertNotNull(handler); + + assertEquals(Ordered.LOWEST_PRECEDENCE, handler.getOrder()); + + List resolvers = handler.getViewResolvers(); + assertEquals(1, resolvers.size()); + assertEquals(FreeMarkerViewResolver.class, resolvers.get(0).getClass()); + + List views = handler.getDefaultViews(); + assertEquals(1, views.size()); + + MimeType type = MimeTypeUtils.parseMimeType("application/json;charset=UTF-8"); + assertEquals(type, views.get(0).getSupportedMediaTypes().get(0)); + } + + + private void assertHasConverter(List> converters, Class clazz, MediaType mediaType) { + ResolvableType type = ResolvableType.forClass(clazz); + assertTrue(converters.stream() + .filter(c -> c.canRead(type, mediaType) && c.canWrite(type, mediaType)) + .findAny() + .isPresent()); + } + + private ApplicationContext loadConfig(Class... configurationClasses) { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.register(configurationClasses); + context.refresh(); + return context; + } + + + @Configuration + static class CustomPatchMatchConfig extends WebReactiveConfiguration { + + @Override + public void configurePathMatching(PathMatchConfigurer configurer) { + configurer.setUseSuffixPatternMatch(false); + configurer.setUseTrailingSlashMatch(false); + } + } + + @Configuration + static class CustomMessageConverterConfig extends WebReactiveConfiguration { + + @Override + protected void configureMessageConverters(List> converters) { + converters.add(new CodecHttpMessageConverter<>(new StringEncoder(), new StringDecoder())); + } + + @Override + protected void extendMessageConverters(List> converters) { + converters.add(new CodecHttpMessageConverter<>(new Jaxb2Encoder(), new Jaxb2Decoder())); + } + } + + @Configuration @SuppressWarnings("unused") + static class CustomViewResolverConfig extends WebReactiveConfiguration { + + @Override + protected void configureViewResolvers(ViewResolverRegistry registry) { + registry.freeMarker(); + registry.defaultViews(new HttpMessageConverterView(new JacksonJsonEncoder())); + } + + @Bean + public FreeMarkerConfigurer freeMarkerConfig() { + return new FreeMarkerConfigurer(); + } + + } +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java index 217c52a6c2..3ad58a92b0 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java @@ -21,7 +21,6 @@ import java.nio.ByteBuffer; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.concurrent.CompletableFuture; import javax.xml.bind.annotation.XmlElement; @@ -40,19 +39,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ResolvableType; -import org.springframework.core.codec.support.ByteBufferDecoder; -import org.springframework.core.codec.support.ByteBufferEncoder; -import org.springframework.core.codec.support.JacksonJsonDecoder; import org.springframework.core.codec.support.JacksonJsonEncoder; -import org.springframework.core.codec.support.Jaxb2Decoder; -import org.springframework.core.codec.support.Jaxb2Encoder; -import org.springframework.core.codec.support.JsonObjectDecoder; -import org.springframework.core.codec.support.StringDecoder; -import org.springframework.core.codec.support.StringEncoder; -import org.springframework.core.convert.ConversionService; -import org.springframework.core.convert.support.GenericConversionService; -import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter; -import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.core.io.buffer.DataBuffer; @@ -62,9 +49,6 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; -import org.springframework.http.converter.reactive.CodecHttpMessageConverter; -import org.springframework.http.converter.reactive.HttpMessageConverter; -import org.springframework.http.converter.reactive.ResourceHttpMessageConverter; import org.springframework.http.server.reactive.AbstractHttpHandlerIntegrationTests; import org.springframework.http.server.reactive.HttpHandler; import org.springframework.http.server.reactive.ZeroCopyIntegrationTests; @@ -78,11 +62,9 @@ import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; import org.springframework.web.reactive.DispatcherHandler; -import org.springframework.web.reactive.result.SimpleResultHandler; -import org.springframework.web.reactive.result.view.ViewResolutionResultHandler; -import org.springframework.web.reactive.result.view.ViewResolver; +import org.springframework.web.reactive.config.ViewResolverRegistry; +import org.springframework.web.reactive.config.WebReactiveConfiguration; import org.springframework.web.reactive.result.view.freemarker.FreeMarkerConfigurer; -import org.springframework.web.reactive.result.view.freemarker.FreeMarkerViewResolver; import org.springframework.web.server.adapter.WebHttpHandlerBuilder; import static org.junit.Assert.assertArrayEquals; @@ -385,61 +367,11 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @Configuration @SuppressWarnings("unused") - static class FrameworkConfig { + static class FrameworkConfig extends WebReactiveConfiguration { - @Bean - public RequestMappingHandlerMapping handlerMapping() { - return new RequestMappingHandlerMapping(); - } - - @Bean - public RequestMappingHandlerAdapter handlerAdapter() { - RequestMappingHandlerAdapter handlerAdapter = new RequestMappingHandlerAdapter(); - handlerAdapter.setMessageConverters(getDefaultMessageConverters()); - handlerAdapter.setConversionService(conversionService()); - return handlerAdapter; - } - - private List> getDefaultMessageConverters() { - return Arrays.asList( - new CodecHttpMessageConverter<>(new ByteBufferEncoder(), new ByteBufferDecoder()), - new CodecHttpMessageConverter<>(new StringEncoder(), new StringDecoder()), - new CodecHttpMessageConverter<>(new Jaxb2Encoder(), new Jaxb2Decoder()), - new CodecHttpMessageConverter<>(new JacksonJsonEncoder(), - new JacksonJsonDecoder(new JsonObjectDecoder()))); - } - - @Bean - public ConversionService conversionService() { - // TODO: test failures with DefaultConversionService - GenericConversionService service = new GenericConversionService(); - service.addConverter(new ReactiveStreamsToCompletableFutureConverter()); - service.addConverter(new ReactiveStreamsToRxJava1Converter()); - return service; - } - - @Bean - public ResponseBodyResultHandler responseBodyResultHandler() { - List> converters = new ArrayList<>(); - converters.add(new ResourceHttpMessageConverter()); - converters.addAll(getDefaultMessageConverters()); - return new ResponseBodyResultHandler(converters, conversionService()); - } - - @Bean - public SimpleResultHandler simpleHandlerResultHandler() { - return new SimpleResultHandler(conversionService()); - } - - @Bean - public ViewResolutionResultHandler viewResolverResultHandler() { - List resolvers = Collections.singletonList(freeMarkerViewResolver()); - return new ViewResolutionResultHandler(resolvers, conversionService()); - } - - @Bean - public ViewResolver freeMarkerViewResolver() { - return new FreeMarkerViewResolver("", ".ftl"); + @Override + protected void configureViewResolvers(ViewResolverRegistry registry) { + registry.freeMarker(); } @Bean From 5c236e1edf2d5b7c347d44e5c6a886f33a6b3713 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 6 Jun 2016 10:12:02 -0400 Subject: [PATCH 389/498] Update to latest Reactor snapshot --- ...veStreamsToCompletableFutureConverter.java | 4 ++-- .../reactive/ChannelSendOperatorTests.java | 10 +++++----- .../reactive/ServerHttpResponseTests.java | 12 +++++------ .../reactive/DispatcherHandlerErrorTests.java | 6 +++--- .../ResponseStatusExceptionHandlerTests.java | 4 ++-- .../handler/SimpleUrlHandlerMappingTests.java | 2 +- .../method/HandlerMethodMappingTests.java | 8 ++++---- .../method/InvocableHandlerMethodTests.java | 6 +++--- ...RequestMappingInfoHandlerMappingTests.java | 4 ++-- ...equestAttributesArgumentResolverTests.java | 20 +++++++++---------- ...ookieValueMethodArgumentResolverTests.java | 6 +++--- ...ssionValueMethodArgumentResolverTests.java | 2 +- ...ariableMapMethodArgumentResolverTests.java | 4 ++-- ...thVariableMethodArgumentResolverTests.java | 2 +- ...tAttributeMethodArgumentResolverTests.java | 20 +++++++++---------- ...tHeaderMapMethodArgumentResolverTests.java | 6 +++--- ...uestHeaderMethodArgumentResolverTests.java | 16 +++++++-------- ...stParamMapMethodArgumentResolverTests.java | 4 ++-- ...questParamMethodArgumentResolverTests.java | 20 +++++++++---------- .../ResponseBodyResultHandlerTests.java | 8 +++----- ...nAttributeMethodArgumentResolverTests.java | 20 +++++++++---------- .../view/UrlBasedViewResolverTests.java | 4 ++-- .../ExceptionHandlingHttpHandlerTests.java | 8 ++++---- .../handler/FilteringWebHandlerTests.java | 15 ++++++-------- .../DefaultWebSessionManagerTests.java | 16 +++++++-------- 25 files changed, 111 insertions(+), 116 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToCompletableFutureConverter.java b/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToCompletableFutureConverter.java index 707b5c8078..41234dfe57 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToCompletableFutureConverter.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToCompletableFutureConverter.java @@ -45,10 +45,10 @@ public class ReactiveStreamsToCompletableFutureConverter implements GenericConve return null; } else if (CompletableFuture.class.isAssignableFrom(source.getClass())) { - return Mono.fromCompletableFuture((CompletableFuture)source); + return Mono.fromFuture((CompletableFuture) source); } else if (CompletableFuture.class.isAssignableFrom(targetType.getResolvableType().getRawClass())) { - return Mono.from((Publisher)source).toCompletableFuture(); + return Mono.from((Publisher) source).toFuture(); } return null; } diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ChannelSendOperatorTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ChannelSendOperatorTests.java index 7db972ebd2..e6c056b7f8 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ChannelSendOperatorTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ChannelSendOperatorTests.java @@ -57,7 +57,7 @@ public class ChannelSendOperatorTests { public void errorBeforeFirstItem() throws Exception { IllegalStateException error = new IllegalStateException("boo"); Mono completion = Mono.error(error).as(this::sendOperator); - Signal signal = completion.materialize().get(); + Signal signal = completion.materialize().block(); assertNotNull(signal); assertSame("Unexpected signal: " + signal, error, signal.getThrowable()); @@ -66,7 +66,7 @@ public class ChannelSendOperatorTests { @Test public void completionBeforeFirstItem() throws Exception { Mono completion = Flux.empty().as(this::sendOperator); - Signal signal = completion.materialize().get(); + Signal signal = completion.materialize().block(); assertNotNull(signal); assertTrue("Unexpected signal: " + signal, signal.isOnComplete()); @@ -78,7 +78,7 @@ public class ChannelSendOperatorTests { @Test public void writeOneItem() throws Exception { Mono completion = Flux.just("one").as(this::sendOperator); - Signal signal = completion.materialize().get(); + Signal signal = completion.materialize().block(); assertNotNull(signal); assertTrue("Unexpected signal: " + signal, signal.isOnComplete()); @@ -93,7 +93,7 @@ public class ChannelSendOperatorTests { public void writeMultipleItems() throws Exception { List items = Arrays.asList("one", "two", "three"); Mono completion = Flux.fromIterable(items).as(this::sendOperator); - Signal signal = completion.materialize().get(); + Signal signal = completion.materialize().block(); assertNotNull(signal); assertTrue("Unexpected signal: " + signal, signal.isOnComplete()); @@ -117,7 +117,7 @@ public class ChannelSendOperatorTests { return i; }); Mono completion = publisher.as(this::sendOperator); - Signal signal = completion.materialize().get(); + Signal signal = completion.materialize().block(); assertNotNull(signal); assertSame("Unexpected signal: " + signal, error, signal.getThrowable()); diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java index e30181f73a..6eff9434bf 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java @@ -45,7 +45,7 @@ public class ServerHttpResponseTests { @Test public void writeWith() throws Exception { TestServerHttpResponse response = new TestServerHttpResponse(); - response.writeWith(Flux.just(wrap("a"), wrap("b"), wrap("c"))).get(); + response.writeWith(Flux.just(wrap("a"), wrap("b"), wrap("c"))).block(); assertTrue(response.headersWritten); assertTrue(response.cookiesWritten); @@ -60,7 +60,7 @@ public class ServerHttpResponseTests { public void writeWithError() throws Exception { TestServerHttpResponse response = new TestServerHttpResponse(); IllegalStateException error = new IllegalStateException("boo"); - response.writeWith(Flux.error(error)).otherwise(ex -> Mono.empty()).get(); + response.writeWith(Flux.error(error)).otherwise(ex -> Mono.empty()).block(); assertFalse(response.headersWritten); assertFalse(response.cookiesWritten); @@ -70,7 +70,7 @@ public class ServerHttpResponseTests { @Test public void setComplete() throws Exception { TestServerHttpResponse response = new TestServerHttpResponse(); - response.setComplete().get(); + response.setComplete().block(); assertTrue(response.headersWritten); assertTrue(response.cookiesWritten); @@ -85,7 +85,7 @@ public class ServerHttpResponseTests { response.getCookies().add(cookie.getName(), cookie); return Mono.empty(); }); - response.writeWith(Flux.just(wrap("a"), wrap("b"), wrap("c"))).get(); + response.writeWith(Flux.just(wrap("a"), wrap("b"), wrap("c"))).block(); assertTrue(response.headersWritten); assertTrue(response.cookiesWritten); @@ -102,7 +102,7 @@ public class ServerHttpResponseTests { TestServerHttpResponse response = new TestServerHttpResponse(); IllegalStateException error = new IllegalStateException("boo"); response.beforeCommit(() -> Mono.error(error)); - response.writeWith(Flux.just(wrap("a"), wrap("b"), wrap("c"))).get(); + response.writeWith(Flux.just(wrap("a"), wrap("b"), wrap("c"))).block(); assertTrue("beforeCommit action errors should be ignored", response.headersWritten); assertTrue("beforeCommit action errors should be ignored", response.cookiesWritten); @@ -122,7 +122,7 @@ public class ServerHttpResponseTests { response.getCookies().add(cookie.getName(), cookie); return Mono.empty(); }); - response.setComplete().get(); + response.setComplete().block(); assertTrue(response.headersWritten); assertTrue(response.cookiesWritten); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java index bbea4c62d1..8a28d1ce30 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java @@ -191,7 +191,7 @@ public class DispatcherHandlerErrorTests { WebHandler webHandler = new ExceptionHandlingWebHandler(this.dispatcherHandler, exceptionHandler); Mono publisher = webHandler.handle(this.exchange); - publisher.get(); + publisher.block(); assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, this.response.getStatus()); } @@ -203,13 +203,13 @@ public class DispatcherHandlerErrorTests { webHandler = new ExceptionHandlingWebHandler(webHandler, new ServerError500ExceptionHandler()); Mono publisher = webHandler.handle(this.exchange); - publisher.get(); + publisher.block(); assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, this.response.getStatus()); } private Throwable awaitErrorSignal(Mono mono) throws Exception { - Signal signal = mono.materialize().get(); + Signal signal = mono.materialize().block(); assertEquals("Unexpected signal: " + signal, SignalKind.onError, signal.getType()); return signal.getThrowable(); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java index f36b67f2bb..5929587ec9 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java @@ -63,7 +63,7 @@ public class ResponseStatusExceptionHandlerTests { Throwable ex = new ResponseStatusException(HttpStatus.BAD_REQUEST, ""); Mono publisher = this.handler.handle(this.exchange, ex); - publisher.get(); + publisher.block(); assertEquals(HttpStatus.BAD_REQUEST, this.response.getStatus()); } @@ -72,7 +72,7 @@ public class ResponseStatusExceptionHandlerTests { Throwable ex = new IllegalStateException(); Mono publisher = this.handler.handle(this.exchange, ex); - Signal signal = publisher.materialize().get(); + Signal signal = publisher.materialize().block(); assertNotNull(signal); assertTrue(signal.hasError()); assertSame(ex, signal.getThrowable()); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingTests.java index eccda562f6..599fcc3b03 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingTests.java @@ -108,7 +108,7 @@ public class SimpleUrlHandlerMappingTests { throws URISyntaxException { ServerWebExchange exchange = createExchange(url); - Object actual = handlerMapping.getHandler(exchange).get(); + Object actual = handlerMapping.getHandler(exchange).block(); if (bean != null) { assertNotNull(actual); assertSame(bean, actual); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/HandlerMethodMappingTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/HandlerMethodMappingTests.java index 87b005f2a9..15df98eb97 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/HandlerMethodMappingTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/HandlerMethodMappingTests.java @@ -84,7 +84,7 @@ public class HandlerMethodMappingTests { this.mapping.registerMapping(key, this.handler, this.method1); Mono result = this.mapping.getHandler(createExchange(HttpMethod.GET, key)); - assertEquals(this.method1, ((HandlerMethod) result.get()).getMethod()); + assertEquals(this.method1, ((HandlerMethod) result.block()).getMethod()); } @Test @@ -93,7 +93,7 @@ public class HandlerMethodMappingTests { this.mapping.registerMapping("/f*", this.handler, this.method2); Mono result = this.mapping.getHandler(createExchange(HttpMethod.GET, "/foo")); - assertEquals(this.method1, ((HandlerMethod) result.get()).getMethod()); + assertEquals(this.method1, ((HandlerMethod) result.block()).getMethod()); } @Test @@ -140,11 +140,11 @@ public class HandlerMethodMappingTests { String key = "foo"; this.mapping.registerMapping(key, this.handler, this.method1); Mono result = this.mapping.getHandler(createExchange(HttpMethod.GET, key)); - assertNotNull(result.get()); + assertNotNull(result.block()); this.mapping.unregisterMapping(key); result = this.mapping.getHandler(createExchange(HttpMethod.GET, key)); - assertNull(result.get()); + assertNull(result.block()); assertNull(this.mapping.getMappingRegistry().getMappingsByUrl(key)); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/InvocableHandlerMethodTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/InvocableHandlerMethodTests.java index 44a5302c36..1e8df0b71e 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/InvocableHandlerMethodTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/InvocableHandlerMethodTests.java @@ -90,7 +90,7 @@ public class InvocableHandlerMethodTests { new RequestParamMethodArgumentResolver(new GenericConversionService(), null, false))); Mono mono = hm.invokeForRequest(this.exchange, this.model); - HandlerResult value = mono.get(); + HandlerResult value = mono.block(); assertNotNull(value); assertEquals("success:null", value.getReturnValue().get()); @@ -102,7 +102,7 @@ public class InvocableHandlerMethodTests { addResolver(hm, Mono.just("value1")); Mono mono = hm.invokeForRequest(this.exchange, this.model); - HandlerResult value = mono.get(); + HandlerResult value = mono.block(); assertNotNull(value); assertEquals("success:value1", value.getReturnValue().get()); @@ -114,7 +114,7 @@ public class InvocableHandlerMethodTests { addResolver(hm, Flux.fromIterable(Arrays.asList("value1", "value2", "value3"))); Mono mono = hm.invokeForRequest(this.exchange, this.model); - HandlerResult value = mono.get(); + HandlerResult value = mono.block(); assertNotNull(value); assertEquals("success:value1", value.getReturnValue().get()); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java index 31f4c3b00c..1fe69c50fb 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java @@ -368,7 +368,7 @@ public class RequestMappingInfoHandlerMappingTests { @SuppressWarnings("ConstantConditions") private HandlerMethod getHandler(ServerWebExchange exchange) throws Exception { Mono handler = this.handlerMapping.getHandler(exchange); - return (HandlerMethod) handler.get(); + return (HandlerMethod) handler.block(); } private void testHttpMediaTypeNotSupportedException(String url) throws Exception { @@ -389,7 +389,7 @@ public class RequestMappingInfoHandlerMappingTests { ModelMap model = new ExtendedModelMap(); Mono mono = new InvocableHandlerMethod(handlerMethod).invokeForRequest(exchange, model); - HandlerResult result = mono.get(); + HandlerResult result = mono.block(); assertNotNull(result); Optional value = result.getReturnValue(); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/AbstractRequestAttributesArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/AbstractRequestAttributesArgumentResolverTests.java index 6e12aea212..c84db1c111 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/AbstractRequestAttributesArgumentResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/AbstractRequestAttributesArgumentResolverTests.java @@ -103,7 +103,7 @@ public abstract class AbstractRequestAttributesArgumentResolverTests { Foo foo = new Foo(); this.exchange.getAttributes().put("foo", foo); mono = this.resolver.resolveArgument(param, null, this.exchange); - assertSame(foo, mono.get()); + assertSame(foo, mono.block()); } @Test @@ -112,36 +112,36 @@ public abstract class AbstractRequestAttributesArgumentResolverTests { Foo foo = new Foo(); this.exchange.getAttributes().put("specialFoo", foo); Mono mono = this.resolver.resolveArgument(param, null, this.exchange); - assertSame(foo, mono.get()); + assertSame(foo, mono.block()); } @Test public void resolveNotRequired() throws Exception { MethodParameter param = initMethodParameter(2); Mono mono = this.resolver.resolveArgument(param, null, this.exchange); - assertNull(mono.get()); + assertNull(mono.block()); Foo foo = new Foo(); this.exchange.getAttributes().put("foo", foo); mono = this.resolver.resolveArgument(param, null, this.exchange); - assertSame(foo, mono.get()); + assertSame(foo, mono.block()); } @Test public void resolveOptional() throws Exception { MethodParameter param = initMethodParameter(3); Mono mono = this.resolver.resolveArgument(param, null, this.exchange); - assertNotNull(mono.get()); - assertEquals(Optional.class, mono.get().getClass()); - assertFalse(((Optional) mono.get()).isPresent()); + assertNotNull(mono.block()); + assertEquals(Optional.class, mono.block().getClass()); + assertFalse(((Optional) mono.block()).isPresent()); Foo foo = new Foo(); this.exchange.getAttributes().put("foo", foo); mono = this.resolver.resolveArgument(param, null, this.exchange); - assertNotNull(mono.get()); - assertEquals(Optional.class, mono.get().getClass()); - Optional optional = (Optional) mono.get(); + assertNotNull(mono.block()); + assertEquals(Optional.class, mono.block().getClass()); + Optional optional = (Optional) mono.block(); assertTrue(optional.isPresent()); assertSame(foo, optional.get()); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/CookieValueMethodArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/CookieValueMethodArgumentResolverTests.java index 4951337b20..de710dada9 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/CookieValueMethodArgumentResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/CookieValueMethodArgumentResolverTests.java @@ -92,7 +92,7 @@ public class CookieValueMethodArgumentResolverTests { this.exchange.getRequest().getCookies().add(expected.getName(), expected); Mono mono = this.resolver.resolveArgument(this.cookieParameter, null, this.exchange); - assertEquals(expected, mono.get()); + assertEquals(expected, mono.block()); } @Test @@ -101,13 +101,13 @@ public class CookieValueMethodArgumentResolverTests { this.exchange.getRequest().getCookies().add(cookie.getName(), cookie); Mono mono = this.resolver.resolveArgument(this.cookieStringParameter, null, this.exchange); - assertEquals("Invalid result", cookie.getValue(), mono.get()); + assertEquals("Invalid result", cookie.getValue(), mono.block()); } @Test public void resolveCookieDefaultValue() { Mono mono = this.resolver.resolveArgument(this.cookieStringParameter, null, this.exchange); - Object result = mono.get(); + Object result = mono.block(); assertTrue(result instanceof String); assertEquals("bar", result); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ExpressionValueMethodArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ExpressionValueMethodArgumentResolverTests.java index 52ae4a2a11..f31d996962 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ExpressionValueMethodArgumentResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ExpressionValueMethodArgumentResolverTests.java @@ -84,7 +84,7 @@ public class ExpressionValueMethodArgumentResolverTests { System.setProperty("systemProperty", "22"); try { Mono mono = this.resolver.resolveArgument(this.paramSystemProperty, null, this.exchange); - Object value = mono.get(); + Object value = mono.block(); assertEquals(22, value); } finally { diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/PathVariableMapMethodArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/PathVariableMapMethodArgumentResolverTests.java index 851e77e804..bdbf50e541 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/PathVariableMapMethodArgumentResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/PathVariableMapMethodArgumentResolverTests.java @@ -88,7 +88,7 @@ public class PathVariableMapMethodArgumentResolverTests { this.exchange.getAttributes().put(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriTemplateVars); Mono mono = this.resolver.resolveArgument(this.paramMap, new ModelMap(), this.exchange); - Object result = mono.get(); + Object result = mono.block(); assertEquals(uriTemplateVars, result); } @@ -96,7 +96,7 @@ public class PathVariableMapMethodArgumentResolverTests { @Test public void resolveArgumentNoUriVars() throws Exception { Mono mono = this.resolver.resolveArgument(this.paramMap, new ModelMap(), this.exchange); - Object result = mono.get(); + Object result = mono.block(); assertEquals(Collections.emptyMap(), result); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/PathVariableMethodArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/PathVariableMethodArgumentResolverTests.java index 065c624903..0f4dacb00f 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/PathVariableMethodArgumentResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/PathVariableMethodArgumentResolverTests.java @@ -89,7 +89,7 @@ public class PathVariableMethodArgumentResolverTests { this.exchange.getAttributes().put(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriTemplateVars); Mono mono = this.resolver.resolveArgument(this.paramNamedString, new ModelMap(), this.exchange); - Object result = mono.get(); + Object result = mono.block(); assertTrue(result instanceof String); assertEquals("value", result); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestAttributeMethodArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestAttributeMethodArgumentResolverTests.java index 000e695cbb..19c7c2e64e 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestAttributeMethodArgumentResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestAttributeMethodArgumentResolverTests.java @@ -97,7 +97,7 @@ public class RequestAttributeMethodArgumentResolverTests { Foo foo = new Foo(); this.exchange.getAttributes().put("foo", foo); mono = this.resolver.resolveArgument(param, null, this.exchange); - assertSame(foo, mono.get()); + assertSame(foo, mono.block()); } @Test @@ -106,36 +106,36 @@ public class RequestAttributeMethodArgumentResolverTests { Foo foo = new Foo(); this.exchange.getAttributes().put("specialFoo", foo); Mono mono = this.resolver.resolveArgument(param, null, this.exchange); - assertSame(foo, mono.get()); + assertSame(foo, mono.block()); } @Test public void resolveNotRequired() throws Exception { MethodParameter param = initMethodParameter(2); Mono mono = this.resolver.resolveArgument(param, null, this.exchange); - assertNull(mono.get()); + assertNull(mono.block()); Foo foo = new Foo(); this.exchange.getAttributes().put("foo", foo); mono = this.resolver.resolveArgument(param, null, this.exchange); - assertSame(foo, mono.get()); + assertSame(foo, mono.block()); } @Test public void resolveOptional() throws Exception { MethodParameter param = initMethodParameter(3); Mono mono = this.resolver.resolveArgument(param, null, this.exchange); - assertNotNull(mono.get()); - assertEquals(Optional.class, mono.get().getClass()); - assertFalse(((Optional) mono.get()).isPresent()); + assertNotNull(mono.block()); + assertEquals(Optional.class, mono.block().getClass()); + assertFalse(((Optional) mono.block()).isPresent()); Foo foo = new Foo(); this.exchange.getAttributes().put("foo", foo); mono = this.resolver.resolveArgument(param, null, this.exchange); - assertNotNull(mono.get()); - assertEquals(Optional.class, mono.get().getClass()); - Optional optional = (Optional) mono.get(); + assertNotNull(mono.block()); + assertEquals(Optional.class, mono.block().getClass()); + Optional optional = (Optional) mono.block(); assertTrue(optional.isPresent()); assertSame(foo, optional.get()); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMapMethodArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMapMethodArgumentResolverTests.java index a2e48579af..50ac008eb9 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMapMethodArgumentResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMapMethodArgumentResolverTests.java @@ -93,7 +93,7 @@ public class RequestHeaderMapMethodArgumentResolverTests { this.exchange.getRequest().getHeaders().add(name, value); Mono mono = this.resolver.resolveArgument(paramMap, null, this.exchange); - Object result = mono.get(); + Object result = mono.block(); assertTrue(result instanceof Map); assertEquals("Invalid result", expected, result); @@ -113,7 +113,7 @@ public class RequestHeaderMapMethodArgumentResolverTests { expected.add(name, value2); Mono mono = this.resolver.resolveArgument(paramMultiValueMap, null, this.exchange); - Object result = mono.get(); + Object result = mono.block(); assertTrue(result instanceof MultiValueMap); assertEquals("Invalid result", expected, result); @@ -133,7 +133,7 @@ public class RequestHeaderMapMethodArgumentResolverTests { expected.add(name, value2); Mono mono = this.resolver.resolveArgument(paramHttpHeaders, null, this.exchange); - Object result = mono.get(); + Object result = mono.block(); assertTrue(result instanceof HttpHeaders); assertEquals("Invalid result", expected, result); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolverTests.java index 5b22d56eb5..c2d76f5fa4 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolverTests.java @@ -109,7 +109,7 @@ public class RequestHeaderMethodArgumentResolverTests { this.exchange.getRequest().getHeaders().add("name", expected); Mono mono = this.resolver.resolveArgument(paramNamedDefaultValueStringHeader, null, this.exchange); - Object result = mono.get(); + Object result = mono.block(); assertTrue(result instanceof String); assertEquals(expected, result); } @@ -120,7 +120,7 @@ public class RequestHeaderMethodArgumentResolverTests { this.exchange.getRequest().getHeaders().put("name", Arrays.asList(expected)); Mono mono = this.resolver.resolveArgument(paramNamedValueStringArray, null, this.exchange); - Object result = mono.get(); + Object result = mono.block(); assertTrue(result instanceof String[]); assertArrayEquals(expected, (String[]) result); } @@ -128,7 +128,7 @@ public class RequestHeaderMethodArgumentResolverTests { @Test public void resolveDefaultValue() throws Exception { Mono mono = this.resolver.resolveArgument(paramNamedDefaultValueStringHeader, null, this.exchange); - Object result = mono.get(); + Object result = mono.block(); assertTrue(result instanceof String); assertEquals("bar", result); } @@ -138,7 +138,7 @@ public class RequestHeaderMethodArgumentResolverTests { System.setProperty("systemProperty", "bar"); try { Mono mono = this.resolver.resolveArgument(paramSystemProperty, null, this.exchange); - Object result = mono.get(); + Object result = mono.block(); assertTrue(result instanceof String); assertEquals("bar", result); } @@ -155,7 +155,7 @@ public class RequestHeaderMethodArgumentResolverTests { System.setProperty("systemProperty", "bar"); try { Mono mono = this.resolver.resolveArgument(paramResolvedNameWithExpression, null, this.exchange); - Object result = mono.get(); + Object result = mono.block(); assertTrue(result instanceof String); assertEquals(expected, result); } @@ -172,7 +172,7 @@ public class RequestHeaderMethodArgumentResolverTests { System.setProperty("systemProperty", "bar"); try { Mono mono = this.resolver.resolveArgument(paramResolvedNameWithPlaceholder, null, this.exchange); - Object result = mono.get(); + Object result = mono.block(); assertTrue(result instanceof String); assertEquals(expected, result); } @@ -196,7 +196,7 @@ public class RequestHeaderMethodArgumentResolverTests { this.exchange.getRequest().getHeaders().add("name", rfc1123val); Mono mono = this.resolver.resolveArgument(paramDate, null, this.exchange); - Object result = mono.get(); + Object result = mono.block(); assertTrue(result instanceof Date); assertEquals(new Date(rfc1123val), result); @@ -208,7 +208,7 @@ public class RequestHeaderMethodArgumentResolverTests { this.exchange.getRequest().getHeaders().add("name", rfc1123val); Mono mono = this.resolver.resolveArgument(paramInstant, null, this.exchange); - Object result = mono.get(); + Object result = mono.block(); assertTrue(result instanceof Instant); assertEquals(Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(rfc1123val)), result); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestParamMapMethodArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestParamMapMethodArgumentResolverTests.java index c33daa06ef..a8ba097182 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestParamMapMethodArgumentResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestParamMapMethodArgumentResolverTests.java @@ -94,7 +94,7 @@ public class RequestParamMapMethodArgumentResolverTests { Map expected = Collections.singletonMap(name, value); Mono mono = resolver.resolveArgument(paramMap, null, exchange); - Object result = mono.get(); + Object result = mono.block(); assertTrue(result instanceof Map); assertEquals(expected, result); @@ -112,7 +112,7 @@ public class RequestParamMapMethodArgumentResolverTests { expected.add(name, value2); Mono mono = this.resolver.resolveArgument(this.paramMultiValueMap, null, this.exchange); - Object result = mono.get(); + Object result = mono.block(); assertTrue(result instanceof MultiValueMap); assertEquals(expected, result); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestParamMethodArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestParamMethodArgumentResolverTests.java index b407348403..26b4ea8e5c 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestParamMethodArgumentResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestParamMethodArgumentResolverTests.java @@ -119,7 +119,7 @@ public class RequestParamMethodArgumentResolverTests { this.exchange.getRequest().getQueryParams().set("name", expected); Mono mono = this.resolver.resolveArgument(this.paramNamedDefaultValueString, null, this.exchange); - Object result = mono.get(); + Object result = mono.block(); assertTrue(result instanceof String); assertEquals("Invalid result", expected, result); @@ -131,7 +131,7 @@ public class RequestParamMethodArgumentResolverTests { this.exchange.getRequest().getQueryParams().put("name", Arrays.asList(expected)); Mono mono = this.resolver.resolveArgument(this.paramNamedStringArray, null, this.exchange); - Object result = mono.get(); + Object result = mono.block(); assertTrue(result instanceof String[]); assertArrayEquals(expected, (String[]) result); @@ -140,7 +140,7 @@ public class RequestParamMethodArgumentResolverTests { @Test public void resolveDefaultValue() throws Exception { Mono mono = this.resolver.resolveArgument(paramNamedDefaultValueString, null, this.exchange); - Object result = mono.get(); + Object result = mono.block(); assertTrue(result instanceof String); assertEquals("Invalid result", "bar", result); @@ -158,7 +158,7 @@ public class RequestParamMethodArgumentResolverTests { public void resolveSimpleTypeParam() throws Exception { this.exchange.getRequest().getQueryParams().set("stringNotAnnot", "plainValue"); Mono mono = this.resolver.resolveArgument(paramStringNotAnnot, null, this.exchange); - Object result = mono.get(); + Object result = mono.block(); assertTrue(result instanceof String); assertEquals("plainValue", result); @@ -167,7 +167,7 @@ public class RequestParamMethodArgumentResolverTests { @Test // SPR-8561 public void resolveSimpleTypeParamToNull() throws Exception { Mono mono = this.resolver.resolveArgument(paramStringNotAnnot, null, this.exchange); - Object result = mono.get(); + Object result = mono.block(); assertNull(result); } @@ -176,7 +176,7 @@ public class RequestParamMethodArgumentResolverTests { public void resolveEmptyValueToDefault() throws Exception { this.exchange.getRequest().getQueryParams().set("name", ""); Mono mono = this.resolver.resolveArgument(paramNamedDefaultValueString, null, this.exchange); - Object result = mono.get(); + Object result = mono.block(); assertEquals("bar", result); } @@ -185,7 +185,7 @@ public class RequestParamMethodArgumentResolverTests { public void resolveEmptyValueWithoutDefault() throws Exception { this.exchange.getRequest().getQueryParams().set("stringNotAnnot", ""); Mono mono = this.resolver.resolveArgument(paramStringNotAnnot, null, this.exchange); - Object result = mono.get(); + Object result = mono.block(); assertEquals("", result); } @@ -194,7 +194,7 @@ public class RequestParamMethodArgumentResolverTests { public void resolveEmptyValueRequiredWithoutDefault() throws Exception { this.exchange.getRequest().getQueryParams().set("name", ""); Mono mono = this.resolver.resolveArgument(paramRequired, null, this.exchange); - Object result = mono.get(); + Object result = mono.block(); assertEquals("", result); } @@ -202,13 +202,13 @@ public class RequestParamMethodArgumentResolverTests { @Test public void resolveOptionalParamValue() throws Exception { Mono mono = this.resolver.resolveArgument(paramOptional, null, this.exchange); - Object result = mono.get(); + Object result = mono.block(); assertEquals(Optional.empty(), result); this.exchange.getRequest().getQueryParams().set("name", "123"); mono = resolver.resolveArgument(paramOptional, null, this.exchange); - result = mono.get(); + result = mono.block(); assertEquals(Optional.class, result.getClass()); Optional value = (Optional) result; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java index 15043cb1f8..de06a67488 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java @@ -24,8 +24,6 @@ import java.util.List; import java.util.Set; import java.util.stream.Collectors; -import javax.print.attribute.standard.Media; - import org.junit.Test; import org.reactivestreams.Publisher; @@ -47,9 +45,9 @@ import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerMapping; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.HandlerResultHandler; -import org.springframework.web.reactive.accept.RequestedContentTypeResolver; import org.springframework.web.reactive.accept.FixedContentTypeResolver; import org.springframework.web.reactive.accept.HeaderContentTypeResolver; +import org.springframework.web.reactive.accept.RequestedContentTypeResolver; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange; import org.springframework.web.server.session.WebSessionManager; @@ -100,7 +98,7 @@ public class ResponseBodyResultHandlerTests { ServerWebExchange exchange = createExchange("/foo"); HandlerResult result = new HandlerResult(new Object(), "fooValue", ResolvableType.forClass(String.class)); - handler.handleResult(exchange, result).get(); + handler.handleResult(exchange, result).block(); assertEquals(contentType, exchange.getResponse().getHeaders().getContentType()); } @@ -114,7 +112,7 @@ public class ResponseBodyResultHandlerTests { HandlerResultHandler handler = createHandler(new StringEncoder(), new JacksonJsonEncoder()); HandlerResult result = new HandlerResult(new Object(), "fooValue", ResolvableType.forClass(String.class)); - handler.handleResult(exchange, result).get(); + handler.handleResult(exchange, result).block(); assertEquals(MediaType.APPLICATION_JSON, exchange.getResponse().getHeaders().getContentType()); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SessionAttributeMethodArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SessionAttributeMethodArgumentResolverTests.java index a85187a085..4eb0cab9d5 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SessionAttributeMethodArgumentResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SessionAttributeMethodArgumentResolverTests.java @@ -104,7 +104,7 @@ public class SessionAttributeMethodArgumentResolverTests { Foo foo = new Foo(); when(this.session.getAttribute("foo")).thenReturn(Optional.of(foo)); mono = this.resolver.resolveArgument(param, null, this.exchange); - assertSame(foo, mono.get()); + assertSame(foo, mono.block()); } @Test @@ -113,36 +113,36 @@ public class SessionAttributeMethodArgumentResolverTests { Foo foo = new Foo(); when(this.session.getAttribute("specialFoo")).thenReturn(Optional.of(foo)); Mono mono = this.resolver.resolveArgument(param, null, this.exchange); - assertSame(foo, mono.get()); + assertSame(foo, mono.block()); } @Test public void resolveNotRequired() throws Exception { MethodParameter param = initMethodParameter(2); Mono mono = this.resolver.resolveArgument(param, null, this.exchange); - assertNull(mono.get()); + assertNull(mono.block()); Foo foo = new Foo(); when(this.session.getAttribute("foo")).thenReturn(Optional.of(foo)); mono = this.resolver.resolveArgument(param, null, this.exchange); - assertSame(foo, mono.get()); + assertSame(foo, mono.block()); } @Test public void resolveOptional() throws Exception { MethodParameter param = initMethodParameter(3); Mono mono = this.resolver.resolveArgument(param, null, this.exchange); - assertNotNull(mono.get()); - assertEquals(Optional.class, mono.get().getClass()); - assertFalse(((Optional) mono.get()).isPresent()); + assertNotNull(mono.block()); + assertEquals(Optional.class, mono.block().getClass()); + assertFalse(((Optional) mono.block()).isPresent()); Foo foo = new Foo(); when(this.session.getAttribute("foo")).thenReturn(Optional.of(foo)); mono = this.resolver.resolveArgument(param, null, this.exchange); - assertNotNull(mono.get()); - assertEquals(Optional.class, mono.get().getClass()); - Optional optional = (Optional) mono.get(); + assertNotNull(mono.block()); + assertEquals(Optional.class, mono.block().getClass()); + Optional optional = (Optional) mono.block(); assertTrue(optional.isPresent()); assertSame(foo, optional.get()); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/UrlBasedViewResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/UrlBasedViewResolverTests.java index d521a6e7ef..98d16dc17a 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/UrlBasedViewResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/UrlBasedViewResolverTests.java @@ -48,10 +48,10 @@ public class UrlBasedViewResolverTests { resolver.setApplicationContext(context); Mono mono = resolver.resolveViewName("my-view", Locale.US); - assertNotNull(mono.get()); + assertNotNull(mono.block()); mono = resolver.resolveViewName("not-my-view", Locale.US); - assertNull(mono.get()); + assertNull(mono.block()); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/server/handler/ExceptionHandlingHttpHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/server/handler/ExceptionHandlingHttpHandlerTests.java index 0e1c0240bd..214fc0cf42 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/server/handler/ExceptionHandlingHttpHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/server/handler/ExceptionHandlingHttpHandlerTests.java @@ -62,7 +62,7 @@ public class ExceptionHandlingHttpHandlerTests { @Test public void handleErrorSignal() throws Exception { WebExceptionHandler exceptionHandler = new BadRequestExceptionHandler(); - createWebHandler(exceptionHandler).handle(this.exchange).get(); + createWebHandler(exceptionHandler).handle(this.exchange).block(); assertEquals(HttpStatus.BAD_REQUEST, this.response.getStatus()); } @@ -75,7 +75,7 @@ public class ExceptionHandlingHttpHandlerTests { new BadRequestExceptionHandler(), new UnresolvedExceptionHandler() }; - createWebHandler(exceptionHandlers).handle(this.exchange).get(); + createWebHandler(exceptionHandlers).handle(this.exchange).block(); assertEquals(HttpStatus.BAD_REQUEST, this.response.getStatus()); } @@ -83,7 +83,7 @@ public class ExceptionHandlingHttpHandlerTests { @Test public void unresolvedException() throws Exception { WebExceptionHandler exceptionHandler = new UnresolvedExceptionHandler(); - createWebHandler(exceptionHandler).handle(this.exchange).get(); + createWebHandler(exceptionHandler).handle(this.exchange).block(); assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, this.response.getStatus()); } @@ -91,7 +91,7 @@ public class ExceptionHandlingHttpHandlerTests { @Test public void thrownExceptionBecomesErrorSignal() throws Exception { WebExceptionHandler exceptionHandler = new BadRequestExceptionHandler(); - createWebHandler(exceptionHandler).handle(this.exchange).get(); + createWebHandler(exceptionHandler).handle(this.exchange).block(); assertEquals(HttpStatus.BAD_REQUEST, this.response.getStatus()); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/server/handler/FilteringWebHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/server/handler/FilteringWebHandlerTests.java index 57078bbe02..206c03e8b2 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/server/handler/FilteringWebHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/server/handler/FilteringWebHandlerTests.java @@ -23,20 +23,17 @@ import org.apache.commons.logging.LogFactory; import org.junit.Before; import org.junit.Test; import reactor.core.publisher.Mono; -import reactor.core.test.TestSubscriber; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.HttpHandler; import org.springframework.http.server.reactive.MockServerHttpRequest; import org.springframework.http.server.reactive.MockServerHttpResponse; -import org.springframework.http.server.reactive.ServerHttpRequest; -import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebExceptionHandler; import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilterChain; import org.springframework.web.server.WebHandler; -import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.WebHttpHandlerBuilder; import static org.junit.Assert.assertEquals; @@ -70,7 +67,7 @@ public class FilteringWebHandlerTests { TestFilter filter2 = new TestFilter(); TestFilter filter3 = new TestFilter(); HttpHandler httpHandler = createHttpHandler(webHandler, filter1, filter2, filter3); - httpHandler.handle(this.request, this.response).get(); + httpHandler.handle(this.request, this.response).block(); assertTrue(filter1.invoked()); assertTrue(filter2.invoked()); @@ -82,7 +79,7 @@ public class FilteringWebHandlerTests { public void zeroFilters() throws Exception { StubWebHandler webHandler = new StubWebHandler(); HttpHandler httpHandler = createHttpHandler(webHandler); - httpHandler.handle(this.request, this.response).get(); + httpHandler.handle(this.request, this.response).block(); assertTrue(webHandler.invoked()); } @@ -94,7 +91,7 @@ public class FilteringWebHandlerTests { ShortcircuitingFilter filter2 = new ShortcircuitingFilter(); TestFilter filter3 = new TestFilter(); HttpHandler httpHandler = createHttpHandler(webHandler, filter1, filter2, filter3); - httpHandler.handle(this.request, this.response).get(); + httpHandler.handle(this.request, this.response).block(); assertTrue(filter1.invoked()); assertTrue(filter2.invoked()); @@ -107,7 +104,7 @@ public class FilteringWebHandlerTests { StubWebHandler webHandler = new StubWebHandler(); AsyncFilter filter = new AsyncFilter(); HttpHandler httpHandler = createHttpHandler(webHandler, filter); - httpHandler.handle(this.request, this.response).get(); + httpHandler.handle(this.request, this.response).block(); assertTrue(filter.invoked()); assertTrue(webHandler.invoked()); @@ -118,7 +115,7 @@ public class FilteringWebHandlerTests { TestExceptionHandler exceptionHandler = new TestExceptionHandler(); HttpHandler handler = WebHttpHandlerBuilder.webHandler(new StubWebHandler()) .filters(new ExceptionFilter()).exceptionHandlers(exceptionHandler).build(); - handler.handle(this.request, this.response).get(); + handler.handle(this.request, this.response).block(); assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, this.response.getStatus()); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/server/session/DefaultWebSessionManagerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/server/session/DefaultWebSessionManagerTests.java index 13efe415b6..297510b6cd 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/server/session/DefaultWebSessionManagerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/server/session/DefaultWebSessionManagerTests.java @@ -66,32 +66,32 @@ public class DefaultWebSessionManagerTests { @Test public void getSessionWithoutStarting() throws Exception { this.idResolver.setIdsToResolve(Collections.emptyList()); - WebSession session = this.manager.getSession(this.exchange).get(); + WebSession session = this.manager.getSession(this.exchange).block(); session.save(); assertFalse(session.isStarted()); assertFalse(session.isExpired()); assertNull(this.idResolver.getSavedId()); - assertNull(this.manager.getSessionStore().retrieveSession(session.getId()).get()); + assertNull(this.manager.getSessionStore().retrieveSession(session.getId()).block()); } @Test public void startSessionExplicitly() throws Exception { this.idResolver.setIdsToResolve(Collections.emptyList()); - WebSession session = this.manager.getSession(this.exchange).get(); + WebSession session = this.manager.getSession(this.exchange).block(); session.start(); session.save(); String id = session.getId(); assertNotNull(this.idResolver.getSavedId()); assertEquals(id, this.idResolver.getSavedId()); - assertSame(session, this.manager.getSessionStore().retrieveSession(id).get()); + assertSame(session, this.manager.getSessionStore().retrieveSession(id).block()); } @Test public void startSessionImplicitly() throws Exception { this.idResolver.setIdsToResolve(Collections.emptyList()); - WebSession session = this.manager.getSession(this.exchange).get(); + WebSession session = this.manager.getSession(this.exchange).block(); session.getAttributes().put("foo", "bar"); session.save(); @@ -104,7 +104,7 @@ public class DefaultWebSessionManagerTests { this.manager.getSessionStore().storeSession(existing); this.idResolver.setIdsToResolve(Collections.singletonList("1")); - WebSession actual = this.manager.getSession(this.exchange).get(); + WebSession actual = this.manager.getSession(this.exchange).block(); assertSame(existing, actual); } @@ -117,7 +117,7 @@ public class DefaultWebSessionManagerTests { this.manager.getSessionStore().storeSession(existing); this.idResolver.setIdsToResolve(Collections.singletonList("1")); - WebSession actual = this.manager.getSession(this.exchange).get(); + WebSession actual = this.manager.getSession(this.exchange).block(); assertNotSame(existing, actual); } @@ -127,7 +127,7 @@ public class DefaultWebSessionManagerTests { this.manager.getSessionStore().storeSession(existing); this.idResolver.setIdsToResolve(Arrays.asList("1", "2", "3")); - WebSession actual = this.manager.getSession(this.exchange).get(); + WebSession actual = this.manager.getSession(this.exchange).block(); assertSame(existing, actual); } From 0a88d5983aedfaa7165d332d415df5e0341c4128 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 6 Jun 2016 17:43:47 -0400 Subject: [PATCH 390/498] Polish ResponseBodyArgumentResolver --- .../RequestBodyArgumentResolver.java | 53 ++++++++++++------- .../annotation/ResponseBodyResultHandler.java | 15 +++--- 2 files changed, 40 insertions(+), 28 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java index fca79df098..7c82bbdfcc 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java @@ -25,7 +25,6 @@ import reactor.core.publisher.Mono; import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.core.convert.ConversionService; -import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.MediaType; import org.springframework.http.converter.reactive.HttpMessageConverter; import org.springframework.ui.ModelMap; @@ -35,8 +34,13 @@ import org.springframework.web.reactive.result.method.HandlerMethodArgumentResol import org.springframework.web.server.ServerWebExchange; /** + * Resolves method arguments annotated with {@code @RequestBody} by reading and + * decoding the body of the request through a compatible + * {@code HttpMessageConverter}. + * * @author Sebastien Deleuze * @author Stephane Maldini + * @author Rossen Stoyanchev */ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolver { @@ -44,15 +48,30 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve private final ConversionService conversionService; - public RequestBodyArgumentResolver(List> messageConverters, + + /** + * Constructor with message converters and a ConversionService. + * @param converters converters for reading the request body with + * @param service for converting to other reactive types from Flux and Mono + */ + public RequestBodyArgumentResolver(List> converters, ConversionService service) { - Assert.notEmpty(messageConverters, "At least one message converter is required."); + + Assert.notEmpty(converters, "At least one message converter is required."); Assert.notNull(service, "'conversionService' is required."); - this.messageConverters = messageConverters; + this.messageConverters = converters; this.conversionService = service; } + /** + * Return the configured message converters. + */ + public List> getMessageConverters() { + return this.messageConverters; + } + + @Override public boolean supportsParameter(MethodParameter parameter) { return parameter.hasParameterAnnotation(RequestBody.class); @@ -70,35 +89,29 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve mediaType = MediaType.APPLICATION_OCTET_STREAM; } - Flux body = exchange.getRequest().getBody(); - Flux elementFlux; + Flux elementFlux = exchange.getRequest().getBody(); - HttpMessageConverter messageConverter = - resolveMessageConverter(elementType, mediaType); - if (messageConverter != null) { - elementFlux = messageConverter.read(elementType, exchange.getRequest()); - } - else { - elementFlux = body; + HttpMessageConverter converter = getMessageConverter(elementType, mediaType); + if (converter != null) { + elementFlux = converter.read(elementType, exchange.getRequest()); } - if (this.conversionService.canConvert(Publisher.class, type.getRawClass())) { - return Mono.just(this.conversionService - .convert(elementFlux, type.getRawClass())); - } - else if (type.getRawClass() == Flux.class) { + if (type.getRawClass() == Flux.class) { return Mono.just(elementFlux); } else if (type.getRawClass() == Mono.class) { return Mono.just(Mono.from(elementFlux)); } + else if (this.conversionService.canConvert(Publisher.class, type.getRawClass())) { + Object target = this.conversionService.convert(elementFlux, type.getRawClass()); + return Mono.just(target); + } // TODO Currently manage only "Foo" parameter, not "List" parameters, Stéphane is going to add toIterable/toIterator to Flux to support that use case return elementFlux.next().map(o -> o); } - private HttpMessageConverter resolveMessageConverter(ResolvableType type, - MediaType mediaType) { + private HttpMessageConverter getMessageConverter(ResolvableType type, MediaType mediaType) { for (HttpMessageConverter messageConverter : this.messageConverters) { if (messageConverter.canRead(type, mediaType)) { return messageConverter; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java index 56f7886ffe..6a04b69091 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java @@ -78,14 +78,6 @@ public class ResponseBodyResultHandler extends ContentNegotiatingResultHandlerSu this(converters, conversionService, new HeaderContentTypeResolver()); } - - /** - * Return the configured message converters. - */ - public List> getMessageConverters() { - return this.messageConverters; - } - /** * Constructor with message converters, a {@code ConversionService}, and a * {@code RequestedContentTypeResolver}. @@ -105,6 +97,13 @@ public class ResponseBodyResultHandler extends ContentNegotiatingResultHandlerSu } + /** + * Return the configured message converters. + */ + public List> getMessageConverters() { + return this.messageConverters; + } + @Override public boolean supports(HandlerResult result) { Object handler = result.getHandler(); From 7ec85b21bbf536292e1e1bffd82d41d10a45277e Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 6 Jun 2016 17:44:08 -0400 Subject: [PATCH 391/498] Remove unused test class --- ...equestAttributesArgumentResolverTests.java | 177 ------------------ 1 file changed, 177 deletions(-) delete mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/AbstractRequestAttributesArgumentResolverTests.java diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/AbstractRequestAttributesArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/AbstractRequestAttributesArgumentResolverTests.java deleted file mode 100644 index c84db1c111..0000000000 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/AbstractRequestAttributesArgumentResolverTests.java +++ /dev/null @@ -1,177 +0,0 @@ -/* - * 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.web.reactive.result.method.annotation; - -import java.lang.reflect.Method; -import java.net.URI; -import java.util.Optional; - -import org.junit.Before; -import org.junit.Test; -import reactor.core.publisher.Mono; -import reactor.core.test.TestSubscriber; - -import org.springframework.core.DefaultParameterNameDiscoverer; -import org.springframework.core.GenericTypeResolver; -import org.springframework.core.MethodParameter; -import org.springframework.core.annotation.SynthesizingMethodParameter; -import org.springframework.http.HttpMethod; -import org.springframework.http.server.reactive.MockServerHttpRequest; -import org.springframework.http.server.reactive.MockServerHttpResponse; -import org.springframework.http.server.reactive.ServerHttpRequest; -import org.springframework.util.ReflectionUtils; -import org.springframework.web.bind.annotation.RequestAttribute; -import org.springframework.web.bind.annotation.SessionAttribute; -import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver; -import org.springframework.web.server.ServerWebExchange; -import org.springframework.web.server.ServerWebInputException; -import org.springframework.web.server.WebSession; -import org.springframework.web.server.adapter.DefaultServerWebExchange; -import org.springframework.web.server.session.WebSessionManager; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -/** - * Base class for {@code @RequestAttribute} and {@code @SessionAttribute} method - * argument resolution tests. - * - * @author Rossen Stoyanchev - */ -public abstract class AbstractRequestAttributesArgumentResolverTests { - - private HandlerMethodArgumentResolver resolver; - - private ServerWebExchange exchange; - - private Method handleMethod; - - - @Before @SuppressWarnings("ConfusingArgumentToVarargsMethod") - public void setUp() throws Exception { - this.resolver = createResolver(); - - ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); - WebSessionManager sessionManager = mock(WebSessionManager.class); - this.exchange = new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); - - WebSession webSession = mock(WebSession.class); - when(sessionManager.getSession(this.exchange)).thenReturn(Mono.just(webSession)); - - this.handleMethod = ReflectionUtils.findMethod(getClass(), getHandleMethodName(), (Class[]) null); - } - - - protected abstract HandlerMethodArgumentResolver createResolver(); - - protected abstract String getHandleMethodName(); - - - @Test - public void supportsParameter() throws Exception { - assertTrue(this.resolver.supportsParameter(new MethodParameter(this.handleMethod, 0))); - assertFalse(this.resolver.supportsParameter(new MethodParameter(this.handleMethod, 4))); - } - - @Test - public void resolve() throws Exception { - MethodParameter param = initMethodParameter(0); - Mono mono = this.resolver.resolveArgument(param, null, this.exchange); - TestSubscriber - .subscribe(mono) - .assertError(ServerWebInputException.class); - - Foo foo = new Foo(); - this.exchange.getAttributes().put("foo", foo); - mono = this.resolver.resolveArgument(param, null, this.exchange); - assertSame(foo, mono.block()); - } - - @Test - public void resolveWithName() throws Exception { - MethodParameter param = initMethodParameter(1); - Foo foo = new Foo(); - this.exchange.getAttributes().put("specialFoo", foo); - Mono mono = this.resolver.resolveArgument(param, null, this.exchange); - assertSame(foo, mono.block()); - } - - @Test - public void resolveNotRequired() throws Exception { - MethodParameter param = initMethodParameter(2); - Mono mono = this.resolver.resolveArgument(param, null, this.exchange); - assertNull(mono.block()); - - Foo foo = new Foo(); - this.exchange.getAttributes().put("foo", foo); - mono = this.resolver.resolveArgument(param, null, this.exchange); - assertSame(foo, mono.block()); - } - - @Test - public void resolveOptional() throws Exception { - MethodParameter param = initMethodParameter(3); - Mono mono = this.resolver.resolveArgument(param, null, this.exchange); - assertNotNull(mono.block()); - assertEquals(Optional.class, mono.block().getClass()); - assertFalse(((Optional) mono.block()).isPresent()); - - Foo foo = new Foo(); - this.exchange.getAttributes().put("foo", foo); - mono = this.resolver.resolveArgument(param, null, this.exchange); - - assertNotNull(mono.block()); - assertEquals(Optional.class, mono.block().getClass()); - Optional optional = (Optional) mono.block(); - assertTrue(optional.isPresent()); - assertSame(foo, optional.get()); - } - - - private MethodParameter initMethodParameter(int parameterIndex) { - MethodParameter param = new SynthesizingMethodParameter(this.handleMethod, parameterIndex); - param.initParameterNameDiscovery(new DefaultParameterNameDiscoverer()); - GenericTypeResolver.resolveParameterType(param, this.resolver.getClass()); - return param; - } - - - @SuppressWarnings({"unused", "OptionalUsedAsFieldOrParameterType"}) - private void handleWithRequestAttribute( - @RequestAttribute Foo foo, - @RequestAttribute("specialFoo") Foo namedFoo, - @RequestAttribute(name="foo", required = false) Foo notRequiredFoo, - @RequestAttribute(name="foo") Optional optionalFoo) { - } - - @SuppressWarnings({"unused", "OptionalUsedAsFieldOrParameterType"}) - private void handleWithSessionAttribute( - @SessionAttribute Foo foo, - @SessionAttribute("specialFoo") Foo namedFoo, - @SessionAttribute(name="foo", required = false) Foo notRequiredFoo, - @SessionAttribute(name="foo") Optional optionalFoo) { - } - - private static class Foo { - } - -} From 36765f0f525a206922215793790aef7027257900 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 6 Jun 2016 22:24:46 -0400 Subject: [PATCH 392/498] Add protected method to reactive config --- .../web/reactive/config/WebReactiveConfiguration.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java index 9f4b0e495a..ee727228a6 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java @@ -91,6 +91,10 @@ public class WebReactiveConfiguration implements ApplicationContextAware { this.applicationContext = applicationContext; } + protected ApplicationContext getApplicationContext() { + return this.applicationContext; + } + @Bean public RequestMappingHandlerMapping requestMappingHandlerMapping() { From a8e5e40d970be20ff7c235b89bea15bc6184d547 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 7 Jun 2016 22:14:59 -0400 Subject: [PATCH 393/498] @RequestBody raises 415 if no matching converter --- .../RequestBodyArgumentResolver.java | 73 +++--- .../MediaTypeNotSupportedStatusException.java | 59 +++++ .../core/codec/support/Pojo.java | 5 + .../RequestBodyArgumentResolverTests.java | 245 ++++++++++++++++++ 4 files changed, 353 insertions(+), 29 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/server/MediaTypeNotSupportedStatusException.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java index 7c82bbdfcc..32348e008f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java @@ -17,6 +17,9 @@ package org.springframework.web.reactive.result.method.annotation; import java.util.List; +import java.util.stream.Collectors; + +import javax.xml.crypto.Data; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -25,6 +28,7 @@ import reactor.core.publisher.Mono; import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.core.convert.ConversionService; +import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.MediaType; import org.springframework.http.converter.reactive.HttpMessageConverter; import org.springframework.ui.ModelMap; @@ -32,6 +36,7 @@ import org.springframework.util.Assert; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver; import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.UnsupportedMediaTypeStatusException; /** * Resolves method arguments annotated with {@code @RequestBody} by reading and @@ -48,6 +53,8 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve private final ConversionService conversionService; + private final List supportedMediaTypes; + /** * Constructor with message converters and a ConversionService. @@ -61,6 +68,9 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve Assert.notNull(service, "'conversionService' is required."); this.messageConverters = converters; this.conversionService = service; + this.supportedMediaTypes = converters.stream() + .flatMap(converter -> converter.getReadableMediaTypes().stream()) + .collect(Collectors.toList()); } @@ -71,6 +81,13 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve return this.messageConverters; } + /** + * Return the configured {@link ConversionService}. + */ + public ConversionService getConversionService() { + return this.conversionService; + } + @Override public boolean supportsParameter(MethodParameter parameter) { @@ -82,42 +99,40 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve ServerWebExchange exchange) { ResolvableType type = ResolvableType.forMethodParameter(parameter); - ResolvableType elementType = type.hasGenerics() ? type.getGeneric(0) : type; - + boolean asyncType = isAsyncType(type); + ResolvableType elementType = (asyncType ? type.getGeneric(0) : type); MediaType mediaType = exchange.getRequest().getHeaders().getContentType(); if (mediaType == null) { mediaType = MediaType.APPLICATION_OCTET_STREAM; } - Flux elementFlux = exchange.getRequest().getBody(); - - HttpMessageConverter converter = getMessageConverter(elementType, mediaType); - if (converter != null) { - elementFlux = converter.read(elementType, exchange.getRequest()); - } - - if (type.getRawClass() == Flux.class) { - return Mono.just(elementFlux); - } - else if (type.getRawClass() == Mono.class) { - return Mono.just(Mono.from(elementFlux)); - } - else if (this.conversionService.canConvert(Publisher.class, type.getRawClass())) { - Object target = this.conversionService.convert(elementFlux, type.getRawClass()); - return Mono.just(target); - } - - // TODO Currently manage only "Foo" parameter, not "List" parameters, Stéphane is going to add toIterable/toIterator to Flux to support that use case - return elementFlux.next().map(o -> o); - } - - private HttpMessageConverter getMessageConverter(ResolvableType type, MediaType mediaType) { - for (HttpMessageConverter messageConverter : this.messageConverters) { - if (messageConverter.canRead(type, mediaType)) { - return messageConverter; + for (HttpMessageConverter converter : getMessageConverters()) { + if (converter.canRead(elementType, mediaType)) { + Flux elementFlux = converter.read(elementType, exchange.getRequest()); + if (Mono.class.equals(type.getRawClass())) { + Object value = Mono.from(elementFlux); + return Mono.just(value); + } + else if (Flux.class.equals(type.getRawClass())) { + return Mono.just(elementFlux); + } + else if (asyncType) { + Object value = getConversionService().convert(elementFlux, type.getRawClass()); + return Mono.just(value); + } + else { + // TODO Currently manage only "Foo" parameter, not "List" parameters, Stéphane is going to add toIterable/toIterator to Flux to support that use case + return elementFlux.next().map(o -> o); + } } } - return null; + + return Mono.error(new UnsupportedMediaTypeStatusException(mediaType, this.supportedMediaTypes)); + } + + private boolean isAsyncType(ResolvableType type) { + return (Mono.class.equals(type.getRawClass()) || Flux.class.equals(type.getRawClass()) || + getConversionService().canConvert(Publisher.class, type.getRawClass())); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/MediaTypeNotSupportedStatusException.java b/spring-web-reactive/src/main/java/org/springframework/web/server/MediaTypeNotSupportedStatusException.java new file mode 100644 index 0000000000..326fbb4395 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/MediaTypeNotSupportedStatusException.java @@ -0,0 +1,59 @@ +/* + * 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.web.server; + +import java.util.Collections; +import java.util.List; + +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; + +/** + * Exception for errors that fit response status 415 (unsupported media type). + * + * @author Rossen Stoyanchev + */ +public class MediaTypeNotSupportedStatusException extends ResponseStatusException { + + private final List supportedMediaTypes; + + + /** + * Constructor for when the Content-Type is invalid. + */ + public MediaTypeNotSupportedStatusException(String reason) { + super(HttpStatus.UNSUPPORTED_MEDIA_TYPE, reason); + this.supportedMediaTypes = Collections.emptyList(); + } + + /** + * Constructor for when the Content-Type is not supported. + */ + public MediaTypeNotSupportedStatusException(List supportedMediaTypes) { + super(HttpStatus.UNSUPPORTED_MEDIA_TYPE, "Unsupported media type", null); + this.supportedMediaTypes = Collections.unmodifiableList(supportedMediaTypes); + } + + + /** + * Return the list of supported content types in cases when the Accept + * header is parsed but not supported, or an empty list otherwise. + */ + public List getSupportedMediaTypes() { + return this.supportedMediaTypes; + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Pojo.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Pojo.java index bcf0b24265..1b7e792953 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Pojo.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Pojo.java @@ -68,4 +68,9 @@ public class Pojo { public int hashCode() { return 31 * foo.hashCode() + bar.hashCode(); } + + @Override + public String toString() { + return "Pojo[foo='" + this.foo + "\'" + ", bar='" + this.bar + "\']"; + } } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java new file mode 100644 index 0000000000..72ac3aece9 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java @@ -0,0 +1,245 @@ +/* + * 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.web.reactive.result.method.annotation; + +import java.lang.reflect.Method; +import java.net.URI; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; + +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.test.TestSubscriber; +import rx.Observable; +import rx.Single; + +import org.springframework.core.LocalVariableTableParameterNameDiscoverer; +import org.springframework.core.MethodParameter; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.core.annotation.SynthesizingMethodParameter; +import org.springframework.core.codec.Decoder; +import org.springframework.core.codec.support.JacksonJsonDecoder; +import org.springframework.core.codec.support.JsonObjectDecoder; +import org.springframework.core.codec.support.Pojo; +import org.springframework.core.codec.support.StringDecoder; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter; +import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.converter.reactive.CodecHttpMessageConverter; +import org.springframework.http.converter.reactive.HttpMessageConverter; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.ui.ExtendedModelMap; +import org.springframework.ui.ModelMap; +import org.springframework.util.ReflectionUtils; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.UnsupportedMediaTypeStatusException; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.DefaultWebSessionManager; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * Unit tests for {@link RequestBodyArgumentResolver}. + * @author Rossen Stoyanchev + */ +public class RequestBodyArgumentResolverTests { + + private RequestBodyArgumentResolver resolver; + + private ServerWebExchange exchange; + + private MockServerHttpRequest request; + + private ModelMap model; + + + @Before + public void setUp() throws Exception { + this.resolver = resolver(new JacksonJsonDecoder(new JsonObjectDecoder())); + this.request = new MockServerHttpRequest(HttpMethod.GET, new URI("/path")); + MockServerHttpResponse response = new MockServerHttpResponse(); + DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(); + this.exchange = new DefaultServerWebExchange(this.request, response, sessionManager); + this.model = new ExtendedModelMap(); + } + + + @Test + public void supports() throws Exception { + RequestBodyArgumentResolver resolver = resolver(new StringDecoder()); + + assertTrue(resolver.supportsParameter(parameter("monoPojo"))); + assertFalse(resolver.supportsParameter(parameter("paramWithoutAnnotation"))); + } + + @Test + public void missingContentType() throws Exception { + String body = "{\"bar\":\"BARBAR\",\"foo\":\"FOOFOO\"}"; + this.request.writeWith(Flux.just(dataBuffer(body))); + Mono result = this.resolver.resolveArgument(parameter("monoPojo"), this.model, this.exchange); + + TestSubscriber.subscribe(result) + .assertError(UnsupportedMediaTypeStatusException.class); + } + + @Test @SuppressWarnings("unchecked") + public void monoPojo() throws Exception { + String body = "{\"bar\":\"b1\",\"foo\":\"f1\"}"; + Mono mono = (Mono) resolve("monoPojo", Mono.class, body); + assertEquals(new Pojo("f1", "b1"), mono.block()); + } + + @Test @SuppressWarnings("unchecked") + public void fluxPojo() throws Exception { + String body = "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]"; + Flux flux = (Flux) resolve("fluxPojo", Flux.class, body); + assertEquals(Arrays.asList(new Pojo("f1", "b1"), new Pojo("f2", "b2")), flux.collectList().block()); + } + + @Test @SuppressWarnings("unchecked") + public void singlePojo() throws Exception { + String body = "{\"bar\":\"b1\",\"foo\":\"f1\"}"; + Single single = (Single) resolve("singlePojo", Single.class, body); + assertEquals(new Pojo("f1", "b1"), single.toBlocking().value()); + } + + @Test @SuppressWarnings("unchecked") + public void observablePojo() throws Exception { + String body = "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]"; + Observable observable = (Observable) resolve("observablePojo", Observable.class, body); + assertEquals(Arrays.asList(new Pojo("f1", "b1"), new Pojo("f2", "b2")), + observable.toList().toBlocking().first()); + } + + @Test @SuppressWarnings("unchecked") + public void futurePojo() throws Exception { + String body = "{\"bar\":\"b1\",\"foo\":\"f1\"}"; + assertEquals(new Pojo("f1", "b1"), resolve("futurePojo", CompletableFuture.class, body).get()); + } + + @Test + public void pojo() throws Exception { + String body = "{\"bar\":\"b1\",\"foo\":\"f1\"}"; + assertEquals(new Pojo("f1", "b1"), resolve("pojo", Pojo.class, body)); + } + + @Test + public void map() throws Exception { + String body = "{\"bar\":\"b1\",\"foo\":\"f1\"}"; + Map map = new HashMap<>(); + map.put("foo", "f1"); + map.put("bar", "b1"); + assertEquals(map, resolve("map", Map.class, body)); + } + + // TODO: @Ignore + + @Test + @Ignore + public void list() throws Exception { + String body = "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]"; + assertEquals(Arrays.asList(new Pojo("f1", "b1"), new Pojo("f2", "b2")), + resolve("list", List.class, body)); + } + + @Test + @Ignore + public void array() throws Exception { + String body = "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]"; + assertArrayEquals(new Pojo[] {new Pojo("f1", "b1"), new Pojo("f2", "b2")}, + resolve("array", Pojo[].class, body)); + } + + + @SuppressWarnings("unchecked") + private T resolve(String paramName, Class valueType, String body) { + this.request.getHeaders().setContentType(MediaType.APPLICATION_JSON); + this.request.writeWith(Flux.just(dataBuffer(body))); + Mono result = this.resolver.resolveArgument(parameter(paramName), this.model, this.exchange); + Object value = result.block(Duration.ofSeconds(5)); + assertNotNull(value); + assertTrue("Actual type: " + value.getClass(), valueType.isAssignableFrom(value.getClass())); + return (T) value; + } + + @SuppressWarnings("Convert2MethodRef") + private RequestBodyArgumentResolver resolver(Decoder... decoders) { + List> converters = new ArrayList<>(); + Arrays.asList(decoders).forEach(decoder -> converters.add(new CodecHttpMessageConverter<>(decoder))); + GenericConversionService service = new GenericConversionService(); + service.addConverter(new ReactiveStreamsToCompletableFutureConverter()); + service.addConverter(new ReactiveStreamsToRxJava1Converter()); + return new RequestBodyArgumentResolver(converters, service); + } + + @SuppressWarnings("ConfusingArgumentToVarargsMethod") + private MethodParameter parameter(String name) { + ParameterNameDiscoverer nameDiscoverer = new LocalVariableTableParameterNameDiscoverer(); + Method method = ReflectionUtils.findMethod(getClass(), "handle", (Class[]) null); + String[] names = nameDiscoverer.getParameterNames(method); + for (int i=0; i < names.length; i++) { + if (name.equals(names[i])) { + return new SynthesizingMethodParameter(method, i); + } + } + throw new IllegalArgumentException("Invalid parameter name '" + name + "'. Actual parameters: " + + Arrays.toString(names)); + } + + private DataBuffer dataBuffer(String body) { + byte[] bytes = body.getBytes(Charset.forName("UTF-8")); + ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); + return new DefaultDataBufferFactory().wrap(byteBuffer); + } + + + @SuppressWarnings("unused") + void handle( + @RequestBody Mono monoPojo, + @RequestBody Flux fluxPojo, + @RequestBody Single singlePojo, + @RequestBody Observable observablePojo, + @RequestBody CompletableFuture futurePojo, + @RequestBody Pojo pojo, + @RequestBody Map map, + @RequestBody List list, + @RequestBody Set set, + @RequestBody Pojo[] array, + Pojo paramWithoutAnnotation) { + } + +} From 4e3c4395936b74a382bd6ae98ae9ccfac6febf4a Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 8 Jun 2016 15:06:27 -0400 Subject: [PATCH 394/498] Polish Encoder and Decoder --- .../springframework/core/codec/Decoder.java | 39 ++++++++++-------- .../springframework/core/codec/Encoder.java | 41 +++++++++++-------- .../core/codec/support/AbstractDecoder.java | 2 +- .../core/codec/support/AbstractEncoder.java | 2 +- .../support/AbstractSingleValueEncoder.java | 4 +- .../core/codec/support/ByteBufferDecoder.java | 8 ++-- .../core/codec/support/ByteBufferEncoder.java | 10 ++--- .../codec/support/JacksonJsonDecoder.java | 6 +-- .../codec/support/JacksonJsonEncoder.java | 12 +++--- .../core/codec/support/Jaxb2Decoder.java | 10 ++--- .../core/codec/support/Jaxb2Encoder.java | 6 +-- .../core/codec/support/JsonObjectDecoder.java | 2 +- .../core/codec/support/ResourceDecoder.java | 10 ++--- .../core/codec/support/ResourceEncoder.java | 6 +-- .../core/codec/support/StringDecoder.java | 8 ++-- .../core/codec/support/StringEncoder.java | 10 ++--- .../core/codec/support/XmlEventDecoder.java | 2 +- .../RequestBodyArgumentResolver.java | 3 -- 18 files changed, 96 insertions(+), 85 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/Decoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/Decoder.java index 1b3d0410bb..cfecb61048 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/Decoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/Decoder.java @@ -26,35 +26,42 @@ import org.springframework.core.io.buffer.DataBuffer; import org.springframework.util.MimeType; /** - * Decode a stream of bytes to a stream of type {@code T}. + * Strategy for decoding a {@link DataBuffer} input stream into an output stream + * of elements of type {@code }. * * @author Sebastien Deleuze - * @see Encoder + * @author Rossen Stoyanchev + * @param the type of elements in the output stream */ public interface Decoder { /** - * Whether the decoder supports the given Java and mime type. - * @param type the stream element type to process. - * @param mimeType the mime type to process. - * @param hints Additional information about how to do decode, optional. - * @return {@code true} if can process; {@code false} otherwise + * Whether the decoder supports the given target element type and the MIME + * type of the source stream. + * + * @param elementType the target element type for the output stream + * @param mimeType the mime type associated with the stream to decode + * @param hints additional information about how to do decode, optional + * @return {@code true} if supported, {@code false} otherwise */ - boolean canDecode(ResolvableType type, MimeType mimeType, Object... hints); + boolean canDecode(ResolvableType elementType, MimeType mimeType, Object... hints); /** - * Decode an input {@link DataBuffer} stream to an output stream of {@code T}. - * @param inputStream the input stream to process. - * @param type the stream element type to process. - * @param mimeType the mime type to process. - * @param hints Additional information about how to do decode, optional. - * @return the output stream + * Decode a {@link DataBuffer} input stream into a Flux of {@code T}. + * + * @param inputStream the {@code DataBuffer} input stream to decode + * @param elementType the expected type of elements in the output stream; + * this type must have been previously passed to the {@link #canDecode} + * method and it must have returned {@code true}. + * @param mimeType the MIME type associated with the input stream, optional + * @param hints additional information about how to do decode, optional + * @return the output stream with decoded elements */ - Flux decode(Publisher inputStream, ResolvableType type, + Flux decode(Publisher inputStream, ResolvableType elementType, MimeType mimeType, Object... hints); /** - * Return the list of mime types this decoder supports. + * Return the list of MIME types this decoder supports. */ List getSupportedMimeTypes(); diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/Encoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/Encoder.java index f7b5395659..62fa412601 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/Encoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/Encoder.java @@ -27,34 +27,41 @@ import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.util.MimeType; /** - * Encode a stream of Objects of type {@code T} into a stream of bytes. + * Strategy to encode a stream of Objects of type {@code } into an output + * stream of bytes. * * @author Sebastien Deleuze - * @see Decoder + * @author Rossen Stoyanchev + * @param the type of elements in the input stream */ public interface Encoder { /** - * Indicate whether the given type and mime type can be processed by this encoder. - * @param type the stream element type to process. - * @param mimeType the mime type to process. - * @param hints Additional information about how to do decode, optional. - * @return {@code true} if can process; {@code false} otherwise + * Whether the encoder supports the given source element type and the MIME + * type for the output stream. + * + * @param elementType the type of elements in the source stream + * @param mimeType the MIME type for the output stream + * @param hints additional information about how to do encode, optional + * @return {@code true} if supported, {@code false} otherwise */ - boolean canEncode(ResolvableType type, MimeType mimeType, Object... hints); + boolean canEncode(ResolvableType elementType, MimeType mimeType, Object... hints); /** - * Encode an input stream of {@code T} to an output {@link DataBuffer} stream. - * @param inputStream the input stream to process. - * @param dataBufferFactory a buffer factory used to create the output - * @param type the stream element type to process. - * @param mimeType the mime type to process. - * @param hints Additional information about how to do decode, optional. + * Encode a stream of Objects of type {@code T} into a {@link DataBuffer} + * output stream. + * + * @param inputStream the input stream of Objects to encode + * @param bufferFactory for creating output stream {@code DataBuffer}'s + * @param elementType the expected type of elements in the input stream; + * this type must have been previously passed to the {@link #canEncode} + * method and it must have returned {@code true}. + * @param mimeType the MIME type for the output stream + * @param hints additional information about how to do encode, optional * @return the output stream */ - Flux encode(Publisher inputStream, - DataBufferFactory dataBufferFactory, ResolvableType type, - MimeType mimeType, Object... hints); + Flux encode(Publisher inputStream, DataBufferFactory bufferFactory, + ResolvableType elementType, MimeType mimeType, Object... hints); /** * Return the list of mime types this encoder supports. diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractDecoder.java index fa1b0102b5..b659b3b6d5 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractDecoder.java @@ -42,7 +42,7 @@ public abstract class AbstractDecoder implements Decoder { } @Override - public boolean canDecode(ResolvableType type, MimeType mimeType, Object... hints) { + public boolean canDecode(ResolvableType elementType, MimeType mimeType, Object... hints) { if (mimeType == null) { return true; } diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractEncoder.java index 98d1c022a4..079a8beb22 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractEncoder.java @@ -42,7 +42,7 @@ public abstract class AbstractEncoder implements Encoder { } @Override - public boolean canEncode(ResolvableType type, MimeType mimeType, Object... hints) { + public boolean canEncode(ResolvableType elementType, MimeType mimeType, Object... hints) { if (mimeType == null) { return true; } diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractSingleValueEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractSingleValueEncoder.java index 82264f003a..c023670d8b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractSingleValueEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractSingleValueEncoder.java @@ -37,13 +37,13 @@ public abstract class AbstractSingleValueEncoder extends AbstractEncoder { @Override public final Flux encode(Publisher inputStream, - DataBufferFactory dataBufferFactory, ResolvableType type, MimeType mimeType, + DataBufferFactory bufferFactory, ResolvableType elementType, MimeType mimeType, Object... hints) { return Flux.from(inputStream). take(1). concatMap(t -> { try { - return encode(t, dataBufferFactory, type, mimeType); + return encode(t, bufferFactory, elementType, mimeType); } catch (Exception ex) { return Flux.error(ex); diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferDecoder.java index a7fa543ad2..3f9ead7694 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferDecoder.java @@ -40,13 +40,13 @@ public class ByteBufferDecoder extends AbstractDecoder { @Override - public boolean canDecode(ResolvableType type, MimeType mimeType, Object... hints) { - Class clazz = type.getRawClass(); - return (super.canDecode(type, mimeType, hints) && ByteBuffer.class.isAssignableFrom(clazz)); + public boolean canDecode(ResolvableType elementType, MimeType mimeType, Object... hints) { + Class clazz = elementType.getRawClass(); + return (super.canDecode(elementType, mimeType, hints) && ByteBuffer.class.isAssignableFrom(clazz)); } @Override - public Flux decode(Publisher inputStream, ResolvableType type, + public Flux decode(Publisher inputStream, ResolvableType elementType, MimeType mimeType, Object... hints) { return Flux.from(inputStream).map((dataBuffer) -> { ByteBuffer copy = ByteBuffer.allocate(dataBuffer.readableByteCount()); diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferEncoder.java index fe1f0a9803..c6e69a494e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferEncoder.java @@ -38,17 +38,17 @@ public class ByteBufferEncoder extends AbstractEncoder { @Override - public boolean canEncode(ResolvableType type, MimeType mimeType, Object... hints) { - Class clazz = type.getRawClass(); - return (super.canEncode(type, mimeType, hints) && ByteBuffer.class.isAssignableFrom(clazz)); + public boolean canEncode(ResolvableType elementType, MimeType mimeType, Object... hints) { + Class clazz = elementType.getRawClass(); + return (super.canEncode(elementType, mimeType, hints) && ByteBuffer.class.isAssignableFrom(clazz)); } @Override public Flux encode(Publisher inputStream, - DataBufferFactory dataBufferFactory, ResolvableType type, MimeType mimeType, + DataBufferFactory bufferFactory, ResolvableType elementType, MimeType mimeType, Object... hints) { - return Flux.from(inputStream).map(dataBufferFactory::wrap); + return Flux.from(inputStream).map(bufferFactory::wrap); } } \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonDecoder.java index 3d6f406f7e..ccf8f5be3b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonDecoder.java @@ -61,14 +61,14 @@ public class JacksonJsonDecoder extends AbstractDecoder { } @Override - public Flux decode(Publisher inputStream, ResolvableType type, + public Flux decode(Publisher inputStream, ResolvableType elementType, MimeType mimeType, Object... hints) { - ObjectReader reader = this.mapper.readerFor(type.getRawClass()); + ObjectReader reader = this.mapper.readerFor(elementType.getRawClass()); Flux stream = Flux.from(inputStream); if (this.preProcessor != null) { - stream = this.preProcessor.decode(inputStream, type, mimeType, hints); + stream = this.preProcessor.decode(inputStream, elementType, mimeType, hints); } return stream.map(dataBuffer -> { diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonEncoder.java index 7c0a9924aa..084998b3f3 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonEncoder.java @@ -64,24 +64,24 @@ public class JacksonJsonEncoder extends AbstractEncoder { @Override public Flux encode(Publisher inputStream, - DataBufferFactory dataBufferFactory, ResolvableType type, MimeType mimeType, + DataBufferFactory bufferFactory, ResolvableType elementType, MimeType mimeType, Object... hints) { if (inputStream instanceof Mono) { // single object return Flux.from(inputStream) - .map(value -> serialize(value, dataBufferFactory)); + .map(value -> serialize(value, bufferFactory)); } else { // array Mono startArray = - Mono.just(dataBufferFactory.wrap(START_ARRAY_BUFFER)); + Mono.just(bufferFactory.wrap(START_ARRAY_BUFFER)); Flux arraySeparators = - Mono.just(dataBufferFactory.wrap(SEPARATOR_BUFFER)).repeat(); + Mono.just(bufferFactory.wrap(SEPARATOR_BUFFER)).repeat(); Mono endArray = - Mono.just(dataBufferFactory.wrap(END_ARRAY_BUFFER)); + Mono.just(bufferFactory.wrap(END_ARRAY_BUFFER)); Flux serializedObjects = Flux.from(inputStream) - .map(value -> serialize(value, dataBufferFactory)); + .map(value -> serialize(value, bufferFactory)); Flux array = Flux.zip(serializedObjects, arraySeparators) .flatMap(tuple -> Flux.just(tuple.getT1(), tuple.getT2())); diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Decoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Decoder.java index bcd86ff262..0bf14b8c4b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Decoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Decoder.java @@ -69,9 +69,9 @@ public class Jaxb2Decoder extends AbstractDecoder { } @Override - public boolean canDecode(ResolvableType type, MimeType mimeType, Object... hints) { - if (super.canDecode(type, mimeType, hints)) { - Class outputClass = type.getRawClass(); + public boolean canDecode(ResolvableType elementType, MimeType mimeType, Object... hints) { + if (super.canDecode(elementType, mimeType, hints)) { + Class outputClass = elementType.getRawClass(); return outputClass.isAnnotationPresent(XmlRootElement.class) || outputClass.isAnnotationPresent(XmlType.class); } @@ -81,9 +81,9 @@ public class Jaxb2Decoder extends AbstractDecoder { } @Override - public Flux decode(Publisher inputStream, ResolvableType type, + public Flux decode(Publisher inputStream, ResolvableType elementType, MimeType mimeType, Object... hints) { - Class outputClass = type.getRawClass(); + Class outputClass = elementType.getRawClass(); Flux xmlEventFlux = this.xmlEventDecoder.decode(inputStream, null, mimeType); diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Encoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Encoder.java index d2d9d2f6cb..e99c44d1d2 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Encoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Encoder.java @@ -48,9 +48,9 @@ public class Jaxb2Encoder extends AbstractSingleValueEncoder { } @Override - public boolean canEncode(ResolvableType type, MimeType mimeType, Object... hints) { - if (super.canEncode(type, mimeType, hints)) { - Class outputClass = type.getRawClass(); + public boolean canEncode(ResolvableType elementType, MimeType mimeType, Object... hints) { + if (super.canEncode(elementType, mimeType, hints)) { + Class outputClass = elementType.getRawClass(); return outputClass.isAnnotationPresent(XmlRootElement.class) || outputClass.isAnnotationPresent(XmlType.class); } diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectDecoder.java index fbcb00ab3d..e45dc66c34 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectDecoder.java @@ -93,7 +93,7 @@ public class JsonObjectDecoder extends AbstractDecoder { } @Override - public Flux decode(Publisher inputStream, ResolvableType type, + public Flux decode(Publisher inputStream, ResolvableType elementType, MimeType mimeType, Object... hints) { return Flux.from(inputStream) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ResourceDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ResourceDecoder.java index 40ef6d0dbe..d36aba1854 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ResourceDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ResourceDecoder.java @@ -43,17 +43,17 @@ public class ResourceDecoder extends AbstractDecoder { } @Override - public boolean canDecode(ResolvableType type, MimeType mimeType, Object... hints) { - Class clazz = type.getRawClass(); + public boolean canDecode(ResolvableType elementType, MimeType mimeType, Object... hints) { + Class clazz = elementType.getRawClass(); return (InputStreamResource.class.equals(clazz) || clazz.isAssignableFrom(ByteArrayResource.class)) && - super.canDecode(type, mimeType, hints); + super.canDecode(elementType, mimeType, hints); } @Override - public Flux decode(Publisher inputStream, ResolvableType type, + public Flux decode(Publisher inputStream, ResolvableType elementType, MimeType mimeType, Object... hints) { - Class clazz = type.getRawClass(); + Class clazz = elementType.getRawClass(); Mono byteArray = Flux.from(inputStream). reduce(DataBuffer::write). diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ResourceEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ResourceEncoder.java index 742c8de634..5498ac153e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ResourceEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ResourceEncoder.java @@ -52,9 +52,9 @@ public class ResourceEncoder extends AbstractSingleValueEncoder { } @Override - public boolean canEncode(ResolvableType type, MimeType mimeType, Object... hints) { - Class clazz = type.getRawClass(); - return (super.canEncode(type, mimeType, hints) && + public boolean canEncode(ResolvableType elementType, MimeType mimeType, Object... hints) { + Class clazz = elementType.getRawClass(); + return (super.canEncode(elementType, mimeType, hints) && Resource.class.isAssignableFrom(clazz)); } diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java index ca477cde85..a9673665c4 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java @@ -70,13 +70,13 @@ public class StringDecoder extends AbstractDecoder { } @Override - public boolean canDecode(ResolvableType type, MimeType mimeType, Object... hints) { - return super.canDecode(type, mimeType, hints) && - String.class.equals(type.getRawClass()); + public boolean canDecode(ResolvableType elementType, MimeType mimeType, Object... hints) { + return super.canDecode(elementType, mimeType, hints) && + String.class.equals(elementType.getRawClass()); } @Override - public Flux decode(Publisher inputStream, ResolvableType type, + public Flux decode(Publisher inputStream, ResolvableType elementType, MimeType mimeType, Object... hints) { Flux inputFlux = Flux.from(inputStream); if (this.splitOnNewline) { diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringEncoder.java index 21c09cfc82..80e64f37a2 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringEncoder.java @@ -43,14 +43,14 @@ public class StringEncoder extends AbstractEncoder { @Override - public boolean canEncode(ResolvableType type, MimeType mimeType, Object... hints) { - Class clazz = type.getRawClass(); - return (super.canEncode(type, mimeType, hints) && String.class.equals(clazz)); + public boolean canEncode(ResolvableType elementType, MimeType mimeType, Object... hints) { + Class clazz = elementType.getRawClass(); + return (super.canEncode(elementType, mimeType, hints) && String.class.equals(clazz)); } @Override public Flux encode(Publisher inputStream, - DataBufferFactory dataBufferFactory, ResolvableType type, MimeType mimeType, + DataBufferFactory bufferFactory, ResolvableType elementType, MimeType mimeType, Object... hints) { Charset charset; if (mimeType != null && mimeType.getCharSet() != null) { @@ -61,7 +61,7 @@ public class StringEncoder extends AbstractEncoder { } return Flux.from(inputStream).map(s -> { byte[] bytes = s.getBytes(charset); - DataBuffer dataBuffer = dataBufferFactory.allocateBuffer(bytes.length); + DataBuffer dataBuffer = bufferFactory.allocateBuffer(bytes.length); dataBuffer.write(bytes); return dataBuffer; }); diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/XmlEventDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/XmlEventDecoder.java index 3f27b97fb7..9dd8a1a9a4 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/XmlEventDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/XmlEventDecoder.java @@ -83,7 +83,7 @@ public class XmlEventDecoder extends AbstractDecoder { } @Override - public Flux decode(Publisher inputStream, ResolvableType type, + public Flux decode(Publisher inputStream, ResolvableType elementType, MimeType mimeType, Object... hints) { Flux flux = Flux.from(inputStream); if (useAalto && aaltoPresent) { diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java index 32348e008f..1d56999bab 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java @@ -19,8 +19,6 @@ package org.springframework.web.reactive.result.method.annotation; import java.util.List; import java.util.stream.Collectors; -import javax.xml.crypto.Data; - import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -28,7 +26,6 @@ import reactor.core.publisher.Mono; import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.core.convert.ConversionService; -import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.MediaType; import org.springframework.http.converter.reactive.HttpMessageConverter; import org.springframework.ui.ModelMap; From 22a6ca1f412856f8a83494f83fee2c7ca3e48a9e Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 8 Jun 2016 16:02:19 -0400 Subject: [PATCH 395/498] Remove Pojo from tests that shouldn't depend on it The Pojo test class from the codec package will end up in spring-core. This commit ensures it is used only from classes that also belong to spring-core. --- .../config/WebReactiveConfigurationTests.java | 17 ++- .../RequestBodyArgumentResolverTests.java | 119 +++++++++++++----- .../view/HttpMessageConverterViewTests.java | 12 +- 3 files changed, 106 insertions(+), 42 deletions(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/WebReactiveConfigurationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/WebReactiveConfigurationTests.java index 74ddf2f793..9e39c8e588 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/WebReactiveConfigurationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/WebReactiveConfigurationTests.java @@ -21,6 +21,8 @@ import java.util.Collections; import java.util.List; import java.util.concurrent.CompletableFuture; +import javax.xml.bind.annotation.XmlRootElement; + import org.junit.Before; import org.junit.Test; import reactor.core.publisher.Flux; @@ -36,7 +38,6 @@ import org.springframework.core.ResolvableType; import org.springframework.core.codec.support.JacksonJsonEncoder; import org.springframework.core.codec.support.Jaxb2Decoder; import org.springframework.core.codec.support.Jaxb2Encoder; -import org.springframework.core.codec.support.Pojo; import org.springframework.core.codec.support.StringDecoder; import org.springframework.core.codec.support.StringEncoder; import org.springframework.core.convert.ConversionService; @@ -141,8 +142,8 @@ public class WebReactiveConfigurationTests { assertHasConverter(converters, ByteBuffer.class, MediaType.APPLICATION_OCTET_STREAM); assertHasConverter(converters, String.class, MediaType.TEXT_PLAIN); assertHasConverter(converters, Resource.class, MediaType.IMAGE_PNG); - assertHasConverter(converters, Pojo.class, MediaType.APPLICATION_XML); - assertHasConverter(converters, Pojo.class, MediaType.APPLICATION_JSON); + assertHasConverter(converters, TestBean.class, MediaType.APPLICATION_XML); + assertHasConverter(converters, TestBean.class, MediaType.APPLICATION_JSON); name = "mvcConversionService"; ConversionService service = context.getBean(name, ConversionService.class); @@ -161,7 +162,7 @@ public class WebReactiveConfigurationTests { assertEquals(2, converters.size()); assertHasConverter(converters, String.class, MediaType.TEXT_PLAIN); - assertHasConverter(converters, Pojo.class, MediaType.APPLICATION_XML); + assertHasConverter(converters, TestBean.class, MediaType.APPLICATION_XML); } @Test @@ -193,8 +194,8 @@ public class WebReactiveConfigurationTests { assertHasConverter(converters, ByteBuffer.class, MediaType.APPLICATION_OCTET_STREAM); assertHasConverter(converters, String.class, MediaType.TEXT_PLAIN); assertHasConverter(converters, Resource.class, MediaType.IMAGE_PNG); - assertHasConverter(converters, Pojo.class, MediaType.APPLICATION_XML); - assertHasConverter(converters, Pojo.class, MediaType.APPLICATION_JSON); + assertHasConverter(converters, TestBean.class, MediaType.APPLICATION_XML); + assertHasConverter(converters, TestBean.class, MediaType.APPLICATION_JSON); } @Test @@ -274,4 +275,8 @@ public class WebReactiveConfigurationTests { } } + + @XmlRootElement + static class TestBean { + } } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java index 72ac3aece9..56178053d3 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java @@ -28,6 +28,8 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.CompletableFuture; +import javax.xml.bind.annotation.XmlRootElement; + import org.junit.Before; import org.junit.Ignore; import org.junit.Test; @@ -44,7 +46,6 @@ import org.springframework.core.annotation.SynthesizingMethodParameter; import org.springframework.core.codec.Decoder; import org.springframework.core.codec.support.JacksonJsonDecoder; import org.springframework.core.codec.support.JsonObjectDecoder; -import org.springframework.core.codec.support.Pojo; import org.springframework.core.codec.support.StringDecoder; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter; @@ -102,7 +103,7 @@ public class RequestBodyArgumentResolverTests { public void supports() throws Exception { RequestBodyArgumentResolver resolver = resolver(new StringDecoder()); - assertTrue(resolver.supportsParameter(parameter("monoPojo"))); + assertTrue(resolver.supportsParameter(parameter("monoTestBean"))); assertFalse(resolver.supportsParameter(parameter("paramWithoutAnnotation"))); } @@ -110,51 +111,51 @@ public class RequestBodyArgumentResolverTests { public void missingContentType() throws Exception { String body = "{\"bar\":\"BARBAR\",\"foo\":\"FOOFOO\"}"; this.request.writeWith(Flux.just(dataBuffer(body))); - Mono result = this.resolver.resolveArgument(parameter("monoPojo"), this.model, this.exchange); + Mono result = this.resolver.resolveArgument(parameter("monoTestBean"), this.model, this.exchange); TestSubscriber.subscribe(result) .assertError(UnsupportedMediaTypeStatusException.class); } @Test @SuppressWarnings("unchecked") - public void monoPojo() throws Exception { + public void monoTestBean() throws Exception { String body = "{\"bar\":\"b1\",\"foo\":\"f1\"}"; - Mono mono = (Mono) resolve("monoPojo", Mono.class, body); - assertEquals(new Pojo("f1", "b1"), mono.block()); + Mono mono = (Mono) resolve("monoTestBean", Mono.class, body); + assertEquals(new TestBean("f1", "b1"), mono.block()); } @Test @SuppressWarnings("unchecked") - public void fluxPojo() throws Exception { + public void fluxTestBean() throws Exception { String body = "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]"; - Flux flux = (Flux) resolve("fluxPojo", Flux.class, body); - assertEquals(Arrays.asList(new Pojo("f1", "b1"), new Pojo("f2", "b2")), flux.collectList().block()); + Flux flux = (Flux) resolve("fluxTestBean", Flux.class, body); + assertEquals(Arrays.asList(new TestBean("f1", "b1"), new TestBean("f2", "b2")), flux.collectList().block()); } @Test @SuppressWarnings("unchecked") - public void singlePojo() throws Exception { + public void singleTestBean() throws Exception { String body = "{\"bar\":\"b1\",\"foo\":\"f1\"}"; - Single single = (Single) resolve("singlePojo", Single.class, body); - assertEquals(new Pojo("f1", "b1"), single.toBlocking().value()); + Single single = (Single) resolve("singleTestBean", Single.class, body); + assertEquals(new TestBean("f1", "b1"), single.toBlocking().value()); } @Test @SuppressWarnings("unchecked") - public void observablePojo() throws Exception { + public void observableTestBean() throws Exception { String body = "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]"; - Observable observable = (Observable) resolve("observablePojo", Observable.class, body); - assertEquals(Arrays.asList(new Pojo("f1", "b1"), new Pojo("f2", "b2")), + Observable observable = (Observable) resolve("observableTestBean", Observable.class, body); + assertEquals(Arrays.asList(new TestBean("f1", "b1"), new TestBean("f2", "b2")), observable.toList().toBlocking().first()); } @Test @SuppressWarnings("unchecked") - public void futurePojo() throws Exception { + public void futureTestBean() throws Exception { String body = "{\"bar\":\"b1\",\"foo\":\"f1\"}"; - assertEquals(new Pojo("f1", "b1"), resolve("futurePojo", CompletableFuture.class, body).get()); + assertEquals(new TestBean("f1", "b1"), resolve("futureTestBean", CompletableFuture.class, body).get()); } @Test - public void pojo() throws Exception { + public void testBean() throws Exception { String body = "{\"bar\":\"b1\",\"foo\":\"f1\"}"; - assertEquals(new Pojo("f1", "b1"), resolve("pojo", Pojo.class, body)); + assertEquals(new TestBean("f1", "b1"), resolve("testBean", TestBean.class, body)); } @Test @@ -172,7 +173,7 @@ public class RequestBodyArgumentResolverTests { @Ignore public void list() throws Exception { String body = "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]"; - assertEquals(Arrays.asList(new Pojo("f1", "b1"), new Pojo("f2", "b2")), + assertEquals(Arrays.asList(new TestBean("f1", "b1"), new TestBean("f2", "b2")), resolve("list", List.class, body)); } @@ -180,8 +181,8 @@ public class RequestBodyArgumentResolverTests { @Ignore public void array() throws Exception { String body = "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]"; - assertArrayEquals(new Pojo[] {new Pojo("f1", "b1"), new Pojo("f2", "b2")}, - resolve("array", Pojo[].class, body)); + assertArrayEquals(new TestBean[] {new TestBean("f1", "b1"), new TestBean("f2", "b2")}, + resolve("array", TestBean[].class, body)); } @@ -229,17 +230,71 @@ public class RequestBodyArgumentResolverTests { @SuppressWarnings("unused") void handle( - @RequestBody Mono monoPojo, - @RequestBody Flux fluxPojo, - @RequestBody Single singlePojo, - @RequestBody Observable observablePojo, - @RequestBody CompletableFuture futurePojo, - @RequestBody Pojo pojo, + @RequestBody Mono monoTestBean, + @RequestBody Flux fluxTestBean, + @RequestBody Single singleTestBean, + @RequestBody Observable observableTestBean, + @RequestBody CompletableFuture futureTestBean, + @RequestBody TestBean testBean, @RequestBody Map map, - @RequestBody List list, - @RequestBody Set set, - @RequestBody Pojo[] array, - Pojo paramWithoutAnnotation) { + @RequestBody List list, + @RequestBody Set set, + @RequestBody TestBean[] array, + TestBean paramWithoutAnnotation) { } + + @XmlRootElement + static class TestBean { + + private String foo; + + private String bar; + + public TestBean() { + } + + public TestBean(String foo, String bar) { + this.foo = foo; + this.bar = bar; + } + + public String getFoo() { + return this.foo; + } + + public void setFoo(String foo) { + this.foo = foo; + } + + public String getBar() { + return this.bar; + } + + public void setBar(String bar) { + this.bar = bar; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o instanceof TestBean) { + TestBean other = (TestBean) o; + return this.foo.equals(other.foo) && this.bar.equals(other.bar); + } + return false; + } + + @Override + public int hashCode() { + return 31 * foo.hashCode() + bar.hashCode(); + } + + @Override + public String toString() { + return "TestBean[foo='" + this.foo + "\'" + ", bar='" + this.bar + "\']"; + } + } } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/HttpMessageConverterViewTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/HttpMessageConverterViewTests.java index d840a572bc..126a7bcdc2 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/HttpMessageConverterViewTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/HttpMessageConverterViewTests.java @@ -23,6 +23,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -33,7 +34,6 @@ import reactor.core.test.TestSubscriber; import org.springframework.core.ResolvableType; import org.springframework.core.codec.support.JacksonJsonEncoder; import org.springframework.core.codec.support.Jaxb2Encoder; -import org.springframework.core.codec.support.Pojo; import org.springframework.core.codec.support.StringEncoder; import org.springframework.core.io.buffer.support.DataBufferTestUtils; import org.springframework.http.HttpMethod; @@ -156,8 +156,11 @@ public class HttpMessageConverterViewTests { @Test public void render() throws Exception { - this.model.addAttribute("pojo", new Pojo("foo", "bar")); - this.view.setModelKeys(Collections.singleton("pojo")); + Map pojoData = new LinkedHashMap<>(); + pojoData.put("foo", "f"); + pojoData.put("bar", "b"); + this.model.addAttribute("pojoData", pojoData); + this.view.setModelKeys(Collections.singleton("pojoData")); MockServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/path")); MockServerHttpResponse response = new MockServerHttpResponse(); @@ -168,8 +171,9 @@ public class HttpMessageConverterViewTests { TestSubscriber .subscribe(response.getBody()) - .assertValuesWith(buf -> assertEquals("{\"foo\":\"foo\",\"bar\":\"bar\"}", + .assertValuesWith(buf -> assertEquals("{\"foo\":\"f\",\"bar\":\"b\"}", DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); } + } From 4fd80bbb67d24d1ff26d8fe1583a5dda5e4c178f Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 8 Jun 2016 18:10:27 -0400 Subject: [PATCH 396/498] Add Jackons decoder tests related List vs Flux --- .../support/JacksonJsonDecoderTests.java | 88 ++++++++++++++++--- 1 file changed, 76 insertions(+), 12 deletions(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonDecoderTests.java index 788d610629..c9c9110589 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonDecoderTests.java @@ -16,6 +16,11 @@ package org.springframework.core.codec.support; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; + +import org.junit.Ignore; import org.junit.Test; import reactor.core.publisher.Flux; import reactor.core.test.TestSubscriber; @@ -29,27 +34,86 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; /** + * Unit tests for {@link JacksonJsonDecoder}. * @author Sebastien Deleuze + * @author Rossen Stoyanchev */ public class JacksonJsonDecoderTests extends AbstractDataBufferAllocatingTestCase { - private final JacksonJsonDecoder decoder = new JacksonJsonDecoder(); - @Test public void canDecode() { - assertTrue(this.decoder.canDecode(null, MediaType.APPLICATION_JSON)); - assertFalse(this.decoder.canDecode(null, MediaType.APPLICATION_XML)); + JacksonJsonDecoder decoder = new JacksonJsonDecoder(); + + assertTrue(decoder.canDecode(null, MediaType.APPLICATION_JSON)); + assertFalse(decoder.canDecode(null, MediaType.APPLICATION_XML)); } @Test - public void decode() { - Flux source = - Flux.just(stringBuffer("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}")); - Flux output = - this.decoder.decode(source, ResolvableType.forClass(Pojo.class), null); - TestSubscriber - .subscribe(output) - .assertValues(new Pojo("foofoo", "barbar")); + public void decodePojo() { + Flux source = Flux.just(stringBuffer("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}")); + ResolvableType elementType = ResolvableType.forClass(Pojo.class); + Flux flux = new JacksonJsonDecoder().decode(source, elementType, null); + + TestSubscriber.subscribe(flux).assertNoError().assertComplete(). + assertValues(new Pojo("foofoo", "barbar")); } + @Test + @Ignore // Issues 112 (no generic type), otherwise works + public void decodeToListWithoutObjectDecoder() throws Exception { + Flux source = Flux.just(stringBuffer( + "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]")); + + Method method = getClass().getDeclaredMethod("handle", List.class); + ResolvableType elementType = ResolvableType.forMethodParameter(method, 0); + Flux flux = new JacksonJsonDecoder().decode(source, elementType, null); + + TestSubscriber.subscribe(flux).assertNoError().assertComplete(). + assertValues(Arrays.asList(new Pojo("f1", "b1"), new Pojo("f2", "b2"))); + } + + @Test + @Ignore // Issue 109 + public void decodeToFluxWithoutObjectDecoder() throws Exception { + Flux source = Flux.just(stringBuffer( + "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]")); + + ResolvableType elementType = ResolvableType.forClass(Pojo.class); + Flux flux = new JacksonJsonDecoder().decode(source, elementType, null); + + TestSubscriber.subscribe(flux).assertNoError().assertComplete(). + assertValues(new Pojo("f1", "b1"), new Pojo("f2", "b2")); + } + + @Test + @Ignore // Issue 109 + public void decodeToListWithObjectDecoder() throws Exception { + Flux source = Flux.just(stringBuffer( + "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]")); + + Method method = getClass().getDeclaredMethod("handle", List.class); + ResolvableType elementType = ResolvableType.forMethodParameter(method, 0); + Flux flux = new JacksonJsonDecoder(new JsonObjectDecoder()).decode(source, elementType, null); + + TestSubscriber.subscribe(flux).assertNoError().assertComplete(). + assertValues(Arrays.asList(new Pojo("f1", "b1"), new Pojo("f2", "b2"))); + } + + @Test + public void decodeToFluxWithObjectDecoder() throws Exception { + Flux source = Flux.just(stringBuffer( + "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]")); + + ResolvableType elementType = ResolvableType.forClass(Pojo.class); + Flux flux = new JacksonJsonDecoder(new JsonObjectDecoder()).decode(source, elementType, null); + + TestSubscriber.subscribe(flux).assertNoError().assertComplete(). + assertValues(new Pojo("f1", "b1"), new Pojo("f2", "b2")); + } + + @SuppressWarnings("unused") + void handle(List list) { + } + + } From 61240ee517026082ce14b83bab9e9edb5eccf9e6 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Thu, 9 Jun 2016 13:34:30 +0200 Subject: [PATCH 397/498] Fixed javadoc typo --- .../springframework/core/io/buffer/support/DataBufferUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java index 09270ff7db..ae990bb96a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java @@ -57,7 +57,7 @@ public abstract class DataBufferUtils { /** * Reads the given {@code InputStream} into a {@code Flux} of - * {@code DataBuffer}s. Closes the stream when the flux inputStream terminated. + * {@code DataBuffer}s. Closes the input stream when the flux is terminated. * @param inputStream the input stream to read from * @param dataBufferFactory the factory to create data buffers with * @param bufferSize the maximum size of the data buffers From b5394a1f50aff18a73615f90ee69af1930dc9698 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Fri, 10 Jun 2016 10:39:26 +0200 Subject: [PATCH 398/498] Polishing --- .../springframework/core/codec/support/XmlEventDecoder.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/XmlEventDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/XmlEventDecoder.java index 9dd8a1a9a4..a5b0be0bdb 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/XmlEventDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/XmlEventDecoder.java @@ -143,13 +143,14 @@ public class XmlEventDecoder extends AbstractDecoder { } } } - DataBufferUtils.release(dataBuffer); return Flux.fromIterable(events); } catch (XMLStreamException ex) { return Mono.error(ex); } - + finally { + DataBufferUtils.release(dataBuffer); + } } } } From 54c2e866c34c1aa5c5b0d24522d3833a5fb5a668 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Fri, 10 Jun 2016 10:59:32 +0200 Subject: [PATCH 399/498] Renamed getSupportedMimeTypes() in [En|De]coder Renamed getSupportedMimeTypes() to getEncodableMimeTypes and getDecodableMimeTypes. This will allow for both Encoder and Decoder to be implemented in the same class. This issue fixes #113. --- .../springframework/core/codec/Decoder.java | 2 +- .../springframework/core/codec/Encoder.java | 2 +- .../core/codec/support/AbstractDecoder.java | 17 +++++++--------- .../core/codec/support/AbstractEncoder.java | 17 +++++++--------- .../reactive/CodecHttpMessageConverter.java | 20 ++++++++++++------- 5 files changed, 29 insertions(+), 29 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/Decoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/Decoder.java index cfecb61048..e3b6804f95 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/Decoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/Decoder.java @@ -63,6 +63,6 @@ public interface Decoder { /** * Return the list of MIME types this decoder supports. */ - List getSupportedMimeTypes(); + List getDecodableMimeTypes(); } diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/Encoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/Encoder.java index 62fa412601..93ea941ee8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/Encoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/Encoder.java @@ -66,6 +66,6 @@ public interface Encoder { /** * Return the list of mime types this encoder supports. */ - List getSupportedMimeTypes(); + List getEncodableMimeTypes(); } diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractDecoder.java index b659b3b6d5..dde5ee7d6f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractDecoder.java @@ -26,19 +26,20 @@ import org.springframework.util.MimeType; /** * @author Sebastien Deleuze + * @author Arjen Poutsma */ public abstract class AbstractDecoder implements Decoder { - private List supportedMimeTypes = Collections.emptyList(); + private List decodableMimeTypes = Collections.emptyList(); protected AbstractDecoder(MimeType... supportedMimeTypes) { - this.supportedMimeTypes = Arrays.asList(supportedMimeTypes); + this.decodableMimeTypes = Arrays.asList(supportedMimeTypes); } @Override - public List getSupportedMimeTypes() { - return this.supportedMimeTypes; + public List getDecodableMimeTypes() { + return this.decodableMimeTypes; } @Override @@ -46,12 +47,8 @@ public abstract class AbstractDecoder implements Decoder { if (mimeType == null) { return true; } - for (MimeType supportedMimeType : this.supportedMimeTypes) { - if (supportedMimeType.isCompatibleWith(mimeType)) { - return true; - } - } - return false; + return this.decodableMimeTypes.stream(). + anyMatch(mt -> mt.isCompatibleWith(mimeType)); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractEncoder.java index 079a8beb22..9bb7cfd315 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractEncoder.java @@ -26,19 +26,20 @@ import org.springframework.util.MimeType; /** * @author Sebastien Deleuze + * @author Arjen Poutsma */ public abstract class AbstractEncoder implements Encoder { - private List supportedMimeTypes = Collections.emptyList(); + private List encodableMimeTypes = Collections.emptyList(); protected AbstractEncoder(MimeType... supportedMimeTypes) { - this.supportedMimeTypes = Arrays.asList(supportedMimeTypes); + this.encodableMimeTypes = Arrays.asList(supportedMimeTypes); } @Override - public List getSupportedMimeTypes() { - return this.supportedMimeTypes; + public List getEncodableMimeTypes() { + return this.encodableMimeTypes; } @Override @@ -46,12 +47,8 @@ public abstract class AbstractEncoder implements Encoder { if (mimeType == null) { return true; } - for (MimeType supportedMimeType : this.supportedMimeTypes) { - if (supportedMimeType.isCompatibleWith(mimeType)) { - return true; - } - } - return false; + return this.encodableMimeTypes.stream(). + anyMatch(mt -> mt.isCompatibleWith(mimeType)); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/CodecHttpMessageConverter.java b/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/CodecHttpMessageConverter.java index 207fbf506a..804ea72207 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/CodecHttpMessageConverter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/CodecHttpMessageConverter.java @@ -18,7 +18,6 @@ package org.springframework.http.converter.reactive; import java.util.Collections; import java.util.List; -import java.util.stream.Collectors; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -47,6 +46,10 @@ public class CodecHttpMessageConverter implements HttpMessageConverter { private final Decoder decoder; + private final List readableMediaTypes; + + private final List writableMediaTypes; + /** * Create a {@code CodecHttpMessageConverter} with the given {@link Encoder}. When * using this constructor, all read-related methods will in {@code false} or an @@ -76,6 +79,13 @@ public class CodecHttpMessageConverter implements HttpMessageConverter { public CodecHttpMessageConverter(Encoder encoder, Decoder decoder) { this.encoder = encoder; this.decoder = decoder; + + this.readableMediaTypes = decoder != null ? + MediaTypeUtils.toMediaTypes(decoder.getDecodableMimeTypes()) : + Collections.emptyList(); + this.writableMediaTypes = encoder != null ? + MediaTypeUtils.toMediaTypes(encoder.getEncodableMimeTypes()) : + Collections.emptyList(); } @Override @@ -90,16 +100,12 @@ public class CodecHttpMessageConverter implements HttpMessageConverter { @Override public List getReadableMediaTypes() { - return this.decoder != null ? this.decoder.getSupportedMimeTypes().stream(). - map(MediaTypeUtils::toMediaType). - collect(Collectors.toList()) : Collections.emptyList(); + return this.readableMediaTypes; } @Override public List getWritableMediaTypes() { - return this.encoder != null ? this.encoder.getSupportedMimeTypes().stream(). - map(MediaTypeUtils::toMediaType). - collect(Collectors.toList()) : Collections.emptyList(); + return this.writableMediaTypes; } @Override From 622d11dbce7209647fab87e5624ecdd9bf50cd67 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Fri, 10 Jun 2016 15:21:36 +0200 Subject: [PATCH 400/498] Upgraded Netty to 4.1.0.Final --- spring-web-reactive/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index 7e18f1e4e1..ab52dcf29a 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -33,7 +33,7 @@ ext { rxJavaVersion = '1.1.5' tomcatVersion = '8.5.2' jettyVersion = '9.3.8.v20160314' - nettyVersion = '4.1.0.CR6' + nettyVersion = '4.1.0.Final' jacksonVersion = '2.7.3' javadocLinks = [ From ea21643a29fcf8d631ac4caa4b0deeac5bd6071d Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Fri, 10 Jun 2016 12:26:06 +0200 Subject: [PATCH 401/498] Various DataBuffer improvements - Added fromIndex parameter to indexOf and lastIndexOf - Moved DataBuffer.tokenize to StringEncoder, as that's the only place it's used. --- .../core/codec/support/StringDecoder.java | 28 +++++++++--- .../core/io/buffer/DataBuffer.java | 6 ++- .../core/io/buffer/DefaultDataBuffer.java | 18 ++++++-- .../core/io/buffer/NettyDataBuffer.java | 25 ++++++++--- .../io/buffer/support/DataBufferUtils.java | 43 ------------------- .../codec/support/StringDecoderTests.java | 3 +- .../core/io/buffer/DataBufferTests.java | 20 +++++++-- .../buffer/support/DataBufferUtilsTests.java | 22 ---------- 8 files changed, 77 insertions(+), 88 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java index a9673665c4..6ef47d1184 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java @@ -19,7 +19,9 @@ package org.springframework.core.codec.support; import java.nio.CharBuffer; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.List; +import java.util.function.IntPredicate; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -47,6 +49,8 @@ public class StringDecoder extends AbstractDecoder { public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; + private static final IntPredicate NEWLINE_DELIMITER = b -> b == '\n' || b == '\r'; + private final boolean splitOnNewline; /** @@ -80,11 +84,7 @@ public class StringDecoder extends AbstractDecoder { MimeType mimeType, Object... hints) { Flux inputFlux = Flux.from(inputStream); if (this.splitOnNewline) { - inputFlux = inputFlux.flatMap(dataBuffer -> { - List tokens = - DataBufferUtils.tokenize(dataBuffer, b -> b == '\n' || b == '\r'); - return Flux.fromIterable(tokens); - }); + inputFlux = inputFlux.flatMap(StringDecoder::splitOnNewline); } Charset charset = getCharset(mimeType); return inputFlux.map(dataBuffer -> { @@ -94,6 +94,24 @@ public class StringDecoder extends AbstractDecoder { }); } + private static Flux splitOnNewline(DataBuffer dataBuffer) { + List results = new ArrayList(); + int startIdx = 0; + int endIdx = 0; + final int limit = dataBuffer.readableByteCount(); + do { + endIdx = dataBuffer.indexOf(NEWLINE_DELIMITER, startIdx); + int length = endIdx != -1 ? endIdx - startIdx + 1 : limit - startIdx; + DataBuffer token = dataBuffer.slice(startIdx, length); + results.add(DataBufferUtils.retain(token)); + startIdx = endIdx + 1; + } + while (startIdx < limit && endIdx != -1); + DataBufferUtils.release(dataBuffer); + return Flux.fromIterable(results); + } + + private Charset getCharset(MimeType mimeType) { if (mimeType != null && mimeType.getCharset() != null) { return mimeType.getCharset(); diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DataBuffer.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DataBuffer.java index 8fd84476ff..822221bdb1 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DataBuffer.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DataBuffer.java @@ -38,19 +38,21 @@ public interface DataBuffer { * Returns the index of the first byte in this buffer that matches the given * predicate. * @param predicate the predicate to match + * @param fromIndex the index to start the search from * @return the index of the first byte that matches {@code predicate}; or {@code -1} * if none match */ - int indexOf(IntPredicate predicate); + int indexOf(IntPredicate predicate, int fromIndex); /** * Returns the index of the last byte in this buffer that matches the given * predicate. * @param predicate the predicate to match + * @param fromIndex the index to start the search from * @return the index of the last byte that matches {@code predicate}; or {@code -1} * if none match */ - int lastIndexOf(IntPredicate predicate); + int lastIndexOf(IntPredicate predicate, int fromIndex); /** * Returns the number of bytes that can be read from this data buffer. diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java index b46a03ce90..5b13e7e1a2 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java @@ -84,8 +84,16 @@ public class DefaultDataBuffer implements DataBuffer { } @Override - public int indexOf(IntPredicate predicate) { - for (int i = 0; i < readableByteCount(); i++) { + public int indexOf(IntPredicate predicate, int fromIndex) { + Assert.notNull(predicate, "'predicate' must not be null"); + + if (fromIndex < 0) { + fromIndex = 0; + } + else if (fromIndex >= this.writePosition) { + return -1; + } + for (int i = fromIndex; i < this.writePosition; i++) { byte b = this.byteBuffer.get(i); if (predicate.test(b)) { return i; @@ -95,8 +103,10 @@ public class DefaultDataBuffer implements DataBuffer { } @Override - public int lastIndexOf(IntPredicate predicate) { - for (int i = readableByteCount() - 1; i >= 0; i--) { + public int lastIndexOf(IntPredicate predicate, int fromIndex) { + Assert.notNull(predicate, "'predicate' must not be null"); + int i = Math.min(fromIndex, this.writePosition - 1); + for (; i >= 0; i--) { byte b = this.byteBuffer.get(i); if (predicate.test(b)) { return i; diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/NettyDataBuffer.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/NettyDataBuffer.java index 4686490fd1..628f7bec66 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/NettyDataBuffer.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/NettyDataBuffer.java @@ -69,15 +69,28 @@ public class NettyDataBuffer implements PooledDataBuffer { } @Override - public int indexOf(IntPredicate predicate) { - IntPredicate negated = predicate.negate(); - return this.byteBuf.forEachByte(negated::test); + public int indexOf(IntPredicate predicate, int fromIndex) { + Assert.notNull(predicate, "'predicate' must not be null"); + if (fromIndex < 0) { + fromIndex = 0; + } + else if (fromIndex >= this.byteBuf.writerIndex()) { + return -1; + } + int length = this.byteBuf.writerIndex() - fromIndex; + + return this.byteBuf.forEachByte(fromIndex, length, predicate.negate()::test); } @Override - public int lastIndexOf(IntPredicate predicate) { - IntPredicate negated = predicate.negate(); - return this.byteBuf.forEachByteDesc(negated::test); + public int lastIndexOf(IntPredicate predicate, int fromIndex) { + Assert.notNull(predicate, "'predicate' must not be null"); + if (fromIndex < 0) { + return -1; + } + fromIndex = Math.min(fromIndex, this.byteBuf.writerIndex() - 1); + + return this.byteBuf.forEachByteDesc(0, fromIndex, predicate.negate()::test); } @Override diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java index ae990bb96a..249eb1146a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/support/DataBufferUtils.java @@ -21,13 +21,9 @@ import java.io.InputStream; import java.nio.ByteBuffer; import java.nio.channels.Channels; import java.nio.channels.ReadableByteChannel; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; import java.util.concurrent.atomic.AtomicLong; import java.util.function.BiFunction; import java.util.function.Consumer; -import java.util.function.IntPredicate; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -124,45 +120,6 @@ public abstract class DataBufferUtils { }); } - /** - * Tokenize the {@link DataBuffer} using the given delimiter - * function. Does not include the delimiter in the result. - * @param dataBuffer the data buffer to tokenize - * @param delimiter the delimiter function - * @return the tokens - */ - public static List tokenize(DataBuffer dataBuffer, - IntPredicate delimiter) { - Assert.notNull(dataBuffer, "'dataBuffer' must not be null"); - Assert.notNull(delimiter, "'delimiter' must not be null"); - - List results = new ArrayList(); - int idx; - do { - idx = dataBuffer.indexOf(delimiter); - if (idx < 0) { - results.add(dataBuffer); - } - else { - if (idx > 0) { - DataBuffer slice = dataBuffer.slice(0, idx); - slice = retain(slice); - results.add(slice); - } - int remainingLen = dataBuffer.readableByteCount() - (idx + 1); - if (remainingLen > 0) { - dataBuffer = dataBuffer.slice(idx + 1, remainingLen); - } - else { - release(dataBuffer); - idx = -1; - } - } - } - while (idx != -1); - return Collections.unmodifiableList(results); - } - /** * Retains the given data buffer, it it is a {@link PooledDataBuffer}. * @param dataBuffer the data buffer to retain diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringDecoderTests.java index 2178eab29c..f89be25a22 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringDecoderTests.java @@ -82,8 +82,7 @@ public class StringDecoderTests extends AbstractDataBufferAllocatingTestCase { TestSubscriber .subscribe(output) .assertNoError() - .assertComplete() - .assertValues("foo", "bar", "baz"); + .assertComplete().assertValues("\n", "foo\r", "\n", "bar\r", "\n", "baz"); } @Test diff --git a/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/DataBufferTests.java b/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/DataBufferTests.java index 4014fd629a..df4a02d789 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/DataBufferTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/DataBufferTests.java @@ -190,10 +190,16 @@ public class DataBufferTests extends AbstractDataBufferAllocatingTestCase { DataBuffer buffer = createDataBuffer(3); buffer.write(new byte[]{'a', 'b', 'c'}); - int result = buffer.indexOf(b -> b == 'c'); + int result = buffer.indexOf(b -> b == 'c', 0); assertEquals(2, result); - result = buffer.indexOf(b -> b == 'z'); + result = buffer.indexOf(b -> b == 'c', Integer.MIN_VALUE); + assertEquals(2, result); + + result = buffer.indexOf(b -> b == 'c', Integer.MAX_VALUE); + assertEquals(-1, result); + + result = buffer.indexOf(b -> b == 'z', 0); assertEquals(-1, result); release(buffer); @@ -204,10 +210,16 @@ public class DataBufferTests extends AbstractDataBufferAllocatingTestCase { DataBuffer buffer = createDataBuffer(3); buffer.write(new byte[]{'a', 'b', 'c'}); - int result = buffer.lastIndexOf(b -> b == 'b'); + int result = buffer.lastIndexOf(b -> b == 'b', 3); assertEquals(1, result); - result = buffer.lastIndexOf(b -> b == 'z'); + result = buffer.lastIndexOf(b -> b == 'b', Integer.MAX_VALUE); + assertEquals(1, result); + + result = buffer.lastIndexOf(b -> b == 'b', Integer.MIN_VALUE); + assertEquals(-1, result); + + result = buffer.lastIndexOf(b -> b == 'z', 0); assertEquals(-1, result); release(buffer); diff --git a/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/support/DataBufferUtilsTests.java b/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/support/DataBufferUtilsTests.java index 1a2cba08ec..63029c5882 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/support/DataBufferUtilsTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/support/DataBufferUtilsTests.java @@ -19,10 +19,8 @@ package org.springframework.core.io.buffer.support; import java.io.InputStream; import java.net.URI; import java.nio.channels.FileChannel; -import java.nio.charset.StandardCharsets; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; -import java.util.List; import org.junit.Test; import reactor.core.publisher.Flux; @@ -31,7 +29,6 @@ import reactor.core.test.TestSubscriber; import org.springframework.core.io.buffer.AbstractDataBufferAllocatingTestCase; import org.springframework.core.io.buffer.DataBuffer; -import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; /** @@ -112,23 +109,4 @@ public class DataBufferUtilsTests extends AbstractDataBufferAllocatingTestCase { release(baz); } - @Test - public void tokenize() { - DataBuffer dataBuffer = stringBuffer("-foo--bar-"); - - List results = DataBufferUtils.tokenize(dataBuffer, b -> b == '-'); - assertEquals(2, results.size()); - - DataBuffer result = results.get(0); - String value = DataBufferTestUtils.dumpString(result, StandardCharsets.UTF_8); - assertEquals("foo", value); - - result = results.get(1); - value = DataBufferTestUtils.dumpString(result, StandardCharsets.UTF_8); - assertEquals("bar", value); - - results.stream().forEach(b -> release(b)); - } - - } \ No newline at end of file From 0a2c3c3744e7696d51b2c5f33c21e1ebc35663a6 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 10 Jun 2016 14:53:19 -0400 Subject: [PATCH 402/498] Polish RequestBodyArgumentResolver --- .../annotation/RequestBodyArgumentResolver.java | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java index 1d56999bab..69d6d892c7 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java @@ -96,8 +96,9 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve ServerWebExchange exchange) { ResolvableType type = ResolvableType.forMethodParameter(parameter); - boolean asyncType = isAsyncType(type); - ResolvableType elementType = (asyncType ? type.getGeneric(0) : type); + boolean isAsyncType = isAsyncType(type); + ResolvableType elementType = (isAsyncType ? type.getGeneric(0) : type); + MediaType mediaType = exchange.getRequest().getHeaders().getContentType(); if (mediaType == null) { mediaType = MediaType.APPLICATION_OCTET_STREAM; @@ -107,18 +108,15 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve if (converter.canRead(elementType, mediaType)) { Flux elementFlux = converter.read(elementType, exchange.getRequest()); if (Mono.class.equals(type.getRawClass())) { - Object value = Mono.from(elementFlux); - return Mono.just(value); + return Mono.just(Mono.from(elementFlux)); } else if (Flux.class.equals(type.getRawClass())) { return Mono.just(elementFlux); } - else if (asyncType) { - Object value = getConversionService().convert(elementFlux, type.getRawClass()); - return Mono.just(value); + else if (isAsyncType) { + return Mono.just(getConversionService().convert(elementFlux, type.getRawClass())); } else { - // TODO Currently manage only "Foo" parameter, not "List" parameters, Stéphane is going to add toIterable/toIterator to Flux to support that use case return elementFlux.next().map(o -> o); } } From 2f8baac4e02ac0b8099a84d2d7c6d401bc7f96d8 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 10 Jun 2016 15:52:29 -0400 Subject: [PATCH 403/498] Validation support for @RequestBody with @Validated --- .../RequestBodyArgumentResolver.java | 63 +++++++++++++++++++ .../RequestBodyArgumentResolverTests.java | 63 ++++++++++++++----- 2 files changed, 112 insertions(+), 14 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java index 69d6d892c7..b58123dadd 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java @@ -16,6 +16,7 @@ package org.springframework.web.reactive.result.method.annotation; +import java.lang.annotation.Annotation; import java.util.List; import java.util.stream.Collectors; @@ -23,16 +24,25 @@ import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import org.springframework.core.Conventions; import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; +import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.convert.ConversionService; import org.springframework.http.MediaType; import org.springframework.http.converter.reactive.HttpMessageConverter; import org.springframework.ui.ModelMap; import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.validation.BeanPropertyBindingResult; +import org.springframework.validation.Errors; +import org.springframework.validation.SmartValidator; +import org.springframework.validation.Validator; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver; import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.ServerWebInputException; import org.springframework.web.server.UnsupportedMediaTypeStatusException; /** @@ -50,6 +60,8 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve private final ConversionService conversionService; + private final Validator validator; + private final List supportedMediaTypes; @@ -61,10 +73,23 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve public RequestBodyArgumentResolver(List> converters, ConversionService service) { + this(converters, service, null); + } + + /** + * Constructor with message converters and a ConversionService. + * @param converters converters for reading the request body with + * @param service for converting to other reactive types from Flux and Mono + * @param validator validator to validate decoded objects with + */ + public RequestBodyArgumentResolver(List> converters, + ConversionService service, Validator validator) { + Assert.notEmpty(converters, "At least one message converter is required."); Assert.notNull(service, "'conversionService' is required."); this.messageConverters = converters; this.conversionService = service; + this.validator = validator; this.supportedMediaTypes = converters.stream() .flatMap(converter -> converter.getReadableMediaTypes().stream()) .collect(Collectors.toList()); @@ -107,6 +132,11 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve for (HttpMessageConverter converter : getMessageConverters()) { if (converter.canRead(elementType, mediaType)) { Flux elementFlux = converter.read(elementType, exchange.getRequest()); + + if (this.validator != null) { + elementFlux= applyValidationIfApplicable(elementFlux, parameter); + } + if (Mono.class.equals(type.getRawClass())) { return Mono.just(Mono.from(elementFlux)); } @@ -130,4 +160,37 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve getConversionService().canConvert(Publisher.class, type.getRawClass())); } + protected Flux applyValidationIfApplicable(Flux elementFlux, MethodParameter methodParam) { + Annotation[] annotations = methodParam.getParameterAnnotations(); + for (Annotation ann : annotations) { + Validated validAnnot = AnnotationUtils.getAnnotation(ann, Validated.class); + if (validAnnot != null || ann.annotationType().getSimpleName().startsWith("Valid")) { + Object hints = (validAnnot != null ? validAnnot.value() : AnnotationUtils.getValue(ann)); + Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); + return elementFlux.map(element -> { + validate(element, validationHints, methodParam); + return element; + }); + } + } + return elementFlux; + } + + /** + * TODO: replace with use of DataBinder + */ + private void validate(Object target, Object[] validationHints, MethodParameter methodParam) { + String name = Conventions.getVariableNameForParameter(methodParam); + Errors errors = new BeanPropertyBindingResult(target, name); + if (!ObjectUtils.isEmpty(validationHints) && this.validator instanceof SmartValidator) { + ((SmartValidator) this.validator).validate(target, errors, validationHints); + } + else if (this.validator != null) { + this.validator.validate(target, errors); + } + if (errors.hasErrors()) { + throw new ServerWebInputException("Validation failed", methodParam); + } + } + } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java index 56178053d3..50bb84234d 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java @@ -27,7 +27,6 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.CompletableFuture; - import javax.xml.bind.annotation.XmlRootElement; import org.junit.Before; @@ -61,8 +60,12 @@ import org.springframework.http.server.reactive.MockServerHttpResponse; import org.springframework.ui.ExtendedModelMap; import org.springframework.ui.ModelMap; import org.springframework.util.ReflectionUtils; +import org.springframework.validation.Errors; +import org.springframework.validation.Validator; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.ServerWebInputException; import org.springframework.web.server.UnsupportedMediaTypeStatusException; import org.springframework.web.server.adapter.DefaultServerWebExchange; import org.springframework.web.server.session.DefaultWebSessionManager; @@ -120,28 +123,28 @@ public class RequestBodyArgumentResolverTests { @Test @SuppressWarnings("unchecked") public void monoTestBean() throws Exception { String body = "{\"bar\":\"b1\",\"foo\":\"f1\"}"; - Mono mono = (Mono) resolve("monoTestBean", Mono.class, body); + Mono mono = (Mono) resolveValue("monoTestBean", Mono.class, body); assertEquals(new TestBean("f1", "b1"), mono.block()); } @Test @SuppressWarnings("unchecked") public void fluxTestBean() throws Exception { String body = "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]"; - Flux flux = (Flux) resolve("fluxTestBean", Flux.class, body); + Flux flux = (Flux) resolveValue("fluxTestBean", Flux.class, body); assertEquals(Arrays.asList(new TestBean("f1", "b1"), new TestBean("f2", "b2")), flux.collectList().block()); } @Test @SuppressWarnings("unchecked") public void singleTestBean() throws Exception { String body = "{\"bar\":\"b1\",\"foo\":\"f1\"}"; - Single single = (Single) resolve("singleTestBean", Single.class, body); + Single single = (Single) resolveValue("singleTestBean", Single.class, body); assertEquals(new TestBean("f1", "b1"), single.toBlocking().value()); } @Test @SuppressWarnings("unchecked") public void observableTestBean() throws Exception { String body = "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]"; - Observable observable = (Observable) resolve("observableTestBean", Observable.class, body); + Observable observable = (Observable) resolveValue("observableTestBean", Observable.class, body); assertEquals(Arrays.asList(new TestBean("f1", "b1"), new TestBean("f2", "b2")), observable.toList().toBlocking().first()); } @@ -149,13 +152,13 @@ public class RequestBodyArgumentResolverTests { @Test @SuppressWarnings("unchecked") public void futureTestBean() throws Exception { String body = "{\"bar\":\"b1\",\"foo\":\"f1\"}"; - assertEquals(new TestBean("f1", "b1"), resolve("futureTestBean", CompletableFuture.class, body).get()); + assertEquals(new TestBean("f1", "b1"), resolveValue("futureTestBean", CompletableFuture.class, body).get()); } @Test public void testBean() throws Exception { String body = "{\"bar\":\"b1\",\"foo\":\"f1\"}"; - assertEquals(new TestBean("f1", "b1"), resolve("testBean", TestBean.class, body)); + assertEquals(new TestBean("f1", "b1"), resolveValue("testBean", TestBean.class, body)); } @Test @@ -164,7 +167,7 @@ public class RequestBodyArgumentResolverTests { Map map = new HashMap<>(); map.put("foo", "f1"); map.put("bar", "b1"); - assertEquals(map, resolve("map", Map.class, body)); + assertEquals(map, resolveValue("map", Map.class, body)); } // TODO: @Ignore @@ -174,7 +177,7 @@ public class RequestBodyArgumentResolverTests { public void list() throws Exception { String body = "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]"; assertEquals(Arrays.asList(new TestBean("f1", "b1"), new TestBean("f2", "b2")), - resolve("list", List.class, body)); + resolveValue("list", List.class, body)); } @Test @@ -182,12 +185,28 @@ public class RequestBodyArgumentResolverTests { public void array() throws Exception { String body = "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]"; assertArrayEquals(new TestBean[] {new TestBean("f1", "b1"), new TestBean("f2", "b2")}, - resolve("array", TestBean[].class, body)); + resolveValue("array", TestBean[].class, body)); + } + + @Test @SuppressWarnings("unchecked") + public void validateMonoTestBean() throws Exception { + String body = "{\"bar\":\"b1\"}"; + Mono mono = (Mono) resolveValue("monoTestBean", Mono.class, body); + TestSubscriber.subscribe(mono).assertNoValues().assertError(ServerWebInputException.class); + } + + @Test @SuppressWarnings("unchecked") + public void validateFluxTestBean() throws Exception { + String body = "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\"}]"; + Flux flux = (Flux) resolveValue("fluxTestBean", Flux.class, body); + + TestSubscriber.subscribe(flux).assertValues(new TestBean("f1", "b1")) + .assertError(ServerWebInputException.class); } @SuppressWarnings("unchecked") - private T resolve(String paramName, Class valueType, String body) { + private T resolveValue(String paramName, Class valueType, String body) { this.request.getHeaders().setContentType(MediaType.APPLICATION_JSON); this.request.writeWith(Flux.just(dataBuffer(body))); Mono result = this.resolver.resolveArgument(parameter(paramName), this.model, this.exchange); @@ -204,7 +223,7 @@ public class RequestBodyArgumentResolverTests { GenericConversionService service = new GenericConversionService(); service.addConverter(new ReactiveStreamsToCompletableFutureConverter()); service.addConverter(new ReactiveStreamsToRxJava1Converter()); - return new RequestBodyArgumentResolver(converters, service); + return new RequestBodyArgumentResolver(converters, service, new TestBeanValidator()); } @SuppressWarnings("ConfusingArgumentToVarargsMethod") @@ -230,8 +249,8 @@ public class RequestBodyArgumentResolverTests { @SuppressWarnings("unused") void handle( - @RequestBody Mono monoTestBean, - @RequestBody Flux fluxTestBean, + @Validated @RequestBody Mono monoTestBean, + @Validated @RequestBody Flux fluxTestBean, @RequestBody Single singleTestBean, @RequestBody Observable observableTestBean, @RequestBody CompletableFuture futureTestBean, @@ -297,4 +316,20 @@ public class RequestBodyArgumentResolverTests { return "TestBean[foo='" + this.foo + "\'" + ", bar='" + this.bar + "\']"; } } + + static class TestBeanValidator implements Validator { + + @Override + public boolean supports(Class clazz) { + return clazz.equals(TestBean.class); + } + + @Override + public void validate(Object target, Errors errors) { + TestBean testBean = (TestBean) target; + if (testBean.getFoo() == null) { + errors.rejectValue("foo", "nullValue"); + } + } + } } From 551b7cd60e20d97d8fff7baf96a64e0c73542fb4 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 10 Jun 2016 16:06:12 -0400 Subject: [PATCH 404/498] Add global Validator bean to WebReactiveConfiguration --- spring-web-reactive/build.gradle | 1 + .../config/WebReactiveConfiguration.java | 58 +++++++++++++++++++ .../RequestMappingHandlerAdapter.java | 23 +++++++- .../config/WebReactiveConfigurationTests.java | 8 ++- 4 files changed, 87 insertions(+), 3 deletions(-) diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index ab52dcf29a..15b2b0ede0 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -111,6 +111,7 @@ dependencies { optional "org.eclipse.jetty:jetty-servlet:${jettyVersion}" optional("org.freemarker:freemarker:2.3.23") optional("com.fasterxml:aalto-xml:1.0.0") + optional("javax.validation:validation-api:1.0.0.GA") provided "javax.servlet:javax.servlet-api:3.1.0" diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java index ee727228a6..e30c1e4fc3 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java @@ -22,6 +22,8 @@ import java.util.Map; import reactor.core.converter.DependencyUtils; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.BeanInitializationException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.annotation.Bean; @@ -48,6 +50,8 @@ import org.springframework.http.converter.reactive.CodecHttpMessageConverter; import org.springframework.http.converter.reactive.HttpMessageConverter; import org.springframework.http.converter.reactive.ResourceHttpMessageConverter; import org.springframework.util.ClassUtils; +import org.springframework.validation.Errors; +import org.springframework.validation.Validator; import org.springframework.web.reactive.accept.RequestedContentTypeResolver; import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder; import org.springframework.web.reactive.result.SimpleHandlerAdapter; @@ -185,6 +189,7 @@ public class WebReactiveConfiguration implements ApplicationContextAware { adapter.setMessageConverters(getMessageConverters()); adapter.setConversionService(mvcConversionService()); + adapter.setValidator(mvcValidator()); return adapter; } @@ -284,6 +289,46 @@ public class WebReactiveConfiguration implements ApplicationContextAware { } } + /** + * Return a global {@link Validator} instance for example for validating + * {@code @RequestBody} method arguments. + *

    Delegates to {@link #getValidator()} first. If that returns {@code null} + * checks the classpath for the presence of a JSR-303 implementations + * before creating a {@code OptionalValidatorFactoryBean}. If a JSR-303 + * implementation is not available, a "no-op" {@link Validator} is returned. + */ + @Bean + public Validator mvcValidator() { + Validator validator = getValidator(); + if (validator == null) { + if (ClassUtils.isPresent("javax.validation.Validator", getClass().getClassLoader())) { + Class clazz; + try { + String name = "org.springframework.validation.beanvalidation.OptionalValidatorFactoryBean"; + clazz = ClassUtils.forName(name, classLoader); + } + catch (ClassNotFoundException ex) { + throw new BeanInitializationException("Could not find default validator class", ex); + } + catch (LinkageError ex) { + throw new BeanInitializationException("Could not load default validator class", ex); + } + validator = (Validator) BeanUtils.instantiate(clazz); + } + else { + validator = new NoOpValidator(); + } + } + return validator; + } + + /** + * Override this method to provide a custom {@link Validator}. + */ + protected Validator getValidator() { + return null; + } + @Bean public SimpleHandlerAdapter simpleHandlerAdapter() { return new SimpleHandlerAdapter(); @@ -317,4 +362,17 @@ public class WebReactiveConfiguration implements ApplicationContextAware { protected void configureViewResolvers(ViewResolverRegistry registry) { } + + private static final class NoOpValidator implements Validator { + + @Override + public boolean supports(Class clazz) { + return false; + } + + @Override + public void validate(Object target, Errors errors) { + } + } + } 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 2cbc579437..dba37ad1f8 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 @@ -39,7 +39,7 @@ import org.springframework.http.converter.reactive.CodecHttpMessageConverter; import org.springframework.http.converter.reactive.HttpMessageConverter; import org.springframework.ui.ExtendedModelMap; import org.springframework.ui.ModelMap; -import org.springframework.util.ObjectUtils; +import org.springframework.validation.Validator; import org.springframework.web.method.HandlerMethod; import org.springframework.web.method.annotation.ExceptionHandlerMethodResolver; import org.springframework.web.reactive.HandlerAdapter; @@ -67,6 +67,8 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory private ConversionService conversionService = new DefaultFormattingConversionService(); + private Validator validator; + private ConfigurableBeanFactory beanFactory; private final Map, ExceptionHandlerMethodResolver> exceptionHandlerCache = new ConcurrentHashMap<>(64); @@ -141,6 +143,23 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory return this.conversionService; } + /** + * Configure a Validator for validation of controller method arguments such + * as {@code @RequestBody}. + * + * TODO: this may be replaced by DataBinder + */ + public void setValidator(Validator validator) { + this.validator = validator; + } + + /** + * Return the configured Validator. + */ + public Validator getValidator() { + return this.validator; + } + /** * A {@link ConfigurableBeanFactory} is expected for resolving expressions * in method argument default values. @@ -173,7 +192,7 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory resolvers.add(new RequestParamMapMethodArgumentResolver()); resolvers.add(new PathVariableMethodArgumentResolver(cs, getBeanFactory())); resolvers.add(new PathVariableMapMethodArgumentResolver()); - resolvers.add(new RequestBodyArgumentResolver(getMessageConverters(), cs)); + resolvers.add(new RequestBodyArgumentResolver(getMessageConverters(), cs, getValidator())); resolvers.add(new RequestHeaderMethodArgumentResolver(cs, getBeanFactory())); resolvers.add(new RequestHeaderMapMethodArgumentResolver()); resolvers.add(new CookieValueMethodArgumentResolver(cs, getBeanFactory())); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/WebReactiveConfigurationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/WebReactiveConfigurationTests.java index 9e39c8e588..43578f2656 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/WebReactiveConfigurationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/WebReactiveConfigurationTests.java @@ -20,7 +20,6 @@ import java.nio.ByteBuffer; import java.util.Collections; import java.util.List; import java.util.concurrent.CompletableFuture; - import javax.xml.bind.annotation.XmlRootElement; import org.junit.Before; @@ -50,6 +49,8 @@ import org.springframework.http.server.reactive.MockServerHttpRequest; import org.springframework.http.server.reactive.MockServerHttpResponse; import org.springframework.util.MimeType; import org.springframework.util.MimeTypeUtils; +import org.springframework.validation.Validator; +import org.springframework.validation.beanvalidation.OptionalValidatorFactoryBean; import org.springframework.web.reactive.accept.RequestedContentTypeResolver; import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter; import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping; @@ -148,6 +149,11 @@ public class WebReactiveConfigurationTests { name = "mvcConversionService"; ConversionService service = context.getBean(name, ConversionService.class); assertSame(service, adapter.getConversionService()); + + name = "mvcValidator"; + Validator validator = context.getBean(name, Validator.class); + assertSame(validator, adapter.getValidator()); + assertEquals(OptionalValidatorFactoryBean.class, validator.getClass()); } @Test From 5141a198d9c58968c6e0d879c0f7b9c55c57b513 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Mon, 20 Jun 2016 13:28:59 +0200 Subject: [PATCH 405/498] Avoid using deprecated methods in StringEncoder --- .../org/springframework/core/codec/support/StringEncoder.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringEncoder.java index 80e64f37a2..97094910a0 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringEncoder.java @@ -53,8 +53,8 @@ public class StringEncoder extends AbstractEncoder { DataBufferFactory bufferFactory, ResolvableType elementType, MimeType mimeType, Object... hints) { Charset charset; - if (mimeType != null && mimeType.getCharSet() != null) { - charset = mimeType.getCharSet(); + if (mimeType != null && mimeType.getCharset() != null) { + charset = mimeType.getCharset(); } else { charset = DEFAULT_CHARSET; From a0e223177927608e67d8b3e20436ecd6e68e7ec5 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Mon, 20 Jun 2016 16:23:02 +0200 Subject: [PATCH 406/498] Use specified ResolvableType in JacksonJsonEncoder This commit also fixes an issue in the HTTP client that used the wrapper type instead of the element type. As a consequence, due to type erasure, we now have to specify the type of the content in DefaultHttpRequestBuilder#contentStream(). --- .../codec/support/JacksonJsonEncoder.java | 18 +++++++-- .../reactive/DefaultHttpRequestBuilder.java | 13 ++++--- .../support/JacksonJsonEncoderTests.java | 37 ++++++++++++++++++- 3 files changed, 58 insertions(+), 10 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonEncoder.java index 084998b3f3..89bdaa9cb7 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonEncoder.java @@ -21,7 +21,10 @@ import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; +import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectWriter; +import com.fasterxml.jackson.databind.type.TypeFactory; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -66,10 +69,13 @@ public class JacksonJsonEncoder extends AbstractEncoder { public Flux encode(Publisher inputStream, DataBufferFactory bufferFactory, ResolvableType elementType, MimeType mimeType, 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) { // single object return Flux.from(inputStream) - .map(value -> serialize(value, bufferFactory)); + .map(value -> serialize(value, bufferFactory, elementType)); } else { // array @@ -81,7 +87,7 @@ public class JacksonJsonEncoder extends AbstractEncoder { Mono.just(bufferFactory.wrap(END_ARRAY_BUFFER)); Flux serializedObjects = Flux.from(inputStream) - .map(value -> serialize(value, bufferFactory)); + .map(value -> serialize(value, bufferFactory, elementType)); Flux array = Flux.zip(serializedObjects, arraySeparators) .flatMap(tuple -> Flux.just(tuple.getT1(), tuple.getT2())); @@ -92,11 +98,15 @@ public class JacksonJsonEncoder extends AbstractEncoder { } } - private DataBuffer serialize(Object value, DataBufferFactory dataBufferFactory) { + private DataBuffer serialize(Object value, DataBufferFactory dataBufferFactory, + ResolvableType type) { + TypeFactory typeFactory = this.mapper.getTypeFactory(); + JavaType javaType = typeFactory.constructType(type.getType()); + ObjectWriter writer = this.mapper.writerFor(javaType); DataBuffer buffer = dataBufferFactory.allocateBuffer(); OutputStream outputStream = buffer.asOutputStream(); try { - this.mapper.writeValue(outputStream, value); + writer.writeValue(outputStream, value); } catch (IOException e) { throw new CodecException("Error while writing the data", e); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultHttpRequestBuilder.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultHttpRequestBuilder.java index cfff9a257d..d76b53f50a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultHttpRequestBuilder.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultHttpRequestBuilder.java @@ -60,6 +60,8 @@ public class DefaultHttpRequestBuilder implements HttpRequestBuilder { protected Publisher contentPublisher; + protected ResolvableType contentType; + protected final List cookies = new ArrayList(); protected DefaultHttpRequestBuilder() { @@ -111,11 +113,13 @@ public class DefaultHttpRequestBuilder implements HttpRequestBuilder { public DefaultHttpRequestBuilder content(Object content) { this.contentPublisher = Mono.just(content); + this.contentType = ResolvableType.forInstance(content); return this; } - public DefaultHttpRequestBuilder contentStream(Publisher content) { + public DefaultHttpRequestBuilder contentStream(Publisher content, ResolvableType type) { this.contentPublisher = Flux.from(content); + this.contentType = type; return this; } @@ -139,22 +143,21 @@ public class DefaultHttpRequestBuilder implements HttpRequestBuilder { request.getHeaders().putAll(this.httpHeaders); if (this.contentPublisher != null) { - ResolvableType requestBodyType = ResolvableType.forInstance(this.contentPublisher); MediaType mediaType = request.getHeaders().getContentType(); Optional> messageEncoder = messageEncoders .stream() - .filter(e -> e.canEncode(requestBodyType, mediaType)) + .filter(e -> e.canEncode(this.contentType, mediaType)) .findFirst(); if (messageEncoder.isPresent()) { request.writeWith(messageEncoder.get() .encode(this.contentPublisher, request.bufferFactory(), - requestBodyType, mediaType)); + this.contentType, mediaType)); } else { throw new WebClientException("Can't write request body " + - "of type '" + requestBodyType.toString() + + "of type '" + this.contentType.toString() + "' for content-type '" + mediaType.toString() + "'"); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonEncoderTests.java index 7c2b7deee0..67bc96dde1 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonEncoderTests.java @@ -16,11 +16,14 @@ package org.springframework.core.codec.support; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; import org.junit.Before; import org.junit.Test; import reactor.core.publisher.Flux; import reactor.core.test.TestSubscriber; +import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.AbstractDataBufferAllocatingTestCase; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.MediaType; @@ -50,8 +53,9 @@ public class JacksonJsonEncoderTests extends AbstractDataBufferAllocatingTestCas public void write() { Flux source = Flux.just(new Pojo("foofoo", "barbar"), new Pojo("foofoofoo", "barbarbar")); + ResolvableType type = ResolvableType.forClass(Pojo.class); Flux output = - this.encoder.encode(source, this.dataBufferFactory, null, null); + this.encoder.encode(source, this.dataBufferFactory, type, null); TestSubscriber .subscribe(output) @@ -64,4 +68,35 @@ public class JacksonJsonEncoderTests extends AbstractDataBufferAllocatingTestCas stringConsumer("]")); } + @Test + public void writeWithType() { + Flux source = Flux.just(new Foo(), new Bar()); + + ResolvableType type = ResolvableType.forClass(ParentClass.class); + Flux output = + this.encoder.encode(source, this.dataBufferFactory, type, null); + + TestSubscriber + .subscribe(output) + .assertComplete() + .assertNoError() + .assertValuesWith(stringConsumer("["), + stringConsumer("{\"type\":\"foo\"}"), + stringConsumer(","), + stringConsumer("{\"type\":\"bar\"}"), + stringConsumer("]")); + } + + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type") + private static class ParentClass { + } + + @JsonTypeName("foo") + private static class Foo extends ParentClass { + } + + @JsonTypeName("bar") + private static class Bar extends ParentClass { + } + } From b5ec47d36078c6dd5518a5f69bc31625f9be4f79 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Mon, 20 Jun 2016 17:10:31 +0200 Subject: [PATCH 407/498] Polishing --- .../core/codec/support/JacksonJsonEncoderTests.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonEncoderTests.java index 67bc96dde1..95af3fe7e9 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonEncoderTests.java @@ -44,13 +44,13 @@ public class JacksonJsonEncoderTests extends AbstractDataBufferAllocatingTestCas } @Test - public void canWrite() { + public void canEncode() { assertTrue(this.encoder.canEncode(null, MediaType.APPLICATION_JSON)); assertFalse(this.encoder.canEncode(null, MediaType.APPLICATION_XML)); } @Test - public void write() { + public void encode() { Flux source = Flux.just(new Pojo("foofoo", "barbar"), new Pojo("foofoofoo", "barbarbar")); ResolvableType type = ResolvableType.forClass(Pojo.class); @@ -69,7 +69,7 @@ public class JacksonJsonEncoderTests extends AbstractDataBufferAllocatingTestCas } @Test - public void writeWithType() { + public void encodeWithType() { Flux source = Flux.just(new Foo(), new Bar()); ResolvableType type = ResolvableType.forClass(ParentClass.class); From 59b7c25003f0fe61163d074d50be558bde6b698b Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Tue, 21 Jun 2016 16:46:24 +0200 Subject: [PATCH 408/498] Use ResolvableType instead of raw Class in JacksonJsonDecoder --- .../core/codec/support/JacksonJsonDecoder.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonDecoder.java index ccf8f5be3b..93815729db 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonDecoder.java @@ -19,8 +19,10 @@ package org.springframework.core.codec.support; import java.io.IOException; import java.nio.charset.StandardCharsets; +import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectReader; +import com.fasterxml.jackson.databind.type.TypeFactory; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -29,6 +31,7 @@ import org.springframework.core.codec.CodecException; import org.springframework.core.codec.Decoder; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.support.DataBufferUtils; +import org.springframework.util.Assert; import org.springframework.util.MimeType; @@ -64,7 +67,11 @@ public class JacksonJsonDecoder extends AbstractDecoder { public Flux decode(Publisher inputStream, ResolvableType elementType, MimeType mimeType, Object... hints) { - ObjectReader reader = this.mapper.readerFor(elementType.getRawClass()); + Assert.notNull(inputStream, "'inputStream' must not be null"); + Assert.notNull(elementType, "'elementType' must not be null"); + TypeFactory typeFactory = this.mapper.getTypeFactory(); + JavaType javaType = typeFactory.constructType(elementType.getType()); + ObjectReader reader = this.mapper.readerFor(javaType); Flux stream = Flux.from(inputStream); if (this.preProcessor != null) { From 9aa6f5caacac316c74e374550a387f222e7e1921 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 21 Jun 2016 17:27:52 -0400 Subject: [PATCH 409/498] Add support for ResponseEntity result handling --- .../config/WebReactiveConfiguration.java | 16 +- ...AbstractMessageConverterResultHandler.java | 116 ++++++++++++++ .../annotation/ResponseBodyResultHandler.java | 84 ++-------- .../ResponseEntityResultHandler.java | 118 ++++++++++++++ .../config/WebReactiveConfigurationTests.java | 36 ++++- .../RequestMappingIntegrationTests.java | 12 ++ .../ResponseBodyResultHandlerTests.java | 8 +- .../ResponseEntityResultHandlerTests.java | 150 ++++++++++++++++++ 8 files changed, 455 insertions(+), 85 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageConverterResultHandler.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java index e30c1e4fc3..740dbcc18a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java @@ -60,6 +60,7 @@ import org.springframework.web.reactive.result.method.HandlerMethodArgumentResol import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter; import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.reactive.result.method.annotation.ResponseBodyResultHandler; +import org.springframework.web.reactive.result.method.annotation.ResponseEntityResultHandler; import org.springframework.web.reactive.result.view.ViewResolutionResultHandler; import org.springframework.web.reactive.result.view.ViewResolver; @@ -335,13 +336,20 @@ public class WebReactiveConfiguration implements ApplicationContextAware { } @Bean - public ResponseBodyResultHandler responseBodyResultHandler() { - return new ResponseBodyResultHandler(getMessageConverters(), mvcConversionService()); + public SimpleResultHandler simpleResultHandler() { + return new SimpleResultHandler(mvcConversionService()); } @Bean - public SimpleResultHandler simpleResultHandler() { - return new SimpleResultHandler(mvcConversionService()); + public ResponseEntityResultHandler responseEntityResultHandler() { + return new ResponseEntityResultHandler(getMessageConverters(), mvcConversionService(), + mvcContentTypeResolver()); + } + + @Bean + public ResponseBodyResultHandler responseBodyResultHandler() { + return new ResponseBodyResultHandler(getMessageConverters(), mvcConversionService(), + mvcContentTypeResolver()); } @Bean diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageConverterResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageConverterResultHandler.java new file mode 100644 index 0000000000..5201bf7f75 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageConverterResultHandler.java @@ -0,0 +1,116 @@ +/* + * 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.web.reactive.result.method.annotation; + +import java.util.List; +import java.util.stream.Collectors; + +import org.reactivestreams.Publisher; +import reactor.core.publisher.Mono; + +import org.springframework.core.ResolvableType; +import org.springframework.core.convert.ConversionService; +import org.springframework.http.MediaType; +import org.springframework.http.converter.reactive.HttpMessageConverter; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.util.Assert; +import org.springframework.web.reactive.accept.RequestedContentTypeResolver; +import org.springframework.web.reactive.result.ContentNegotiatingResultHandlerSupport; +import org.springframework.web.server.NotAcceptableStatusException; +import org.springframework.web.server.ServerWebExchange; + +/** + * Abstract base class for result handlers that handle return values by writing + * to the response with {@link HttpMessageConverter}. + * + * @author Rossen Stoyanchev + */ +public abstract class AbstractMessageConverterResultHandler extends ContentNegotiatingResultHandlerSupport { + + private final List> messageConverters; + + + /** + * Constructor with message converters, a {@code ConversionService}, and a + * {@code RequestedContentTypeResolver}. + * + * @param converters converters for writing the response body with + * @param conversionService for converting other reactive types (e.g. + * rx.Observable, rx.Single, etc.) to Flux or Mono + * @param contentTypeResolver for resolving the requested content type + */ + protected AbstractMessageConverterResultHandler(List> converters, + ConversionService conversionService, RequestedContentTypeResolver contentTypeResolver) { + + super(conversionService, contentTypeResolver); + Assert.notEmpty(converters, "At least one message converter is required."); + this.messageConverters = converters; + } + + /** + * Return the configured message converters. + */ + public List> getMessageConverters() { + return this.messageConverters; + } + + + @SuppressWarnings("unchecked") + protected Mono writeBody(ServerWebExchange exchange, Object body, ResolvableType bodyType) { + + Publisher publisher; + ResolvableType elementType; + + if (getConversionService().canConvert(bodyType.getRawClass(), Publisher.class)) { + if (body != null) { + publisher = getConversionService().convert(body, Publisher.class); + } + else { + publisher = Mono.empty(); + } + elementType = bodyType.getGeneric(0); + if (Void.class.equals(elementType.getRawClass())) { + return Mono.from((Publisher) publisher); + } + } + else { + publisher = Mono.justOrEmpty(body); + elementType = bodyType; + } + + List producibleTypes = getProducibleMediaTypes(elementType); + MediaType bestMediaType = selectMediaType(exchange, producibleTypes); + + if (bestMediaType != null) { + for (HttpMessageConverter converter : getMessageConverters()) { + if (converter.canWrite(elementType, bestMediaType)) { + ServerHttpResponse response = exchange.getResponse(); + return converter.write((Publisher) publisher, elementType, bestMediaType, response); + } + } + } + + return Mono.error(new NotAcceptableStatusException(producibleTypes)); + } + + private List getProducibleMediaTypes(ResolvableType elementType) { + return getMessageConverters().stream() + .filter(converter -> converter.canWrite(elementType, null)) + .flatMap(converter -> converter.getWritableMediaTypes().stream()) + .collect(Collectors.toList()); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java index 6a04b69091..1fbafc0b41 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java @@ -17,29 +17,20 @@ package org.springframework.web.reactive.result.method.annotation; import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; -import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; import org.springframework.core.MethodParameter; -import org.springframework.core.Ordered; import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.convert.ConversionService; -import org.springframework.http.MediaType; import org.springframework.http.converter.reactive.HttpMessageConverter; -import org.springframework.http.server.reactive.ServerHttpResponse; -import org.springframework.util.Assert; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.HandlerResultHandler; import org.springframework.web.reactive.accept.HeaderContentTypeResolver; import org.springframework.web.reactive.accept.RequestedContentTypeResolver; -import org.springframework.web.reactive.result.ContentNegotiatingResultHandlerSupport; -import org.springframework.web.server.NotAcceptableStatusException; import org.springframework.web.server.ServerWebExchange; @@ -48,21 +39,18 @@ import org.springframework.web.server.ServerWebExchange; * with {@code @ResponseBody} writing to the body of the request or response with * an {@link HttpMessageConverter}. * - *

    By default the order for the result handler is set to 0. It is generally - * safe and expected it will be ordered ahead of other result handlers since it - * only gets involved based on the presence of an {@code @ResponseBody} - * annotation. + *

    By default the order for the result handler is set to 100. It detects the + * presence of an {@code @ResponseBody} annotation and should be ordered after + * result handlers that look for a specific return type such as + * {@code ResponseEntity}. * * @author Rossen Stoyanchev * @author Stephane Maldini * @author Sebastien Deleuze * @author Arjen Poutsma */ -public class ResponseBodyResultHandler extends ContentNegotiatingResultHandlerSupport - implements HandlerResultHandler, Ordered { - - private final List> messageConverters; - +public class ResponseBodyResultHandler extends AbstractMessageConverterResultHandler + implements HandlerResultHandler { /** * Constructor with message converters and a {@code ConversionService} only @@ -90,20 +78,11 @@ public class ResponseBodyResultHandler extends ContentNegotiatingResultHandlerSu public ResponseBodyResultHandler(List> converters, ConversionService conversionService, RequestedContentTypeResolver contentTypeResolver) { - super(conversionService, contentTypeResolver); - Assert.notEmpty(converters, "At least one message converter is required."); - this.messageConverters = converters; - setOrder(0); + super(converters, conversionService, contentTypeResolver); + setOrder(100); } - /** - * Return the configured message converters. - */ - public List> getMessageConverters() { - return this.messageConverters; - } - @Override public boolean supports(HandlerResult result) { Object handler = result.getHandler(); @@ -117,51 +96,10 @@ public class ResponseBodyResultHandler extends ContentNegotiatingResultHandlerSu } @Override - @SuppressWarnings("unchecked") public Mono handleResult(ServerWebExchange exchange, HandlerResult result) { - - Publisher publisher; - ResolvableType elementType; - ResolvableType returnType = result.getReturnValueType(); - - if (getConversionService().canConvert(returnType.getRawClass(), Publisher.class)) { - Optional optionalValue = result.getReturnValue(); - if (optionalValue.isPresent()) { - publisher = getConversionService().convert(optionalValue.get(), Publisher.class); - } - else { - publisher = Mono.empty(); - } - elementType = returnType.getGeneric(0); - if (Void.class.equals(elementType.getRawClass())) { - return Mono.from((Publisher)publisher); - } - } - else { - publisher = Mono.justOrEmpty(result.getReturnValue()); - elementType = returnType; - } - - List producibleTypes = getProducibleMediaTypes(elementType); - MediaType bestMediaType = selectMediaType(exchange, producibleTypes); - - if (bestMediaType != null) { - for (HttpMessageConverter converter : this.messageConverters) { - if (converter.canWrite(elementType, bestMediaType)) { - ServerHttpResponse response = exchange.getResponse(); - return converter.write((Publisher) publisher, elementType, bestMediaType, response); - } - } - } - - return Mono.error(new NotAcceptableStatusException(producibleTypes)); - } - - private List getProducibleMediaTypes(ResolvableType type) { - return this.messageConverters.stream() - .filter(converter -> converter.canWrite(type, null)) - .flatMap(converter -> converter.getWritableMediaTypes().stream()) - .collect(Collectors.toList()); + Object body = result.getReturnValue().orElse(null); + ResolvableType bodyType = result.getReturnValueType(); + return writeBody(exchange, body, bodyType); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java new file mode 100644 index 0000000000..08a7114d90 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java @@ -0,0 +1,118 @@ +/* + * 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.web.reactive.result.method.annotation; + +import java.util.List; +import java.util.Optional; + +import reactor.core.publisher.Mono; + +import org.springframework.core.ResolvableType; +import org.springframework.core.convert.ConversionService; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.reactive.HttpMessageConverter; +import org.springframework.util.Assert; +import org.springframework.web.reactive.HandlerResult; +import org.springframework.web.reactive.HandlerResultHandler; +import org.springframework.web.reactive.accept.HeaderContentTypeResolver; +import org.springframework.web.reactive.accept.RequestedContentTypeResolver; +import org.springframework.web.server.ServerWebExchange; + +/** + * Handles {@link HttpEntity} and {@link ResponseEntity} return values. + * + *

    By default the order for this result handler is set to 0. It is generally + * safe to place it early in the order as it looks for a concrete return type. + * + * @author Rossen Stoyanchev + */ +public class ResponseEntityResultHandler extends AbstractMessageConverterResultHandler + implements HandlerResultHandler { + + /** + * Constructor with message converters and a {@code ConversionService} only + * and creating a {@link HeaderContentTypeResolver}, i.e. using Accept header + * to determine the requested content type. + * + * @param converters converters for writing the response body with + * @param conversionService for converting to Flux and Mono from other reactive types + */ + public ResponseEntityResultHandler(List> converters, + ConversionService conversionService) { + + this(converters, conversionService, new HeaderContentTypeResolver()); + } + + /** + * Constructor with message converters, a {@code ConversionService}, and a + * {@code RequestedContentTypeResolver}. + * + * @param converters converters for writing the response body with + * @param conversionService for converting other reactive types (e.g. + * rx.Observable, rx.Single, etc.) to Flux or Mono + * @param contentTypeResolver for resolving the requested content type + */ + public ResponseEntityResultHandler(List> converters, + ConversionService conversionService, RequestedContentTypeResolver contentTypeResolver) { + + super(converters, conversionService, contentTypeResolver); + setOrder(0); + } + + + @Override + public boolean supports(HandlerResult result) { + ResolvableType returnType = result.getReturnValueType(); + return (HttpEntity.class.isAssignableFrom(returnType.getRawClass()) && + !RequestEntity.class.isAssignableFrom(returnType.getRawClass())); + } + + + @Override + public Mono handleResult(ServerWebExchange exchange, HandlerResult result) { + + Object body = null; + + Optional optional = result.getReturnValue(); + if (optional.isPresent()) { + Assert.isInstanceOf(HttpEntity.class, optional.get()); + HttpEntity httpEntity = (HttpEntity) optional.get(); + + if (httpEntity instanceof ResponseEntity) { + ResponseEntity responseEntity = (ResponseEntity) httpEntity; + exchange.getResponse().setStatusCode(responseEntity.getStatusCode()); + } + + HttpHeaders entityHeaders = httpEntity.getHeaders(); + HttpHeaders responseHeaders = exchange.getResponse().getHeaders(); + + if (!entityHeaders.isEmpty()) { + entityHeaders.entrySet().stream() + .filter(entry -> responseHeaders.containsKey(entry.getKey())) + .forEach(entry -> responseHeaders.put(entry.getKey(), entry.getValue())); + } + + body = httpEntity.getBody(); + } + + ResolvableType bodyType = result.getReturnValueType().getGeneric(0); + return writeBody(exchange, body, bodyType); + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/WebReactiveConfigurationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/WebReactiveConfigurationTests.java index 43578f2656..0f4e8e32c7 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/WebReactiveConfigurationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/WebReactiveConfigurationTests.java @@ -55,6 +55,7 @@ import org.springframework.web.reactive.accept.RequestedContentTypeResolver; import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter; import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.reactive.result.method.annotation.ResponseBodyResultHandler; +import org.springframework.web.reactive.result.method.annotation.ResponseEntityResultHandler; import org.springframework.web.reactive.result.view.HttpMessageConverterView; import org.springframework.web.reactive.result.view.View; import org.springframework.web.reactive.result.view.ViewResolutionResultHandler; @@ -183,13 +184,12 @@ public class WebReactiveConfigurationTests { service.canConvert(Observable.class, Flux.class); } - @Test - public void responseBodyResultHandler() throws Exception { + public void responseEntityResultHandler() throws Exception { ApplicationContext context = loadConfig(WebReactiveConfiguration.class); - String name = "responseBodyResultHandler"; - ResponseBodyResultHandler handler = context.getBean(name, ResponseBodyResultHandler.class); + String name = "responseEntityResultHandler"; + ResponseEntityResultHandler handler = context.getBean(name, ResponseEntityResultHandler.class); assertNotNull(handler); assertEquals(0, handler.getOrder()); @@ -202,6 +202,34 @@ public class WebReactiveConfigurationTests { assertHasConverter(converters, Resource.class, MediaType.IMAGE_PNG); assertHasConverter(converters, TestBean.class, MediaType.APPLICATION_XML); assertHasConverter(converters, TestBean.class, MediaType.APPLICATION_JSON); + + name = "mvcContentTypeResolver"; + RequestedContentTypeResolver resolver = context.getBean(name, RequestedContentTypeResolver.class); + assertSame(resolver, handler.getContentTypeResolver()); + } + + @Test + public void responseBodyResultHandler() throws Exception { + ApplicationContext context = loadConfig(WebReactiveConfiguration.class); + + String name = "responseBodyResultHandler"; + ResponseBodyResultHandler handler = context.getBean(name, ResponseBodyResultHandler.class); + assertNotNull(handler); + + assertEquals(100, handler.getOrder()); + + List> converters = handler.getMessageConverters(); + assertEquals(5, converters.size()); + + assertHasConverter(converters, ByteBuffer.class, MediaType.APPLICATION_OCTET_STREAM); + assertHasConverter(converters, String.class, MediaType.TEXT_PLAIN); + assertHasConverter(converters, Resource.class, MediaType.IMAGE_PNG); + assertHasConverter(converters, TestBean.class, MediaType.APPLICATION_XML); + assertHasConverter(converters, TestBean.class, MediaType.APPLICATION_JSON); + + name = "mvcContentTypeResolver"; + RequestedContentTypeResolver resolver = context.getBean(name, RequestedContentTypeResolver.class); + assertSame(resolver, handler.getContentTypeResolver()); } @Test diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java index 3ad58a92b0..5e5338a33f 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java @@ -172,6 +172,12 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati serializeAsPojo("http://localhost:" + port + "/completable-future"); } + @Test + @Ignore // Issue #119 + public void serializeAsMonoResponseEntity() throws Exception { + serializeAsPojo("http://localhost:" + port + "/monoResponseEntity"); + } + @Test public void serializeAsMono() throws Exception { serializeAsPojo("http://localhost:" + port + "/mono"); @@ -450,6 +456,12 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati return Observable.just(ByteBuffer.wrap("Hello!".getBytes())); } + @RequestMapping("/monoResponseEntity") + public ResponseEntity> monoResponseEntity() { + Mono body = Mono.just(new Person("Robert")); + return ResponseEntity.ok(body); + } + @RequestMapping("/mono") public Mono monoResponseBody() { return Mono.just(new Person("Robert")); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java index de06a67488..0d0745ede9 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java @@ -56,6 +56,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; +import static org.springframework.http.MediaType.APPLICATION_JSON_UTF8; /** @@ -87,20 +88,19 @@ public class ResponseBodyResultHandlerTests { @Test public void defaultOrder() throws Exception { ResponseBodyResultHandler handler = createHandler(new StringEncoder()); - assertEquals(0, handler.getOrder()); + assertEquals(100, handler.getOrder()); } @Test public void usesContentTypeResolver() throws Exception { - MediaType contentType = MediaType.APPLICATION_JSON_UTF8; - RequestedContentTypeResolver resolver = new FixedContentTypeResolver(contentType); + RequestedContentTypeResolver resolver = new FixedContentTypeResolver(APPLICATION_JSON_UTF8); HandlerResultHandler handler = createHandler(resolver, new StringEncoder(), new JacksonJsonEncoder()); ServerWebExchange exchange = createExchange("/foo"); HandlerResult result = new HandlerResult(new Object(), "fooValue", ResolvableType.forClass(String.class)); handler.handleResult(exchange, result).block(); - assertEquals(contentType, exchange.getResponse().getHeaders().getContentType()); + assertEquals(APPLICATION_JSON_UTF8, exchange.getResponse().getHeaders().getContentType()); } @Test diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java new file mode 100644 index 0000000000..3cdd9ad26c --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java @@ -0,0 +1,150 @@ +/* + * 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.web.reactive.result.method.annotation; + +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.Test; +import reactor.core.test.TestSubscriber; + +import org.springframework.core.ResolvableType; +import org.springframework.core.codec.Encoder; +import org.springframework.core.codec.support.JacksonJsonEncoder; +import org.springframework.core.codec.support.StringEncoder; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.core.io.buffer.support.DataBufferTestUtils; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.reactive.CodecHttpMessageConverter; +import org.springframework.http.converter.reactive.HttpMessageConverter; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.ui.ExtendedModelMap; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.reactive.HandlerResult; +import org.springframework.web.reactive.HandlerResultHandler; +import org.springframework.web.reactive.accept.FixedContentTypeResolver; +import org.springframework.web.reactive.accept.HeaderContentTypeResolver; +import org.springframework.web.reactive.accept.RequestedContentTypeResolver; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.WebSessionManager; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.springframework.http.MediaType.APPLICATION_JSON_UTF8; + +/** + * Unit tests for {@link ResponseEntityResultHandler}. + * @author Rossen Stoyanchev + */ +public class ResponseEntityResultHandlerTests { + + private MockServerHttpResponse response = new MockServerHttpResponse(); + + + @Test + public void supports() throws NoSuchMethodException { + ResponseEntityResultHandler handler = createHandler(new StringEncoder()); + TestController controller = new TestController(); + + HandlerMethod hm = new HandlerMethod(controller, TestController.class.getMethod("responseString")); + ResolvableType type = ResolvableType.forMethodParameter(hm.getReturnType()); + assertTrue(handler.supports(new HandlerResult(hm, null, type, new ExtendedModelMap()))); + + hm = new HandlerMethod(controller, TestController.class.getMethod("responseVoid")); + type = ResolvableType.forMethodParameter(hm.getReturnType()); + assertTrue(handler.supports(new HandlerResult(hm, null, type, new ExtendedModelMap()))); + + hm = new HandlerMethod(controller, TestController.class.getMethod("string")); + type = ResolvableType.forMethodParameter(hm.getReturnType()); + assertFalse(handler.supports(new HandlerResult(hm, null, type, new ExtendedModelMap()))); + } + + @Test + public void defaultOrder() throws Exception { + ResponseEntityResultHandler handler = createHandler(new StringEncoder()); + assertEquals(0, handler.getOrder()); + } + + @Test + public void jsonResponseBody() throws Exception { + RequestedContentTypeResolver resolver = new FixedContentTypeResolver(APPLICATION_JSON_UTF8); + HandlerResultHandler handler = createHandler(resolver, new StringEncoder(), new JacksonJsonEncoder()); + + TestController controller = new TestController(); + HandlerMethod hm = new HandlerMethod(controller, controller.getClass().getMethod("responseString")); + ResolvableType type = ResolvableType.forMethodParameter(hm.getReturnType()); + HandlerResult result = new HandlerResult(hm, ResponseEntity.ok("fooValue"), type); + + ServerWebExchange exchange = createExchange("/foo"); + handler.handleResult(exchange, result).block(); + + assertEquals(HttpStatus.OK, this.response.getStatus()); + assertEquals(APPLICATION_JSON_UTF8, this.response.getHeaders().getContentType()); + TestSubscriber.subscribe(this.response.getBody()) + .assertValuesWith(buf -> assertEquals("\"fooValue\"", + DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); + } + + + private ResponseEntityResultHandler createHandler(Encoder... encoders) { + return createHandler(new HeaderContentTypeResolver(), encoders); + } + + private ResponseEntityResultHandler createHandler(RequestedContentTypeResolver resolver, + Encoder... encoders) { + + List> converters = Arrays.stream(encoders) + .map(encoder -> new CodecHttpMessageConverter<>(encoder, null)) + .collect(Collectors.toList()); + return new ResponseEntityResultHandler(converters, new DefaultConversionService(), resolver); + } + + private ServerWebExchange createExchange(String path) throws URISyntaxException { + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI(path)); + WebSessionManager sessionManager = mock(WebSessionManager.class); + return new DefaultServerWebExchange(request, this.response, sessionManager); + } + + + @SuppressWarnings("unused") + private static class TestController { + + public ResponseEntity responseString() { + return null; + } + + public ResponseEntity responseVoid() { + return null; + } + + public String string() { + return null; + } + } + +} From aeb35787d7214f5f9d0636cce19105dec2a53193 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Thu, 16 Jun 2016 12:03:41 +0200 Subject: [PATCH 410/498] Add flushing support This commit add flushing support thanks to the FlushingDataBuffer wrapper that allows to identify the elements that should trigger a flush. --- .../core/io/buffer/FlushingDataBuffer.java | 120 ++++++++++++++++++ .../http/ReactiveHttpOutputMessage.java | 3 + .../reactive/ReactorServerHttpResponse.java | 8 +- .../reactive/RxNettyServerHttpResponse.java | 26 ++-- .../reactive/ServletHttpHandlerAdapter.java | 17 ++- .../reactive/UndertowHttpHandlerAdapter.java | 28 ++-- .../reactive/FlushingIntegrationTests.java | 82 ++++++++++++ 7 files changed, 263 insertions(+), 21 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/core/io/buffer/FlushingDataBuffer.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/http/server/reactive/FlushingIntegrationTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/FlushingDataBuffer.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/FlushingDataBuffer.java new file mode 100644 index 0000000000..f11f201959 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/FlushingDataBuffer.java @@ -0,0 +1,120 @@ +/* + * 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.io.buffer; + +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.util.function.IntPredicate; + +import org.springframework.util.Assert; + +/** + * {@link DataBuffer} wrapper that indicates the file or the socket writing this buffer + * should be flushed. + * + * @author Sebastien Deleuze + */ +public class FlushingDataBuffer implements DataBuffer { + + private final DataBuffer buffer; + + public FlushingDataBuffer(DataBuffer buffer) { + Assert.notNull(buffer); + this.buffer = buffer; + } + + @Override + public DataBufferFactory factory() { + return this.buffer.factory(); + } + + @Override + public int indexOf(IntPredicate predicate, int fromIndex) { + return this.buffer.indexOf(predicate, fromIndex); + } + + @Override + public int lastIndexOf(IntPredicate predicate, int fromIndex) { + return this.buffer.lastIndexOf(predicate, fromIndex); + } + + @Override + public int readableByteCount() { + return this.buffer.readableByteCount(); + } + + @Override + public byte read() { + return this.buffer.read(); + } + + @Override + public DataBuffer read(byte[] destination) { + return this.buffer.read(destination); + } + + @Override + public DataBuffer read(byte[] destination, int offset, int length) { + return this.buffer.read(destination, offset, length); + } + + @Override + public DataBuffer write(byte b) { + return this.buffer.write(b); + } + + @Override + public DataBuffer write(byte[] source) { + return this.buffer.write(source); + } + + @Override + public DataBuffer write(byte[] source, int offset, int length) { + return this.write(source, offset, length); + } + + @Override + public DataBuffer write(DataBuffer... buffers) { + return this.buffer.write(buffers); + } + + @Override + public DataBuffer write(ByteBuffer... buffers) { + return this.buffer.write(buffers); + } + + @Override + public DataBuffer slice(int index, int length) { + return this.buffer.slice(index, length); + } + + @Override + public ByteBuffer asByteBuffer() { + return this.buffer.asByteBuffer(); + } + + @Override + public InputStream asInputStream() { + return this.buffer.asInputStream(); + } + + @Override + public OutputStream asOutputStream() { + return this.buffer.asOutputStream(); + } +} diff --git a/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java b/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java index 7f139545e6..cda8fd58ed 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java @@ -23,6 +23,7 @@ import reactor.core.publisher.Mono; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.FlushingDataBuffer; /** * A "reactive" HTTP output message that accepts output as a {@link Publisher}. @@ -47,6 +48,8 @@ public interface ReactiveHttpOutputMessage extends HttpMessage { * flushed before depending on the configuration, the HTTP engine and the amount of * data sent). * + *

    Each {@link FlushingDataBuffer} element will trigger a flush. + * * @param body the body content publisher * @return a publisher that indicates completion or error. */ diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java index 1f293f6fac..fd2467ec09 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java @@ -30,6 +30,7 @@ import reactor.io.netty.http.HttpChannel; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.FlushingDataBuffer; import org.springframework.core.io.buffer.NettyDataBuffer; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseCookie; @@ -66,7 +67,12 @@ public class ReactorServerHttpResponse extends AbstractServerHttpResponse @Override protected Mono writeWithInternal(Publisher publisher) { - return this.channel.send(Flux.from(publisher).map(this::toByteBuf)); + return Flux.from(publisher) + .window() + .concatMap(w -> this.channel.send(w + .takeUntil(db -> db instanceof FlushingDataBuffer) + .map(this::toByteBuf))) + .then(); } @Override diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java index 936b4c97be..61aac8cc6f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java @@ -17,6 +17,7 @@ package org.springframework.http.server.reactive; import io.netty.buffer.ByteBuf; +import io.netty.buffer.CompositeByteBuf; import io.netty.buffer.Unpooled; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.cookie.Cookie; @@ -28,6 +29,7 @@ import reactor.core.publisher.Mono; import rx.Observable; import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.FlushingDataBuffer; import org.springframework.core.io.buffer.NettyDataBuffer; import org.springframework.core.io.buffer.NettyDataBufferFactory; import org.springframework.http.HttpStatus; @@ -63,20 +65,14 @@ public class RxNettyServerHttpResponse extends AbstractServerHttpResponse { } @Override - protected Mono writeWithInternal(Publisher publisher) { - Observable content = - RxJava1ObservableConverter.from(publisher).map(this::toByteBuf); - Observable completion = this.response.write(content); - return RxJava1ObservableConverter.from(completion).then(); + protected Mono writeWithInternal(Publisher body) { + Observable content = RxJava1ObservableConverter.from(body).map(this::toByteBuf); + return RxJava1ObservableConverter.from(this.response.write(content, bb -> bb instanceof FlushingByteBuf)).then(); } private ByteBuf toByteBuf(DataBuffer buffer) { - if (buffer instanceof NettyDataBuffer) { - return ((NettyDataBuffer) buffer).getNativeBuffer(); - } - else { - return Unpooled.wrappedBuffer(buffer.asByteBuffer()); - } + ByteBuf byteBuf = (buffer instanceof NettyDataBuffer ? ((NettyDataBuffer) buffer).getNativeBuffer() : Unpooled.wrappedBuffer(buffer.asByteBuffer())); + return (buffer instanceof FlushingDataBuffer ? new FlushingByteBuf(byteBuf) : byteBuf); } @Override @@ -104,6 +100,14 @@ public class RxNettyServerHttpResponse extends AbstractServerHttpResponse { } } + private class FlushingByteBuf extends CompositeByteBuf { + + public FlushingByteBuf(ByteBuf byteBuf) { + super(byteBuf.alloc(), byteBuf.isDirect(), 1); + this.addComponent(true, byteBuf); + } + } + /* While the underlying implementation of {@link ZeroCopyHttpOutputMessage} seems to work; it does bypass {@link #applyBeforeCommit} and more importantly it doesn't change diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java index 0c463bbda2..3b06bd2776 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java @@ -39,6 +39,7 @@ import reactor.core.util.BackpressureUtils; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DefaultDataBufferFactory; +import org.springframework.core.io.buffer.FlushingDataBuffer; import org.springframework.core.io.buffer.support.DataBufferUtils; import org.springframework.http.HttpStatus; import org.springframework.util.Assert; @@ -330,6 +331,9 @@ public class ServletHttpHandlerAdapter extends HttpServlet { logger.trace("written: " + written + " total: " + total); if (written == total) { + if (dataBuffer instanceof FlushingDataBuffer) { + flush(output); + } releaseBuffer(); if (!completed) { subscription.request(1); @@ -361,6 +365,17 @@ public class ServletHttpHandlerAdapter extends HttpServlet { return bytesWritten; } + private void flush(ServletOutputStream output) { + if (output.isReady()) { + logger.trace("Flushing"); + try { + output.flush(); + } + catch (IOException ignored) { + } + } + } + private void releaseBuffer() { DataBufferUtils.release(dataBuffer); dataBuffer = null; @@ -373,4 +388,4 @@ public class ServletHttpHandlerAdapter extends HttpServlet { } } -} +} \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java index f0ed1d5a99..bc2c89ab1a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java @@ -35,6 +35,8 @@ import reactor.core.util.BackpressureUtils; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.FlushingDataBuffer; +import org.springframework.core.io.buffer.support.DataBufferUtils; import org.springframework.util.Assert; /** @@ -201,6 +203,8 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle private volatile ByteBuffer byteBuffer; + private volatile DataBuffer dataBuffer; + private volatile boolean completed = false; private Subscription subscription; @@ -232,6 +236,7 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle logger.trace("onNext. buffer: " + dataBuffer); this.byteBuffer = dataBuffer.asByteBuffer(); + this.dataBuffer = dataBuffer; } @Override @@ -266,8 +271,6 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle } } catch (IOException ignored) { - logger.error(ignored, ignored); - } } @@ -283,6 +286,9 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle logger.trace("written: " + written + " total: " + total); if (written == total) { + if (dataBuffer instanceof FlushingDataBuffer) { + flush(channel); + } releaseBuffer(); if (!completed) { subscription.request(1); @@ -302,11 +308,6 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle } - private void releaseBuffer() { - byteBuffer = null; - - } - private int writeByteBuffer(StreamSinkChannel channel) throws IOException { int written; int totalWritten = 0; @@ -318,8 +319,19 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle return totalWritten; } + private void flush(StreamSinkChannel channel) throws IOException { + logger.trace("Flushing"); + channel.flush(); + } + + private void releaseBuffer() { + DataBufferUtils.release(dataBuffer); + dataBuffer = null; + byteBuffer = null; + } + } } -} +} \ No newline at end of file diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/FlushingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/FlushingIntegrationTests.java new file mode 100644 index 0000000000..344a49283c --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/FlushingIntegrationTests.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.http.server.reactive; + +import org.junit.Before; +import org.junit.Test; +import static org.springframework.web.client.reactive.HttpRequestBuilders.get; +import static org.springframework.web.client.reactive.WebResponseExtractors.bodyStream; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.test.TestSubscriber; + +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.FlushingDataBuffer; +import org.springframework.http.client.reactive.ReactorHttpClientRequestFactory; +import org.springframework.web.client.reactive.WebClient; + +/** + * @author Sebastien Deleuze + */ +public class FlushingIntegrationTests extends AbstractHttpHandlerIntegrationTests { + + private WebClient webClient; + + @Before + public void setup() throws Exception { + super.setup(); + this.webClient = new WebClient(new ReactorHttpClientRequestFactory()); + } + + @Test + public void testFlushing() throws Exception { + Mono result = this.webClient + .perform(get("http://localhost:" + port)) + .extract(bodyStream(String.class)) + .take(2) + .reduce((s1, s2) -> s1 + s2); + + TestSubscriber + .subscribe(result) + .await() + .assertValues("data0data1"); + } + + + @Override + protected HttpHandler createHttpHandler() { + return new FlushingHandler(); + } + + private static class FlushingHandler implements HttpHandler { + + @Override + public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { + Flux responseBody = Flux + .interval(50) + .take(2) + .concatWith(Flux.never()) + .map(l -> { + byte[] data = ("data" + l).getBytes(); + DataBuffer buffer = response.bufferFactory().allocateBuffer(data.length); + buffer.write(data); + return new FlushingDataBuffer(buffer); + }); + return response.writeWith(responseBody); + } + } +} From 90048122315213038ddf2ec1b1cb005a1d6cba26 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Thu, 16 Jun 2016 12:03:53 +0200 Subject: [PATCH 411/498] Add Server-Sent Events support Flux is Spring Web Reactive equivalent to Spring MVC SseEmitter type. It allows to send Server-Sent Events in a reactive way. Sending Flux or Flux is equivalent to sending Flux with the data property set to the String or Pojo value. For example: @RestController public class SseController { @RequestMapping("/sse/string") Flux string() { return Flux.interval(Duration.ofSeconds(1)).map(l -> "foo " + l); } @RequestMapping("/sse/person") Flux person() { return Flux.interval(Duration.ofSeconds(1)).map(l -> new Person(Long.toString(l), "foo", "bar")); } @RequestMapping("/sse-raw") Flux sse() { return Flux.interval(Duration.ofSeconds(1)).map(l -> { SseEvent event = new SseEvent(); event.setId(Long.toString(l)); event.setData("foo\nbar"); event.setComment("bar\nbaz"); return event; }); } } --- .../core/codec/support/SseEventEncoder.java | 127 +++++++++ .../reactive/SseHttpMessageConverter.java | 75 +++++ .../web/reactive/sse/SseEvent.java | 164 +++++++++++ .../codec/support/SseEventEncoderTests.java | 135 +++++++++ .../annotation/SseIntegrationTests.java | 266 ++++++++++++++++++ 5 files changed, 767 insertions(+) create mode 100644 spring-web-reactive/src/main/java/org/springframework/core/codec/support/SseEventEncoder.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/SseHttpMessageConverter.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/sse/SseEvent.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/core/codec/support/SseEventEncoderTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/SseEventEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/SseEventEncoder.java new file mode 100644 index 0000000000..8d95cc8cda --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/SseEventEncoder.java @@ -0,0 +1,127 @@ +/* + * 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.support; + +import java.util.List; +import java.util.Optional; + +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.core.ResolvableType; +import org.springframework.core.codec.CodecException; +import org.springframework.core.codec.Encoder; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.FlushingDataBuffer; +import org.springframework.util.Assert; +import org.springframework.util.MimeType; +import org.springframework.web.reactive.sse.SseEvent; + +/** + * An encoder for {@link SseEvent}s that also supports any other kind of {@link Object} + * (in that case, the object will be the data of the {@link SseEvent}). + * @author Sebastien Deleuze + */ +public class SseEventEncoder extends AbstractEncoder { + + private final Encoder stringEncoder; + + private final List> dataEncoders; + + + public SseEventEncoder(Encoder stringEncoder, List> dataEncoders) { + super(new MimeType("text", "event-stream")); + Assert.notNull(stringEncoder, "'stringEncoder' must not be null"); + Assert.notNull(dataEncoders, "'dataEncoders' must not be null"); + this.stringEncoder = stringEncoder; + this.dataEncoders = dataEncoders; + } + + @Override + public Flux encode(Publisher inputStream, DataBufferFactory bufferFactory, ResolvableType type, MimeType sseMimeType, Object... hints) { + + return Flux.from(inputStream).flatMap(input -> { + SseEvent event = (SseEvent.class.equals(type.getRawClass()) ? (SseEvent)input : new SseEvent(input)); + + StringBuilder sb = new StringBuilder(); + + if (event.getId() != null) { + sb.append("id:"); + sb.append(event.getId()); + sb.append("\n"); + } + + if (event.getName() != null) { + sb.append("event:"); + sb.append(event.getName()); + sb.append("\n"); + } + + if (event.getReconnectTime() != null) { + sb.append("retry:"); + sb.append(event.getReconnectTime().toString()); + sb.append("\n"); + } + + if (event.getComment() != null) { + sb.append(":"); + sb.append(event.getComment().replaceAll("\\n", "\n:")); + sb.append("\n"); + } + + Object data = event.getData(); + Flux dataBuffer = Flux.empty(); + MimeType stringMimeType = this.stringEncoder.getEncodableMimeTypes().get(0); + MimeType mimeType = (event.getMimeType() == null ? + (data instanceof String ? stringMimeType : new MimeType("*")) : event.getMimeType()); + if (data != null) { + sb.append("data:"); + if (data instanceof String && mimeType.isCompatibleWith(stringMimeType)) { + sb.append(((String)data).replaceAll("\\n", "\ndata:")).append("\n"); + } + else { + Optional> encoder = dataEncoders + .stream() + .filter(e -> e.canEncode(ResolvableType.forClass(data.getClass()), mimeType)) + .findFirst(); + + if (encoder.isPresent()) { + dataBuffer = ((Encoder)encoder.get()) + .encode(Mono.just(data), bufferFactory, ResolvableType.forClass(data.getClass()), mimeType) + .concatWith(encodeString("\n", bufferFactory, stringMimeType)); + } + else { + throw new CodecException("No suitable encoder found!"); + } + } + } + + return Flux + .concat(encodeString(sb.toString(), bufferFactory, stringMimeType), dataBuffer) + .reduce((buf1, buf2) -> buf1.write(buf2)) + .concatWith(encodeString("\n", bufferFactory, stringMimeType).map(b -> new FlushingDataBuffer(b))); + }); + + } + + private Flux encodeString(String str, DataBufferFactory bufferFactory, MimeType mimeType) { + return stringEncoder.encode(Mono.just(str), bufferFactory, ResolvableType.forClass(String.class), mimeType); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/SseHttpMessageConverter.java b/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/SseHttpMessageConverter.java new file mode 100644 index 0000000000..7f1100a996 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/SseHttpMessageConverter.java @@ -0,0 +1,75 @@ +/* + * 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.http.converter.reactive; + + +import java.util.Arrays; +import java.util.List; + +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.core.ResolvableType; +import org.springframework.core.codec.Encoder; +import org.springframework.core.codec.support.JacksonJsonEncoder; +import org.springframework.core.codec.support.SseEventEncoder; +import org.springframework.core.codec.support.StringEncoder; +import org.springframework.http.MediaType; +import org.springframework.http.ReactiveHttpOutputMessage; +import org.springframework.web.reactive.sse.SseEvent; + +/** + * Implementation of {@link HttpMessageConverter} that can stream Server-Sent Events + * response. + * + * It allows to write {@code Flux}, which is Spring Web Reactive equivalent + * to Spring MVC {@code SseEmitter}. + * + * Sending {@code Flux} or {@code Flux} is equivalent to sending + * {@code Flux} with the {@code data} property set to the {@code String} or + * {@code Pojo} value. + * + * @author Sebastien Deleuze + * @see SseEvent + * @see Server-Sent Events W3C recommandation + */ +public class SseHttpMessageConverter extends CodecHttpMessageConverter { + + /** + * Default constructor that creates a new instance configured with {@link StringEncoder} + * and {@link JacksonJsonEncoder} encoders. + */ + public SseHttpMessageConverter() { + this(new StringEncoder(), Arrays.asList(new JacksonJsonEncoder())); + } + + public SseHttpMessageConverter(Encoder stringEncoder, List> dataEncoders) { + // 1 SseEvent element = 1 DataBuffer element so flush after each element + super(new SseEventEncoder(stringEncoder, dataEncoders), null); + } + + @Override + public Mono write(Publisher inputStream, ResolvableType type, + MediaType contentType, ReactiveHttpOutputMessage outputMessage) { + + outputMessage.getHeaders().add("Content-Type", "text/event-stream"); + // Keep the SSE connection open even for cold stream in order to avoid unexpected Browser reconnection + return super.write(Flux.from(inputStream).concatWith(Flux.never()), type, contentType, outputMessage); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/sse/SseEvent.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/sse/SseEvent.java new file mode 100644 index 0000000000..6f0eaf6828 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/sse/SseEvent.java @@ -0,0 +1,164 @@ +/* + * 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.web.reactive.sse; + +import org.springframework.http.converter.reactive.SseHttpMessageConverter; +import org.springframework.util.MimeType; + +/** + * Represent a Server-Sent Event. + * + *

    {@code Flux} is Spring Web Reactive equivalent to Spring MVC + * {@code SseEmitter} type. It allows to send Server-Sent Events in a reactive way. + * + * @author Sebastien Deleuze + * @see SseHttpMessageConverter + * @see Server-Sent Events W3C recommandation + */ +public class SseEvent { + + private String id; + + private String name; + + private Object data; + + private MimeType mimeType; + + private Long reconnectTime; + + private String comment; + + /** + * Create an empty instance. + */ + public SseEvent() { + } + + /** + * Create an instance with the provided {@code data}. + */ + public SseEvent(Object data) { + this.data = data; + } + + /** + * Create an instance with the provided {@code data} and {@code mediaType}. + */ + public SseEvent(Object data, MimeType mimeType) { + this.data = data; + this.mimeType = mimeType; + } + + /** + * Set the {@code id} SSE field + */ + public void setId(String id) { + this.id = id; + } + + /** + * @see #setId(String) + */ + public String getId() { + return id; + } + + /** + * Set the {@code event} SSE field + */ + public void setName(String name) { + this.name = name; + } + + /** + * @see #setName(String) + */ + public String getName() { + return name; + } + + /** + * Set {@code data} SSE field. If a multiline {@code String} is provided, it will be + * turned into multiple {@code data} field lines by as + * defined in Server-Sent Events W3C recommandation. + * + * If no {@code mediaType} is defined, default {@link SseHttpMessageConverter} will: + * - Turn single line {@code String} to a single {@code data} field + * - Turn multiline line {@code String} to multiple {@code data} fields + * - Serialize other {@code Object} as JSON + * + * @see #setMimeType(MimeType) + */ + public void setData(Object data) { + this.data = data; + } + + /** + * @see #setData(Object) + */ + public Object getData() { + return data; + } + + /** + * Set the {@link MimeType} used to serialize the {@code data}. + * {@link SseHttpMessageConverter} should be configured with the relevant encoder to be + * able to serialize it. + */ + public void setMimeType(MimeType mimeType) { + this.mimeType = mimeType; + } + + /** + * @see #setMimeType(MimeType) + */ + public MimeType getMimeType() { + return mimeType; + } + + /** + * Set the {@code retry} SSE field + */ + public void setReconnectTime(Long reconnectTime) { + this.reconnectTime = reconnectTime; + } + + /** + * @see #setReconnectTime(Long) + */ + public Long getReconnectTime() { + return reconnectTime; + } + + /** + * Set SSE comment. If a multiline comment is provided, it will be turned into multiple + * SSE comment lines by {@link SseHttpMessageConverter} as defined in Server-Sent Events W3C + * recommandation. + */ + public void setComment(String comment) { + this.comment = comment; + } + + /** + * @see #setComment(String) + */ + public String getComment() { + return comment; + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/SseEventEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/SseEventEncoderTests.java new file mode 100644 index 0000000000..b4d86264e7 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/SseEventEncoderTests.java @@ -0,0 +1,135 @@ +/* + * 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.support; + +import java.util.Arrays; + +import org.junit.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.test.TestSubscriber; + +import org.springframework.core.ResolvableType; +import org.springframework.core.io.buffer.AbstractDataBufferAllocatingTestCase; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.util.MimeType; +import org.springframework.web.reactive.sse.SseEvent; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + + +/** + * @author Sebastien Deleuze + */ +public class SseEventEncoderTests extends AbstractDataBufferAllocatingTestCase { + + @Test + public void nullMimeType() { + SseEventEncoder encoder = new SseEventEncoder(new StringEncoder(), Arrays.asList(new JacksonJsonEncoder())); + assertTrue(encoder.canEncode(ResolvableType.forClass(Object.class), null)); + } + + @Test + public void unsupportedMimeType() { + SseEventEncoder encoder = new SseEventEncoder(new StringEncoder(), Arrays.asList(new JacksonJsonEncoder())); + assertFalse(encoder.canEncode(ResolvableType.forClass(Object.class), new MimeType("foo", "bar"))); + } + + @Test + public void supportedMimeType() { + SseEventEncoder encoder = new SseEventEncoder(new StringEncoder(), Arrays.asList(new JacksonJsonEncoder())); + assertTrue(encoder.canEncode(ResolvableType.forClass(Object.class), new MimeType("text", "event-stream"))); + } + + @Test + public void encodeServerSentEvent() { + SseEventEncoder encoder = new SseEventEncoder(new StringEncoder(), Arrays.asList(new JacksonJsonEncoder())); + SseEvent event = new SseEvent(); + event.setId("c42"); + event.setName("foo"); + event.setComment("bla\nbla bla\nbla bla bla"); + event.setReconnectTime(123L); + Mono source = Mono.just(event); + Flux output = encoder.encode(source, this.dataBufferFactory, + ResolvableType.forClass(SseEvent.class), new MimeType("text", "event-stream")); + TestSubscriber + .subscribe(output) + .assertNoError() + .assertValuesWith( + stringConsumer( + "id:c42\n" + + "event:foo\n" + + "retry:123\n" + + ":bla\n:bla bla\n:bla bla bla\n"), + stringConsumer("\n") + ); + } + + @Test + public void encodeString() { + SseEventEncoder encoder = new SseEventEncoder(new StringEncoder(), Arrays.asList(new JacksonJsonEncoder())); + Flux source = Flux.just("foo", "bar"); + Flux output = encoder.encode(source, this.dataBufferFactory, + ResolvableType.forClass(String.class), new MimeType("text", "event-stream")); + TestSubscriber + .subscribe(output) + .assertNoError() + .assertValuesWith( + stringConsumer("data:foo\n"), + stringConsumer("\n"), + stringConsumer("data:bar\n"), + stringConsumer("\n") + ); + } + + @Test + public void encodeMultilineString() { + SseEventEncoder encoder = new SseEventEncoder(new StringEncoder(), Arrays.asList(new JacksonJsonEncoder())); + Flux source = Flux.just("foo\nbar", "foo\nbaz"); + Flux output = encoder.encode(source, this.dataBufferFactory, + ResolvableType.forClass(String.class), new MimeType("text", "event-stream")); + TestSubscriber + .subscribe(output) + .assertNoError() + .assertValuesWith( + stringConsumer("data:foo\ndata:bar\n"), + stringConsumer("\n"), + stringConsumer("data:foo\ndata:baz\n"), + stringConsumer("\n") + ); + } + + + @Test + public void encodePojo() { + SseEventEncoder encoder = new SseEventEncoder(new StringEncoder(), Arrays.asList(new JacksonJsonEncoder())); + Flux source = Flux.just(new Pojo("foofoo", "barbar"), new Pojo("foofoofoo", "barbarbar")); + Flux output = encoder.encode(source, this.dataBufferFactory, + ResolvableType.forClass(Pojo.class), new MimeType("text", "event-stream")); + TestSubscriber + .subscribe(output) + .assertNoError() + .assertValuesWith( + stringConsumer("data:{\"foo\":\"foofoo\",\"bar\":\"barbar\"}\n"), + stringConsumer("\n"), + stringConsumer("data:{\"foo\":\"foofoofoo\",\"bar\":\"barbarbar\"}\n"), + stringConsumer("\n") + ); + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java new file mode 100644 index 0000000000..23397d340f --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java @@ -0,0 +1,266 @@ +/* + * 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.web.reactive.result.method.annotation; + +import java.time.Duration; +import java.util.Arrays; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runners.Parameterized; +import static org.springframework.web.client.reactive.HttpRequestBuilders.get; +import static org.springframework.web.client.reactive.WebResponseExtractors.bodyStream; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.test.TestSubscriber; + +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.codec.support.ByteBufferDecoder; +import org.springframework.core.codec.support.JacksonJsonDecoder; +import org.springframework.core.codec.support.JsonObjectDecoder; +import org.springframework.core.codec.support.StringDecoder; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter; +import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; +import org.springframework.http.MediaType; +import org.springframework.http.client.reactive.ReactorHttpClientRequestFactory; +import org.springframework.http.converter.reactive.HttpMessageConverter; +import org.springframework.http.converter.reactive.SseHttpMessageConverter; +import org.springframework.http.server.reactive.AbstractHttpHandlerIntegrationTests; +import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.http.server.reactive.boot.JettyHttpServer; +import org.springframework.http.server.reactive.boot.ReactorHttpServer; +import org.springframework.http.server.reactive.boot.RxNettyHttpServer; +import org.springframework.http.server.reactive.boot.TomcatHttpServer; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.reactive.WebClient; +import org.springframework.web.reactive.DispatcherHandler; +import org.springframework.web.reactive.result.SimpleResultHandler; +import org.springframework.web.reactive.sse.SseEvent; +import org.springframework.web.server.adapter.WebHttpHandlerBuilder; + +/** + * @author Sebastien Deleuze + */ +public class SseIntegrationTests extends AbstractHttpHandlerIntegrationTests { + + // TODO Fix Undertow support and remove this method + @Parameterized.Parameters(name = "server [{0}]") + public static Object[][] arguments() { + return new Object[][] { + {new JettyHttpServer()}, + {new RxNettyHttpServer()}, + {new ReactorHttpServer()}, + {new TomcatHttpServer()}, + }; + } + + private AnnotationConfigApplicationContext wac; + + private WebClient webClient; + + @Before + public void setup() throws Exception { + super.setup(); + this.webClient = new WebClient(new ReactorHttpClientRequestFactory()); + this.webClient.setMessageDecoders(Arrays.asList( + new ByteBufferDecoder(), + new StringDecoder(false), + new JacksonJsonDecoder(new JsonObjectDecoder()))); + } + + @Override + protected HttpHandler createHttpHandler() { + this.wac = new AnnotationConfigApplicationContext(); + this.wac.register(TestConfiguration.class); + this.wac.refresh(); + + DispatcherHandler webHandler = new DispatcherHandler(); + webHandler.setApplicationContext(this.wac); + + return WebHttpHandlerBuilder.webHandler(webHandler).build(); + } + + @Test + public void sseAsString() throws Exception { + Mono result = this.webClient + .perform(get("http://localhost:" + port + "/sse/string") + .accept(new MediaType("text", "event-stream"))) + .extract(bodyStream(String.class)) + .take(Duration.ofMillis(500)) + .reduce((s1, s2) -> s1 + s2); + + TestSubscriber + .subscribe(result) + .await() + .assertValues("data:foo 0\n\ndata:foo 1\n\n"); + } + + @Test + public void sseAsPojo() throws Exception { + Mono result = this.webClient + .perform(get("http://localhost:" + port + "/sse/person") + .accept(new MediaType("text", "event-stream"))) + .extract(bodyStream(String.class)) + .take(Duration.ofMillis(500)) + .reduce((s1, s2) -> s1 + s2); + + TestSubscriber + .subscribe(result) + .await() + .assertValues("data:{\"name\":\"foo 0\"}\n\ndata:{\"name\":\"foo 1\"}\n\n"); + } + + @Test + public void sseAsEvent() throws Exception { + Mono result = this.webClient + .perform(get("http://localhost:" + port + "/sse/event") + .accept(new MediaType("text", "event-stream"))) + .extract(bodyStream(String.class)) + .take(Duration.ofMillis(500)) + .reduce((s1, s2) -> s1 + s2); + + TestSubscriber + .subscribe(result) + .await() + .assertValues("id:0\n:bar\ndata:foo\n\nid:1\n:bar\ndata:foo\n\n"); + } + + @RestController + @SuppressWarnings("unused") + static class SseController { + + @RequestMapping("/sse/string") + Flux string() { + return Flux.interval(Duration.ofMillis(100)).map(l -> "foo " + l).take(2); + } + + @RequestMapping("/sse/person") + Flux person() { + return Flux.interval(Duration.ofMillis(100)).map(l -> new Person("foo " + l)).take(2); + } + + @RequestMapping("/sse/event") + Flux sse() { + return Flux.interval(Duration.ofMillis(100)).map(l -> { + SseEvent event = new SseEvent(); + event.setId(Long.toString(l)); + event.setData("foo"); + event.setComment("bar"); + return event; + }).take(2); + } + + } + + @Configuration + @SuppressWarnings("unused") + static class TestConfiguration { + + private DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory(); + + @Bean + public SseController sseController() { + return new SseController(); + } + + @Bean + public RequestMappingHandlerMapping handlerMapping() { + return new RequestMappingHandlerMapping(); + } + + @Bean + public RequestMappingHandlerAdapter handlerAdapter() { + RequestMappingHandlerAdapter handlerAdapter = new RequestMappingHandlerAdapter(); + handlerAdapter.setConversionService(conversionService()); + return handlerAdapter; + } + + @Bean + public ConversionService conversionService() { + GenericConversionService service = new GenericConversionService(); + service.addConverter(new ReactiveStreamsToCompletableFutureConverter()); + service.addConverter(new ReactiveStreamsToRxJava1Converter()); + return service; + } + + @Bean + public ResponseBodyResultHandler responseBodyResultHandler() { + List> converters = Arrays.asList(new SseHttpMessageConverter()); + return new ResponseBodyResultHandler(converters, conversionService()); + } + + @Bean + public SimpleResultHandler simpleHandlerResultHandler() { + return new SimpleResultHandler(conversionService()); + } + + } + + private static class Person { + + private String name; + + @SuppressWarnings("unused") + public Person() { + } + + public Person(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Person person = (Person) o; + return !(this.name != null ? !this.name.equals(person.name) : person.name != null); + } + + @Override + public int hashCode() { + return this.name != null ? this.name.hashCode() : 0; + } + + @Override + public String toString() { + return "Person{" + + "name='" + name + '\'' + + '}'; + } + } + +} From 81496624a9cb718d330a3e72f01006951735188c Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Thu, 16 Jun 2016 08:52:50 +0200 Subject: [PATCH 412/498] Fixed Servlet flush Servlet flush will now occur on the next possible write if it cannot be done immediately. --- .../server/reactive/ServletHttpHandlerAdapter.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java index 3b06bd2776..6f8bcf8ce5 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java @@ -247,8 +247,11 @@ public class ServletHttpHandlerAdapter extends HttpServlet { private volatile boolean completed = false; + private volatile boolean flushOnNext = false; + private Subscription subscription; + public ResponseBodySubscriber(ServletAsyncContextSynchronizer synchronizer, int bufferSize) { this.synchronizer = synchronizer; @@ -321,6 +324,12 @@ public class ServletHttpHandlerAdapter extends HttpServlet { ServletOutputStream output = synchronizer.getResponse().getOutputStream(); boolean ready = output.isReady(); + + if (flushOnNext) { + flush(output); + ready = output.isReady(); + } + logger.trace("ready: " + ready + " buffer: " + dataBuffer); if (ready) { @@ -370,9 +379,12 @@ public class ServletHttpHandlerAdapter extends HttpServlet { logger.trace("Flushing"); try { output.flush(); + flushOnNext = false; } catch (IOException ignored) { } + } else { + flushOnNext = true; } } From 3c80c19c1905f29e489de59db96fe7e5afcda551 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Wed, 22 Jun 2016 14:22:18 +0200 Subject: [PATCH 413/498] Take in account Rossen and Arjen feedbacks --- .../core/io/buffer/FlushingDataBuffer.java | 7 ++- .../codec}/SseEventEncoder.java | 41 ++++++++------- .../reactive/SseHttpMessageConverter.java | 16 ++---- .../codec/support/SseEventEncoderTests.java | 23 ++++---- .../reactive/FlushingIntegrationTests.java | 2 + .../annotation/SseIntegrationTests.java | 52 ++++--------------- 6 files changed, 57 insertions(+), 84 deletions(-) rename spring-web-reactive/src/main/java/org/springframework/{core/codec/support => http/codec}/SseEventEncoder.java (70%) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/FlushingDataBuffer.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/FlushingDataBuffer.java index f11f201959..c1a16ac1b3 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/FlushingDataBuffer.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/FlushingDataBuffer.java @@ -33,6 +33,10 @@ public class FlushingDataBuffer implements DataBuffer { private final DataBuffer buffer; + public FlushingDataBuffer() { + this.buffer = new DefaultDataBufferFactory().allocateBuffer(0); + } + public FlushingDataBuffer(DataBuffer buffer) { Assert.notNull(buffer); this.buffer = buffer; @@ -85,7 +89,7 @@ public class FlushingDataBuffer implements DataBuffer { @Override public DataBuffer write(byte[] source, int offset, int length) { - return this.write(source, offset, length); + return this.buffer.write(source, offset, length); } @Override @@ -117,4 +121,5 @@ public class FlushingDataBuffer implements DataBuffer { public OutputStream asOutputStream() { return this.buffer.asOutputStream(); } + } diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/SseEventEncoder.java b/spring-web-reactive/src/main/java/org/springframework/http/codec/SseEventEncoder.java similarity index 70% rename from spring-web-reactive/src/main/java/org/springframework/core/codec/support/SseEventEncoder.java rename to spring-web-reactive/src/main/java/org/springframework/http/codec/SseEventEncoder.java index 8d95cc8cda..ffeae04ab8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/SseEventEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/codec/SseEventEncoder.java @@ -14,8 +14,9 @@ * limitations under the License. */ -package org.springframework.core.codec.support; +package org.springframework.http.codec; +import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Optional; @@ -26,6 +27,7 @@ import reactor.core.publisher.Mono; import org.springframework.core.ResolvableType; import org.springframework.core.codec.CodecException; import org.springframework.core.codec.Encoder; +import org.springframework.core.codec.support.AbstractEncoder; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.FlushingDataBuffer; @@ -40,24 +42,22 @@ import org.springframework.web.reactive.sse.SseEvent; */ public class SseEventEncoder extends AbstractEncoder { - private final Encoder stringEncoder; - private final List> dataEncoders; - public SseEventEncoder(Encoder stringEncoder, List> dataEncoders) { + public SseEventEncoder(List> dataEncoders) { super(new MimeType("text", "event-stream")); - Assert.notNull(stringEncoder, "'stringEncoder' must not be null"); Assert.notNull(dataEncoders, "'dataEncoders' must not be null"); - this.stringEncoder = stringEncoder; this.dataEncoders = dataEncoders; } @Override - public Flux encode(Publisher inputStream, DataBufferFactory bufferFactory, ResolvableType type, MimeType sseMimeType, Object... hints) { + public Flux encode(Publisher inputStream, DataBufferFactory bufferFactory, + ResolvableType type, MimeType sseMimeType, Object... hints) { return Flux.from(inputStream).flatMap(input -> { - SseEvent event = (SseEvent.class.equals(type.getRawClass()) ? (SseEvent)input : new SseEvent(input)); + SseEvent event = (SseEvent.class.equals(type.getRawClass()) ? + (SseEvent)input : new SseEvent(input)); StringBuilder sb = new StringBuilder(); @@ -87,12 +87,11 @@ public class SseEventEncoder extends AbstractEncoder { Object data = event.getData(); Flux dataBuffer = Flux.empty(); - MimeType stringMimeType = this.stringEncoder.getEncodableMimeTypes().get(0); MimeType mimeType = (event.getMimeType() == null ? - (data instanceof String ? stringMimeType : new MimeType("*")) : event.getMimeType()); + new MimeType("*") : event.getMimeType()); if (data != null) { sb.append("data:"); - if (data instanceof String && mimeType.isCompatibleWith(stringMimeType)) { + if (data instanceof String) { sb.append(((String)data).replaceAll("\\n", "\ndata:")).append("\n"); } else { @@ -103,8 +102,9 @@ public class SseEventEncoder extends AbstractEncoder { if (encoder.isPresent()) { dataBuffer = ((Encoder)encoder.get()) - .encode(Mono.just(data), bufferFactory, ResolvableType.forClass(data.getClass()), mimeType) - .concatWith(encodeString("\n", bufferFactory, stringMimeType)); + .encode(Mono.just(data), bufferFactory, + ResolvableType.forClass(data.getClass()), mimeType) + .concatWith(encodeString("\n", bufferFactory)); } else { throw new CodecException("No suitable encoder found!"); @@ -112,16 +112,19 @@ public class SseEventEncoder extends AbstractEncoder { } } - return Flux - .concat(encodeString(sb.toString(), bufferFactory, stringMimeType), dataBuffer) - .reduce((buf1, buf2) -> buf1.write(buf2)) - .concatWith(encodeString("\n", bufferFactory, stringMimeType).map(b -> new FlushingDataBuffer(b))); + return Flux.concat( + encodeString(sb.toString(), bufferFactory), + dataBuffer, + encodeString("\n", bufferFactory).map(b -> new FlushingDataBuffer(b)) + ); }); } - private Flux encodeString(String str, DataBufferFactory bufferFactory, MimeType mimeType) { - return stringEncoder.encode(Mono.just(str), bufferFactory, ResolvableType.forClass(String.class), mimeType); + private Mono encodeString(String str, DataBufferFactory bufferFactory) { + byte[] bytes = str.getBytes(StandardCharsets.UTF_8); + DataBuffer buffer = bufferFactory.allocateBuffer(bytes.length).write(bytes); + return Mono.just(buffer); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/SseHttpMessageConverter.java b/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/SseHttpMessageConverter.java index 7f1100a996..705bed7d75 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/SseHttpMessageConverter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/SseHttpMessageConverter.java @@ -26,9 +26,7 @@ import reactor.core.publisher.Mono; import org.springframework.core.ResolvableType; import org.springframework.core.codec.Encoder; -import org.springframework.core.codec.support.JacksonJsonEncoder; -import org.springframework.core.codec.support.SseEventEncoder; -import org.springframework.core.codec.support.StringEncoder; +import org.springframework.http.codec.SseEventEncoder; import org.springframework.http.MediaType; import org.springframework.http.ReactiveHttpOutputMessage; import org.springframework.web.reactive.sse.SseEvent; @@ -51,16 +49,10 @@ import org.springframework.web.reactive.sse.SseEvent; public class SseHttpMessageConverter extends CodecHttpMessageConverter { /** - * Default constructor that creates a new instance configured with {@link StringEncoder} - * and {@link JacksonJsonEncoder} encoders. + * Constructor that creates a new instance configured with the specified data encoders. */ - public SseHttpMessageConverter() { - this(new StringEncoder(), Arrays.asList(new JacksonJsonEncoder())); - } - - public SseHttpMessageConverter(Encoder stringEncoder, List> dataEncoders) { - // 1 SseEvent element = 1 DataBuffer element so flush after each element - super(new SseEventEncoder(stringEncoder, dataEncoders), null); + public SseHttpMessageConverter(List> dataEncoders) { + super(new SseEventEncoder(dataEncoders), null); } @Override diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/SseEventEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/SseEventEncoderTests.java index b4d86264e7..3d6807829c 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/SseEventEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/SseEventEncoderTests.java @@ -26,6 +26,7 @@ import reactor.core.test.TestSubscriber; import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.AbstractDataBufferAllocatingTestCase; import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.http.codec.SseEventEncoder; import org.springframework.util.MimeType; import org.springframework.web.reactive.sse.SseEvent; @@ -40,25 +41,25 @@ public class SseEventEncoderTests extends AbstractDataBufferAllocatingTestCase { @Test public void nullMimeType() { - SseEventEncoder encoder = new SseEventEncoder(new StringEncoder(), Arrays.asList(new JacksonJsonEncoder())); + SseEventEncoder encoder = new SseEventEncoder(Arrays.asList(new JacksonJsonEncoder())); assertTrue(encoder.canEncode(ResolvableType.forClass(Object.class), null)); } @Test public void unsupportedMimeType() { - SseEventEncoder encoder = new SseEventEncoder(new StringEncoder(), Arrays.asList(new JacksonJsonEncoder())); + SseEventEncoder encoder = new SseEventEncoder(Arrays.asList(new JacksonJsonEncoder())); assertFalse(encoder.canEncode(ResolvableType.forClass(Object.class), new MimeType("foo", "bar"))); } @Test public void supportedMimeType() { - SseEventEncoder encoder = new SseEventEncoder(new StringEncoder(), Arrays.asList(new JacksonJsonEncoder())); + SseEventEncoder encoder = new SseEventEncoder(Arrays.asList(new JacksonJsonEncoder())); assertTrue(encoder.canEncode(ResolvableType.forClass(Object.class), new MimeType("text", "event-stream"))); } @Test public void encodeServerSentEvent() { - SseEventEncoder encoder = new SseEventEncoder(new StringEncoder(), Arrays.asList(new JacksonJsonEncoder())); + SseEventEncoder encoder = new SseEventEncoder(Arrays.asList(new JacksonJsonEncoder())); SseEvent event = new SseEvent(); event.setId("c42"); event.setName("foo"); @@ -82,7 +83,7 @@ public class SseEventEncoderTests extends AbstractDataBufferAllocatingTestCase { @Test public void encodeString() { - SseEventEncoder encoder = new SseEventEncoder(new StringEncoder(), Arrays.asList(new JacksonJsonEncoder())); + SseEventEncoder encoder = new SseEventEncoder(Arrays.asList(new JacksonJsonEncoder())); Flux source = Flux.just("foo", "bar"); Flux output = encoder.encode(source, this.dataBufferFactory, ResolvableType.forClass(String.class), new MimeType("text", "event-stream")); @@ -99,7 +100,7 @@ public class SseEventEncoderTests extends AbstractDataBufferAllocatingTestCase { @Test public void encodeMultilineString() { - SseEventEncoder encoder = new SseEventEncoder(new StringEncoder(), Arrays.asList(new JacksonJsonEncoder())); + SseEventEncoder encoder = new SseEventEncoder(Arrays.asList(new JacksonJsonEncoder())); Flux source = Flux.just("foo\nbar", "foo\nbaz"); Flux output = encoder.encode(source, this.dataBufferFactory, ResolvableType.forClass(String.class), new MimeType("text", "event-stream")); @@ -117,7 +118,7 @@ public class SseEventEncoderTests extends AbstractDataBufferAllocatingTestCase { @Test public void encodePojo() { - SseEventEncoder encoder = new SseEventEncoder(new StringEncoder(), Arrays.asList(new JacksonJsonEncoder())); + SseEventEncoder encoder = new SseEventEncoder(Arrays.asList(new JacksonJsonEncoder())); Flux source = Flux.just(new Pojo("foofoo", "barbar"), new Pojo("foofoofoo", "barbarbar")); Flux output = encoder.encode(source, this.dataBufferFactory, ResolvableType.forClass(Pojo.class), new MimeType("text", "event-stream")); @@ -125,9 +126,13 @@ public class SseEventEncoderTests extends AbstractDataBufferAllocatingTestCase { .subscribe(output) .assertNoError() .assertValuesWith( - stringConsumer("data:{\"foo\":\"foofoo\",\"bar\":\"barbar\"}\n"), + stringConsumer("data:"), + stringConsumer("{\"foo\":\"foofoo\",\"bar\":\"barbar\"}"), + stringConsumer("\n"), + stringConsumer("\n"), + stringConsumer("data:"), + stringConsumer("{\"foo\":\"foofoofoo\",\"bar\":\"barbarbar\"}"), stringConsumer("\n"), - stringConsumer("data:{\"foo\":\"foofoofoo\",\"bar\":\"barbarbar\"}\n"), stringConsumer("\n") ); } diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/FlushingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/FlushingIntegrationTests.java index 344a49283c..8dfc741a5e 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/FlushingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/FlushingIntegrationTests.java @@ -62,6 +62,8 @@ public class FlushingIntegrationTests extends AbstractHttpHandlerIntegrationTest return new FlushingHandler(); } + // Handler that never completes designed to test if flushing is perform correctly when + // a FlushingDataBuffer is written private static class FlushingHandler implements HttpHandler { @Override diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java index 23397d340f..2c98530d8c 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java @@ -34,14 +34,9 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.codec.support.ByteBufferDecoder; import org.springframework.core.codec.support.JacksonJsonDecoder; +import org.springframework.core.codec.support.JacksonJsonEncoder; import org.springframework.core.codec.support.JsonObjectDecoder; import org.springframework.core.codec.support.StringDecoder; -import org.springframework.core.convert.ConversionService; -import org.springframework.core.convert.support.GenericConversionService; -import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter; -import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter; -import org.springframework.core.io.buffer.DataBufferFactory; -import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.MediaType; import org.springframework.http.client.reactive.ReactorHttpClientRequestFactory; import org.springframework.http.converter.reactive.HttpMessageConverter; @@ -56,7 +51,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.reactive.WebClient; import org.springframework.web.reactive.DispatcherHandler; -import org.springframework.web.reactive.result.SimpleResultHandler; +import org.springframework.web.reactive.config.WebReactiveConfiguration; import org.springframework.web.reactive.sse.SseEvent; import org.springframework.web.server.adapter.WebHttpHandlerBuilder; @@ -108,7 +103,7 @@ public class SseIntegrationTests extends AbstractHttpHandlerIntegrationTests { .perform(get("http://localhost:" + port + "/sse/string") .accept(new MediaType("text", "event-stream"))) .extract(bodyStream(String.class)) - .take(Duration.ofMillis(500)) + .take(Duration.ofMillis(1000)) .reduce((s1, s2) -> s1 + s2); TestSubscriber @@ -123,7 +118,7 @@ public class SseIntegrationTests extends AbstractHttpHandlerIntegrationTests { .perform(get("http://localhost:" + port + "/sse/person") .accept(new MediaType("text", "event-stream"))) .extract(bodyStream(String.class)) - .take(Duration.ofMillis(500)) + .take(Duration.ofMillis(1000)) .reduce((s1, s2) -> s1 + s2); TestSubscriber @@ -138,7 +133,7 @@ public class SseIntegrationTests extends AbstractHttpHandlerIntegrationTests { .perform(get("http://localhost:" + port + "/sse/event") .accept(new MediaType("text", "event-stream"))) .extract(bodyStream(String.class)) - .take(Duration.ofMillis(500)) + .take(Duration.ofMillis(1000)) .reduce((s1, s2) -> s1 + s2); TestSubscriber @@ -176,46 +171,17 @@ public class SseIntegrationTests extends AbstractHttpHandlerIntegrationTests { @Configuration @SuppressWarnings("unused") - static class TestConfiguration { - - private DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory(); + static class TestConfiguration extends WebReactiveConfiguration { @Bean public SseController sseController() { return new SseController(); } - @Bean - public RequestMappingHandlerMapping handlerMapping() { - return new RequestMappingHandlerMapping(); + @Override + protected void extendMessageConverters(List> converters) { + converters.add(new SseHttpMessageConverter(Arrays.asList(new JacksonJsonEncoder()))); } - - @Bean - public RequestMappingHandlerAdapter handlerAdapter() { - RequestMappingHandlerAdapter handlerAdapter = new RequestMappingHandlerAdapter(); - handlerAdapter.setConversionService(conversionService()); - return handlerAdapter; - } - - @Bean - public ConversionService conversionService() { - GenericConversionService service = new GenericConversionService(); - service.addConverter(new ReactiveStreamsToCompletableFutureConverter()); - service.addConverter(new ReactiveStreamsToRxJava1Converter()); - return service; - } - - @Bean - public ResponseBodyResultHandler responseBodyResultHandler() { - List> converters = Arrays.asList(new SseHttpMessageConverter()); - return new ResponseBodyResultHandler(converters, conversionService()); - } - - @Bean - public SimpleResultHandler simpleHandlerResultHandler() { - return new SimpleResultHandler(conversionService()); - } - } private static class Person { From 6b3d5f1bc57a2285abce83121f516250422e03b6 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Thu, 23 Jun 2016 17:02:44 +0200 Subject: [PATCH 414/498] Turn FlushingDataBuffer to an empty DataBuffer --- .../core/io/buffer/FlushingDataBuffer.java | 17 +++++++------- .../http/codec/SseEventEncoder.java | 3 ++- .../codec/support/SseEventEncoderTests.java | 22 +++++++++++-------- .../reactive/FlushingIntegrationTests.java | 9 ++++---- 4 files changed, 28 insertions(+), 23 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/FlushingDataBuffer.java b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/FlushingDataBuffer.java index c1a16ac1b3..3d77bd7f48 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/FlushingDataBuffer.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/io/buffer/FlushingDataBuffer.java @@ -21,26 +21,25 @@ import java.io.OutputStream; import java.nio.ByteBuffer; import java.util.function.IntPredicate; -import org.springframework.util.Assert; - /** - * {@link DataBuffer} wrapper that indicates the file or the socket writing this buffer - * should be flushed. + * Empty {@link DataBuffer} that indicates to the file or the socket writing it that + * previously buffered data should be flushed. * * @author Sebastien Deleuze + * @see FlushingDataBuffer#INSTANCE */ public class FlushingDataBuffer implements DataBuffer { + /** Singleton instance of this class */ + public static final FlushingDataBuffer INSTANCE = new FlushingDataBuffer(); + private final DataBuffer buffer; - public FlushingDataBuffer() { + + private FlushingDataBuffer() { this.buffer = new DefaultDataBufferFactory().allocateBuffer(0); } - public FlushingDataBuffer(DataBuffer buffer) { - Assert.notNull(buffer); - this.buffer = buffer; - } @Override public DataBufferFactory factory() { diff --git a/spring-web-reactive/src/main/java/org/springframework/http/codec/SseEventEncoder.java b/spring-web-reactive/src/main/java/org/springframework/http/codec/SseEventEncoder.java index ffeae04ab8..505ee42cf0 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/codec/SseEventEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/codec/SseEventEncoder.java @@ -115,7 +115,8 @@ public class SseEventEncoder extends AbstractEncoder { return Flux.concat( encodeString(sb.toString(), bufferFactory), dataBuffer, - encodeString("\n", bufferFactory).map(b -> new FlushingDataBuffer(b)) + encodeString("\n", bufferFactory), + Mono.just(FlushingDataBuffer.INSTANCE) ); }); diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/SseEventEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/SseEventEncoderTests.java index 3d6807829c..6f64ecbbc5 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/SseEventEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/SseEventEncoderTests.java @@ -18,6 +18,7 @@ package org.springframework.core.codec.support; import java.util.Arrays; +import static org.junit.Assert.*; import org.junit.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -26,14 +27,11 @@ import reactor.core.test.TestSubscriber; 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.FlushingDataBuffer; import org.springframework.http.codec.SseEventEncoder; import org.springframework.util.MimeType; import org.springframework.web.reactive.sse.SseEvent; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - - /** * @author Sebastien Deleuze */ @@ -77,7 +75,8 @@ public class SseEventEncoderTests extends AbstractDataBufferAllocatingTestCase { "event:foo\n" + "retry:123\n" + ":bla\n:bla bla\n:bla bla bla\n"), - stringConsumer("\n") + stringConsumer("\n"), + b -> assertEquals(FlushingDataBuffer.class, b.getClass()) ); } @@ -93,8 +92,10 @@ public class SseEventEncoderTests extends AbstractDataBufferAllocatingTestCase { .assertValuesWith( stringConsumer("data:foo\n"), stringConsumer("\n"), + b -> assertEquals(FlushingDataBuffer.class, b.getClass()), stringConsumer("data:bar\n"), - stringConsumer("\n") + stringConsumer("\n"), + b -> assertEquals(FlushingDataBuffer.class, b.getClass()) ); } @@ -110,12 +111,13 @@ public class SseEventEncoderTests extends AbstractDataBufferAllocatingTestCase { .assertValuesWith( stringConsumer("data:foo\ndata:bar\n"), stringConsumer("\n"), + b -> assertEquals(FlushingDataBuffer.class, b.getClass()), stringConsumer("data:foo\ndata:baz\n"), - stringConsumer("\n") + stringConsumer("\n"), + b -> assertEquals(FlushingDataBuffer.class, b.getClass()) ); } - @Test public void encodePojo() { SseEventEncoder encoder = new SseEventEncoder(Arrays.asList(new JacksonJsonEncoder())); @@ -130,10 +132,12 @@ public class SseEventEncoderTests extends AbstractDataBufferAllocatingTestCase { stringConsumer("{\"foo\":\"foofoo\",\"bar\":\"barbar\"}"), stringConsumer("\n"), stringConsumer("\n"), + b -> assertEquals(FlushingDataBuffer.class, b.getClass()), stringConsumer("data:"), stringConsumer("{\"foo\":\"foofoofoo\",\"bar\":\"barbarbar\"}"), stringConsumer("\n"), - stringConsumer("\n") + stringConsumer("\n"), + b -> assertEquals(FlushingDataBuffer.class, b.getClass()) ); } diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/FlushingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/FlushingIntegrationTests.java index 8dfc741a5e..2427abdca8 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/FlushingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/FlushingIntegrationTests.java @@ -70,14 +70,15 @@ public class FlushingIntegrationTests extends AbstractHttpHandlerIntegrationTest public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { Flux responseBody = Flux .interval(50) - .take(2) - .concatWith(Flux.never()) .map(l -> { byte[] data = ("data" + l).getBytes(); DataBuffer buffer = response.bufferFactory().allocateBuffer(data.length); buffer.write(data); - return new FlushingDataBuffer(buffer); - }); + return buffer; + }) + .take(2) + .concatWith(Mono.just(FlushingDataBuffer.INSTANCE)) + .concatWith(Flux.never()); return response.writeWith(responseBody); } } From 52325a21ffb76d30d8f9253934fec621f4e3f121 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Fri, 24 Jun 2016 13:42:46 +0200 Subject: [PATCH 415/498] Fixed Undertow flush support Reactored Servlet 3.1 and Undertow response support into an AbstractResponseBodySubscriber that uses an internal state machine, making thread-safity a lot easier. --- .../AbstractResponseBodySubscriber.java | 284 ++++++++++++++++++ .../reactive/ServletHttpHandlerAdapter.java | 234 +++++++-------- .../reactive/UndertowHttpHandlerAdapter.java | 167 ++++------ .../annotation/SseIntegrationTests.java | 21 +- 4 files changed, 453 insertions(+), 253 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractResponseBodySubscriber.java diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractResponseBodySubscriber.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractResponseBodySubscriber.java new file mode 100644 index 0000000000..7c174edd12 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractResponseBodySubscriber.java @@ -0,0 +1,284 @@ +/* + * 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.http.server.reactive; + +import java.io.IOException; +import java.nio.channels.Channel; +import java.util.concurrent.atomic.AtomicReference; +import javax.servlet.WriteListener; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import reactor.core.util.BackpressureUtils; + +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.FlushingDataBuffer; +import org.springframework.core.io.buffer.support.DataBufferUtils; +import org.springframework.util.Assert; + +/** + * Abstract base class for {@code Subscriber} implementations that bridge between + * event-listener APIs and Reactive Streams. Specifically, base class for the Servlet 3.1 + * and Undertow support. + * @author Arjen Poutsma + * @see ServletServerHttpRequest + * @see UndertowHttpHandlerAdapter + */ +abstract class AbstractResponseBodySubscriber implements Subscriber { + + protected final Log logger = LogFactory.getLog(getClass()); + + private final AtomicReference state = + new AtomicReference<>(State.UNSUBSCRIBED); + + private volatile DataBuffer currentBuffer; + + private volatile boolean subscriptionCompleted; + + private Subscription subscription; + + @Override + public final void onSubscribe(Subscription subscription) { + if (logger.isTraceEnabled()) { + logger.trace(this.state + " onSubscribe: " + subscription); + } + this.state.get().onSubscribe(this, subscription); + } + + @Override + public final void onNext(DataBuffer dataBuffer) { + if (logger.isTraceEnabled()) { + logger.trace(this.state + " onNext: " + dataBuffer); + } + this.state.get().onNext(this, dataBuffer); + } + + @Override + public final void onError(Throwable t) { + if (logger.isErrorEnabled()) { + logger.error(this.state + " onError: " + t, t); + } + this.state.get().onError(this, t); + } + + @Override + public final void onComplete() { + if (logger.isTraceEnabled()) { + logger.trace(this.state + " onComplete"); + } + this.state.get().onComplete(this); + } + + /** + * Called via a listener interface to indicate that writing is possible. + * @see WriteListener#onWritePossible() + * @see org.xnio.ChannelListener#handleEvent(Channel) + */ + protected final void onWritePossible() { + this.state.get().onWritePossible(this); + } + + /** + * Called when a {@link DataBuffer} is received via {@link Subscriber#onNext(Object)} + * @param dataBuffer the buffer that was received. + */ + protected void receiveBuffer(DataBuffer dataBuffer) { + Assert.state(this.currentBuffer == null); + this.currentBuffer = dataBuffer; + } + + /** + * Called when the current buffer should be + * {@linkplain DataBufferUtils#release(DataBuffer) released}. + */ + protected void releaseBuffer() { + if (logger.isTraceEnabled()) { + logger.trace("releaseBuffer: " + this.currentBuffer); + } + DataBufferUtils.release(this.currentBuffer); + this.currentBuffer = null; + } + + /** + * Writes the given data buffer to the output, indicating if the entire buffer was + * written. + * @param dataBuffer the data buffer to write + * @return {@code true} if {@code dataBuffer} was fully written and a new buffer + * can be requested; {@code false} otherwise + */ + protected abstract boolean write(DataBuffer dataBuffer) throws IOException; + + /** + * Writes the given exception to the output. + */ + protected abstract void writeError(Throwable t); + + /** + * Flushes the output. + */ + protected abstract void flush() throws IOException; + + /** + * Closes the output. + */ + protected abstract void close(); + + private void changeState(State oldState, State newState) { + this.state.compareAndSet(oldState, newState); + } + + /** + * Represents a state for the {@link Subscriber} to be in. The following figure + * indicate the four different states that exist, and the relationships between them. + * + *
    +	 *       UNSUBSCRIBED
    +	 *        |
    +	 *        v
    +	 * REQUESTED <---> RECEIVED
    +	 *         |       |
    +	 *         v       v
    +	 *         COMPLETED
    +	 * 
    + * Refer to the individual states for more information. + */ + private enum State { + + /** + * The initial unsubscribed state. Will respond to {@code onSubscribe} by + * requesting 1 buffer from the subscription, and change state to {@link + * #REQUESTED}. + */ + UNSUBSCRIBED { + @Override + void onSubscribe(AbstractResponseBodySubscriber subscriber, + Subscription subscription) { + if (BackpressureUtils.validate(subscriber.subscription, subscription)) { + subscriber.subscription = subscription; + subscriber.changeState(this, REQUESTED); + subscription.request(1); + } + } + }, + /** + * State that gets entered after a buffer has been + * {@linkplain Subscription#request(long) requested}. Responds to {@code onNext} + * by changing state to {@link #RECEIVED}, and responds to {@code onComplete} by + * changing state to {@link #COMPLETED}. + */ + REQUESTED { + @Override + void onNext(AbstractResponseBodySubscriber subscriber, + DataBuffer dataBuffer) { + subscriber.changeState(this, RECEIVED); + subscriber.receiveBuffer(dataBuffer); + } + + @Override + void onComplete(AbstractResponseBodySubscriber subscriber) { + subscriber.subscriptionCompleted = true; + subscriber.changeState(this, COMPLETED); + subscriber.close(); + } + }, + /** + * State that gets entered after a buffer has been + * {@linkplain Subscriber#onNext(Object) received}. Responds to + * {@code onWritePossible} by writing the current buffer, and if it can be + * written completely, changes state to either {@link #REQUESTED} if the + * subscription has not been completed; or {@link #COMPLETED} if it has. + */ + RECEIVED { + @Override + void onWritePossible(AbstractResponseBodySubscriber subscriber) { + DataBuffer dataBuffer = subscriber.currentBuffer; + try { + boolean writeCompleted = subscriber.write(dataBuffer); + if (writeCompleted) { + if (dataBuffer instanceof FlushingDataBuffer) { + subscriber.flush(); + } + subscriber.releaseBuffer(); + boolean subscriptionCompleted = subscriber.subscriptionCompleted; + if (!subscriptionCompleted) { + subscriber.changeState(this, REQUESTED); + subscriber.subscription.request(1); + } + else { + subscriber.changeState(this, COMPLETED); + subscriber.close(); + } + } + } + catch (IOException ex) { + subscriber.onError(ex); + } + } + + @Override + void onComplete(AbstractResponseBodySubscriber subscriber) { + subscriber.subscriptionCompleted = true; + } + }, + /** + * The terminal completed state. Does not respond to any events. + */ + COMPLETED { + @Override + void onNext(AbstractResponseBodySubscriber subscriber, + DataBuffer dataBuffer) { + // ignore + } + + @Override + void onError(AbstractResponseBodySubscriber subscriber, Throwable t) { + // ignore + } + + @Override + void onComplete(AbstractResponseBodySubscriber subscriber) { + // ignore + } + }; + + void onSubscribe(AbstractResponseBodySubscriber subscriber, Subscription s) { + throw new IllegalStateException(toString()); + } + + void onNext(AbstractResponseBodySubscriber subscriber, DataBuffer dataBuffer) { + throw new IllegalStateException(toString()); + } + + void onError(AbstractResponseBodySubscriber subscriber, Throwable t) { + subscriber.changeState(this, COMPLETED); + subscriber.writeError(t); + subscriber.close(); + } + + void onComplete(AbstractResponseBodySubscriber subscriber) { + throw new IllegalStateException(toString()); + } + + void onWritePossible(AbstractResponseBodySubscriber subscriber) { + // ignore + } + + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java index 6f8bcf8ce5..aa1edf35cd 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java @@ -34,13 +34,10 @@ import org.apache.commons.logging.LogFactory; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import reactor.core.publisher.Mono; -import reactor.core.util.BackpressureUtils; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DefaultDataBufferFactory; -import org.springframework.core.io.buffer.FlushingDataBuffer; -import org.springframework.core.io.buffer.support.DataBufferUtils; import org.springframework.http.HttpStatus; import org.springframework.util.Assert; @@ -88,16 +85,17 @@ public class ServletHttpHandlerAdapter extends HttpServlet { ServletAsyncContextSynchronizer synchronizer = new ServletAsyncContextSynchronizer(context); RequestBodyPublisher requestBody = - new RequestBodyPublisher(synchronizer, dataBufferFactory, bufferSize); + new RequestBodyPublisher(synchronizer, this.dataBufferFactory, + this.bufferSize); requestBody.registerListener(); ServletServerHttpRequest request = new ServletServerHttpRequest(servletRequest, requestBody); ResponseBodySubscriber responseBody = - new ResponseBodySubscriber(synchronizer, bufferSize); + new ResponseBodySubscriber(synchronizer, this.bufferSize); responseBody.registerListener(); ServletServerHttpResponse response = - new ServletServerHttpResponse(servletResponse, dataBufferFactory, + new ServletServerHttpResponse(servletResponse, this.dataBufferFactory, publisher -> Mono .from(subscriber -> publisher.subscribe(responseBody))); @@ -162,16 +160,17 @@ public class ServletHttpHandlerAdapter extends HttpServlet { } public void registerListener() throws IOException { - this.synchronizer.getRequest().getInputStream().setReadListener(readListener); + this.synchronizer.getRequest().getInputStream() + .setReadListener(this.readListener); } @Override protected void noLongerStalled() { try { - readListener.onDataAvailable(); + this.readListener.onDataAvailable(); } catch (IOException ex) { - readListener.onError(ex); + this.readListener.onError(ex); } } @@ -183,7 +182,9 @@ public class ServletHttpHandlerAdapter extends HttpServlet { return; } logger.trace("onDataAvailable"); - ServletInputStream input = synchronizer.getRequest().getInputStream(); + ServletInputStream input = + RequestBodyPublisher.this.synchronizer.getRequest() + .getInputStream(); while (true) { if (!checkSubscriptionForDemand()) { @@ -198,15 +199,17 @@ public class ServletHttpHandlerAdapter extends HttpServlet { break; } - int read = input.read(buffer); + int read = input.read(RequestBodyPublisher.this.buffer); logger.trace("Input read:" + read); if (read == -1) { break; } else if (read > 0) { - DataBuffer dataBuffer = dataBufferFactory.allocateBuffer(read); - dataBuffer.write(buffer, 0, read); + DataBuffer dataBuffer = + RequestBodyPublisher.this.dataBufferFactory + .allocateBuffer(read); + dataBuffer.write(RequestBodyPublisher.this.buffer, 0, read); publishOnNext(dataBuffer); } @@ -216,7 +219,7 @@ public class ServletHttpHandlerAdapter extends HttpServlet { @Override public void onAllDataRead() throws IOException { logger.trace("All data read"); - synchronizer.readComplete(); + RequestBodyPublisher.this.synchronizer.readComplete(); publishOnComplete(); } @@ -224,7 +227,7 @@ public class ServletHttpHandlerAdapter extends HttpServlet { @Override public void onError(Throwable t) { logger.trace("RequestBodyReadListener Error", t); - synchronizer.readComplete(); + RequestBodyPublisher.this.synchronizer.readComplete(); publishOnError(t); } @@ -232,9 +235,7 @@ public class ServletHttpHandlerAdapter extends HttpServlet { } - private static class ResponseBodySubscriber implements Subscriber { - - private static final Log logger = LogFactory.getLog(ResponseBodySubscriber.class); + private static class ResponseBodySubscriber extends AbstractResponseBodySubscriber { private final ResponseBodyWriteListener writeListener = new ResponseBodyWriteListener(); @@ -243,14 +244,7 @@ public class ServletHttpHandlerAdapter extends HttpServlet { private final int bufferSize; - private volatile DataBuffer dataBuffer; - - private volatile boolean completed = false; - - private volatile boolean flushOnNext = false; - - private Subscription subscription; - + private volatile boolean flushOnNext; public ResponseBodySubscriber(ServletAsyncContextSynchronizer synchronizer, int bufferSize) { @@ -259,145 +253,119 @@ public class ServletHttpHandlerAdapter extends HttpServlet { } public void registerListener() throws IOException { - synchronizer.getResponse().getOutputStream().setWriteListener(writeListener); + outputStream().setWriteListener(this.writeListener); + } + + private ServletOutputStream outputStream() throws IOException { + return this.synchronizer.getResponse().getOutputStream(); } @Override - public void onSubscribe(Subscription subscription) { - logger.trace("onSubscribe. Subscription: " + subscription); - if (BackpressureUtils.validate(this.subscription, subscription)) { - this.subscription = subscription; - this.subscription.request(1); - } - } + protected void receiveBuffer(DataBuffer dataBuffer) { + super.receiveBuffer(dataBuffer); - @Override - public void onNext(DataBuffer dataBuffer) { - Assert.state(this.dataBuffer == null); - - logger.trace("onNext. buffer: " + dataBuffer); - - this.dataBuffer = dataBuffer; try { - this.writeListener.onWritePossible(); + if (outputStream().isReady()) { + onWritePossible(); + } } - catch (IOException e) { - onError(e); + catch (IOException ignored) { } } @Override - public void onError(Throwable t) { - logger.error("onError", t); + protected boolean write(DataBuffer dataBuffer) throws IOException { + ServletOutputStream output = outputStream(); + + boolean ready = output.isReady(); + + if (this.flushOnNext) { + flush(); + ready = output.isReady(); + } + + if (this.logger.isTraceEnabled()) { + this.logger.trace("write: " + dataBuffer + " ready: " + ready); + } + + if (ready) { + int total = dataBuffer.readableByteCount(); + int written = writeDataBuffer(dataBuffer); + + if (this.logger.isTraceEnabled()) { + this.logger.trace("written: " + written + " total: " + total); + } + return written == total; + } + else { + return false; + } + } + + @Override + protected void writeError(Throwable t) { HttpServletResponse response = (HttpServletResponse) this.synchronizer.getResponse(); - response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); - this.synchronizer.complete(); + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + + @Override + protected void flush() throws IOException { + ServletOutputStream output = outputStream(); + if (output.isReady()) { + if (logger.isTraceEnabled()) { + this.logger.trace("flush"); + } + try { + output.flush(); + this.flushOnNext = false; + } + catch (IOException ignored) { + } + } + else { + this.flushOnNext = true; + } } @Override - public void onComplete() { - logger.trace("onComplete. buffer: " + this.dataBuffer); + protected void close() { + this.synchronizer.writeComplete(); + } - this.completed = true; + private int writeDataBuffer(DataBuffer dataBuffer) throws IOException { + InputStream input = dataBuffer.asInputStream(); + ServletOutputStream output = outputStream(); - if (this.dataBuffer != null) { - try { - this.writeListener.onWritePossible(); - } - catch (IOException ex) { - onError(ex); - } + int bytesWritten = 0; + byte[] buffer = new byte[this.bufferSize]; + int bytesRead = -1; + + while (output.isReady() && (bytesRead = input.read(buffer)) != -1) { + output.write(buffer, 0, bytesRead); + bytesWritten += bytesRead; } - if (this.dataBuffer == null) { - this.synchronizer.writeComplete(); - } + return bytesWritten; } private class ResponseBodyWriteListener implements WriteListener { @Override public void onWritePossible() throws IOException { - logger.trace("onWritePossible"); - ServletOutputStream output = synchronizer.getResponse().getOutputStream(); - - boolean ready = output.isReady(); - - if (flushOnNext) { - flush(output); - ready = output.isReady(); - } - - logger.trace("ready: " + ready + " buffer: " + dataBuffer); - - if (ready) { - if (dataBuffer != null) { - - int total = dataBuffer.readableByteCount(); - int written = writeDataBuffer(); - - logger.trace("written: " + written + " total: " + total); - if (written == total) { - if (dataBuffer instanceof FlushingDataBuffer) { - flush(output); - } - releaseBuffer(); - if (!completed) { - subscription.request(1); - } - else { - synchronizer.writeComplete(); - } - } - } - else if (subscription != null) { - subscription.request(1); - } - } - } - - private int writeDataBuffer() throws IOException { - InputStream input = dataBuffer.asInputStream(); - ServletOutputStream output = synchronizer.getResponse().getOutputStream(); - - int bytesWritten = 0; - byte[] buffer = new byte[bufferSize]; - int bytesRead = -1; - - while (output.isReady() && (bytesRead = input.read(buffer)) != -1) { - output.write(buffer, 0, bytesRead); - bytesWritten += bytesRead; - } - - return bytesWritten; - } - - private void flush(ServletOutputStream output) { - if (output.isReady()) { - logger.trace("Flushing"); - try { - output.flush(); - flushOnNext = false; - } - catch (IOException ignored) { - } - } else { - flushOnNext = true; - } - } - - private void releaseBuffer() { - DataBufferUtils.release(dataBuffer); - dataBuffer = null; + ResponseBodySubscriber.this.onWritePossible(); } @Override public void onError(Throwable ex) { - logger.error("ResponseBodyWriteListener error", ex); + // Error on writing to the HTTP stream, so any further writes will probably + // fail. Let's log instead of calling {@link #writeError}. + ResponseBodySubscriber.this.logger + .error("ResponseBodyWriteListener error", ex); } } } + } \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java index bc2c89ab1a..4d3f4ac7ae 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java @@ -31,12 +31,9 @@ import org.xnio.IoUtils; import org.xnio.channels.StreamSinkChannel; import org.xnio.channels.StreamSourceChannel; import reactor.core.publisher.Mono; -import reactor.core.util.BackpressureUtils; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; -import org.springframework.core.io.buffer.FlushingDataBuffer; -import org.springframework.core.io.buffer.support.DataBufferUtils; import org.springframework.util.Assert; /** @@ -65,7 +62,7 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle public void handleRequest(HttpServerExchange exchange) throws Exception { RequestBodyPublisher requestBody = - new RequestBodyPublisher(exchange, dataBufferFactory); + new RequestBodyPublisher(exchange, this.dataBufferFactory); requestBody.registerListener(); ServerHttpRequest request = new UndertowServerHttpRequest(exchange, requestBody); @@ -76,7 +73,7 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle ServerHttpResponse response = new UndertowServerHttpResponse(exchange, responseChannel, publisher -> Mono.from(subscriber -> publisher.subscribe(responseBody)), - dataBufferFactory); + this.dataBufferFactory); this.delegate.handle(request, response).subscribe(new Subscriber() { @@ -130,7 +127,7 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle } public void registerListener() { - this.requestChannel.getReadSetter().set(listener); + this.requestChannel.getReadSetter().set(this.listener); this.requestChannel.resumeReads(); } @@ -145,7 +142,7 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle @Override protected void noLongerStalled() { - listener.handleEvent(requestChannel); + this.listener.handleEvent(this.requestChannel); } private class RequestBodyListener @@ -157,7 +154,8 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle return; } logger.trace("handleEvent"); - ByteBuffer byteBuffer = pooledByteBuffer.getBuffer(); + ByteBuffer byteBuffer = + RequestBodyPublisher.this.pooledByteBuffer.getBuffer(); try { while (true) { if (!checkSubscriptionForDemand()) { @@ -177,7 +175,9 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle } else { byteBuffer.flip(); - DataBuffer dataBuffer = dataBufferFactory.wrap(byteBuffer); + DataBuffer dataBuffer = + RequestBodyPublisher.this.dataBufferFactory + .wrap(byteBuffer); publishOnNext(dataBuffer); } } @@ -190,9 +190,7 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle } - private static class ResponseBodySubscriber implements Subscriber { - - private static final Log logger = LogFactory.getLog(ResponseBodySubscriber.class); + private static class ResponseBodySubscriber extends AbstractResponseBodySubscriber { private final ChannelListener listener = new ResponseBodyListener(); @@ -203,12 +201,6 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle private volatile ByteBuffer byteBuffer; - private volatile DataBuffer dataBuffer; - - private volatile boolean completed = false; - - private Subscription subscription; - public ResponseBodySubscriber(HttpServerExchange exchange, StreamSinkChannel responseChannel) { this.exchange = exchange; @@ -216,58 +208,77 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle } public void registerListener() { - this.responseChannel.getWriteSetter().set(listener); + this.responseChannel.getWriteSetter().set(this.listener); this.responseChannel.resumeWrites(); } - @Override - public void onSubscribe(Subscription subscription) { - logger.trace("onSubscribe. Subscription: " + subscription); - if (BackpressureUtils.validate(this.subscription, subscription)) { - this.subscription = subscription; - this.subscription.request(1); + protected void writeError(Throwable t) { + if (!this.exchange.isResponseStarted() && + this.exchange.getStatusCode() < 500) { + this.exchange.setStatusCode(500); } } @Override - public void onNext(DataBuffer dataBuffer) { - Assert.state(this.byteBuffer == null); - logger.trace("onNext. buffer: " + dataBuffer); - - this.byteBuffer = dataBuffer.asByteBuffer(); - this.dataBuffer = dataBuffer; - } - - @Override - public void onError(Throwable t) { - logger.error("onError", t); - if (!exchange.isResponseStarted() && exchange.getStatusCode() < 500) { - exchange.setStatusCode(500); + protected void flush() throws IOException { + if (logger.isTraceEnabled()) { + logger.trace("flush"); } - closeChannel(responseChannel); + this.responseChannel.flush(); } @Override - public void onComplete() { - logger.trace("onComplete. buffer: " + this.byteBuffer); - - this.completed = true; - + protected boolean write(DataBuffer dataBuffer) throws IOException { if (this.byteBuffer == null) { - closeChannel(responseChannel); + return false; } + if (logger.isTraceEnabled()) { + logger.trace("write: " + dataBuffer); + } + int total = this.byteBuffer.remaining(); + int written = writeByteBuffer(this.byteBuffer); + + if (logger.isTraceEnabled()) { + logger.trace("written: " + written + " total: " + total); + } + return written == total; } - private void closeChannel(StreamSinkChannel channel) { - try { - channel.shutdownWrites(); + private int writeByteBuffer(ByteBuffer byteBuffer) throws IOException { + int written; + int totalWritten = 0; + do { + written = this.responseChannel.write(byteBuffer); + totalWritten += written; + } + while (byteBuffer.hasRemaining() && written > 0); + return totalWritten; + } - if (!channel.flush()) { - channel.getWriteSetter().set(ChannelListeners - .flushingChannelListener(o -> IoUtils.safeClose(channel), + @Override + protected void receiveBuffer(DataBuffer dataBuffer) { + super.receiveBuffer(dataBuffer); + this.byteBuffer = dataBuffer.asByteBuffer(); + } + + @Override + protected void releaseBuffer() { + super.releaseBuffer(); + this.byteBuffer = null; + } + + @Override + protected void close() { + try { + this.responseChannel.shutdownWrites(); + + if (!this.responseChannel.flush()) { + this.responseChannel.getWriteSetter().set(ChannelListeners + .flushingChannelListener( + o -> IoUtils.safeClose(this.responseChannel), ChannelListeners.closingChannelExceptionHandler())); - channel.resumeWrites(); + this.responseChannel.resumeWrites(); } } catch (IOException ignored) { @@ -278,60 +289,12 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle @Override public void handleEvent(StreamSinkChannel channel) { - if (byteBuffer != null) { - try { - int total = byteBuffer.remaining(); - int written = writeByteBuffer(channel); - - logger.trace("written: " + written + " total: " + total); - - if (written == total) { - if (dataBuffer instanceof FlushingDataBuffer) { - flush(channel); - } - releaseBuffer(); - if (!completed) { - subscription.request(1); - } - else { - closeChannel(channel); - } - } - } - catch (IOException ex) { - onError(ex); - } - } - else if (subscription != null) { - subscription.request(1); - } - - } - - private int writeByteBuffer(StreamSinkChannel channel) throws IOException { - int written; - int totalWritten = 0; - do { - written = channel.write(byteBuffer); - totalWritten += written; - } - while (byteBuffer.hasRemaining() && written > 0); - return totalWritten; - } - - private void flush(StreamSinkChannel channel) throws IOException { - logger.trace("Flushing"); - channel.flush(); - } - - private void releaseBuffer() { - DataBufferUtils.release(dataBuffer); - dataBuffer = null; - byteBuffer = null; + onWritePossible(); } } } + } \ No newline at end of file diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java index 2c98530d8c..1c9b514a29 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java @@ -22,9 +22,6 @@ import java.util.List; import org.junit.Before; import org.junit.Test; -import org.junit.runners.Parameterized; -import static org.springframework.web.client.reactive.HttpRequestBuilders.get; -import static org.springframework.web.client.reactive.WebResponseExtractors.bodyStream; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.test.TestSubscriber; @@ -43,10 +40,6 @@ import org.springframework.http.converter.reactive.HttpMessageConverter; import org.springframework.http.converter.reactive.SseHttpMessageConverter; import org.springframework.http.server.reactive.AbstractHttpHandlerIntegrationTests; import org.springframework.http.server.reactive.HttpHandler; -import org.springframework.http.server.reactive.boot.JettyHttpServer; -import org.springframework.http.server.reactive.boot.ReactorHttpServer; -import org.springframework.http.server.reactive.boot.RxNettyHttpServer; -import org.springframework.http.server.reactive.boot.TomcatHttpServer; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.reactive.WebClient; @@ -55,22 +48,14 @@ import org.springframework.web.reactive.config.WebReactiveConfiguration; import org.springframework.web.reactive.sse.SseEvent; import org.springframework.web.server.adapter.WebHttpHandlerBuilder; +import static org.springframework.web.client.reactive.HttpRequestBuilders.get; +import static org.springframework.web.client.reactive.WebResponseExtractors.bodyStream; + /** * @author Sebastien Deleuze */ public class SseIntegrationTests extends AbstractHttpHandlerIntegrationTests { - // TODO Fix Undertow support and remove this method - @Parameterized.Parameters(name = "server [{0}]") - public static Object[][] arguments() { - return new Object[][] { - {new JettyHttpServer()}, - {new RxNettyHttpServer()}, - {new ReactorHttpServer()}, - {new TomcatHttpServer()}, - }; - } - private AnnotationConfigApplicationContext wac; private WebClient webClient; From 59d3721a40a81a429cc5090d00aa805e666daf99 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Fri, 24 Jun 2016 17:09:33 +0200 Subject: [PATCH 416/498] Support default content type in CodecHttpMessageConverter --- .../reactive/CodecHttpMessageConverter.java | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/CodecHttpMessageConverter.java b/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/CodecHttpMessageConverter.java index 804ea72207..75c56cdc7f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/CodecHttpMessageConverter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/CodecHttpMessageConverter.java @@ -39,6 +39,7 @@ import org.springframework.http.support.MediaTypeUtils; * {@link Encoder} and {@link Decoder}. * * @author Arjen Poutsma + * @author Sebastien Deleuze */ public class CodecHttpMessageConverter implements HttpMessageConverter { @@ -132,11 +133,28 @@ public class CodecHttpMessageConverter implements HttpMessageConverter { } HttpHeaders headers = outputMessage.getHeaders(); if (headers.getContentType() == null) { - headers.setContentType(contentType); + MediaType contentTypeToUse = contentType; + if (contentType == null || contentType.isWildcardType() || contentType.isWildcardSubtype()) { + contentTypeToUse = getDefaultContentType(type); + } + headers.setContentType(contentTypeToUse); } DataBufferFactory dataBufferFactory = outputMessage.bufferFactory(); Flux body = this.encoder.encode(inputStream, dataBufferFactory, type, contentType); return outputMessage.writeWith(body); } + + /** + * Returns the default content type for the given type. Called when {@link #write} + * is invoked without a specified content type parameter. + *

    By default, this returns a {@link MediaType} created using the first element of + * the encoder {@link Encoder#getEncodableMimeTypes() encodableMimeTypes} property, if any. + * Can be overridden in subclasses. + * @param type the type to return the content type for + * @return the content type, or {@code null} if not known + */ + protected MediaType getDefaultContentType(ResolvableType type) { + return (!this.writableMediaTypes.isEmpty() ? this.writableMediaTypes.get(0) : null); + } } From e6a0b39df5ce6deda90c4fe3b2e712bf28d1938b Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Fri, 24 Jun 2016 17:10:56 +0200 Subject: [PATCH 417/498] Remove SseHttpMessageConverter CodecHttpMessageConverter is now suitable for SSE since it now handles default content type. --- .../http/codec/SseEventEncoder.java | 5 +- .../reactive/SseHttpMessageConverter.java | 67 ------------------- .../web/reactive/sse/SseEvent.java | 14 ++-- .../ResponseEntityResultHandlerTests.java | 1 - .../annotation/SseIntegrationTests.java | 7 +- 5 files changed, 16 insertions(+), 78 deletions(-) delete mode 100644 spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/SseHttpMessageConverter.java diff --git a/spring-web-reactive/src/main/java/org/springframework/http/codec/SseEventEncoder.java b/spring-web-reactive/src/main/java/org/springframework/http/codec/SseEventEncoder.java index 505ee42cf0..f06fedc25f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/codec/SseEventEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/codec/SseEventEncoder.java @@ -112,11 +112,14 @@ public class SseEventEncoder extends AbstractEncoder { } } + // Keep the SSE connection open even for cold stream in order to avoid + // unexpected browser reconnection return Flux.concat( encodeString(sb.toString(), bufferFactory), dataBuffer, encodeString("\n", bufferFactory), - Mono.just(FlushingDataBuffer.INSTANCE) + Mono.just(FlushingDataBuffer.INSTANCE), + Flux.never() ); }); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/SseHttpMessageConverter.java b/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/SseHttpMessageConverter.java deleted file mode 100644 index 705bed7d75..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/SseHttpMessageConverter.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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.http.converter.reactive; - - -import java.util.Arrays; -import java.util.List; - -import org.reactivestreams.Publisher; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -import org.springframework.core.ResolvableType; -import org.springframework.core.codec.Encoder; -import org.springframework.http.codec.SseEventEncoder; -import org.springframework.http.MediaType; -import org.springframework.http.ReactiveHttpOutputMessage; -import org.springframework.web.reactive.sse.SseEvent; - -/** - * Implementation of {@link HttpMessageConverter} that can stream Server-Sent Events - * response. - * - * It allows to write {@code Flux}, which is Spring Web Reactive equivalent - * to Spring MVC {@code SseEmitter}. - * - * Sending {@code Flux} or {@code Flux} is equivalent to sending - * {@code Flux} with the {@code data} property set to the {@code String} or - * {@code Pojo} value. - * - * @author Sebastien Deleuze - * @see SseEvent - * @see Server-Sent Events W3C recommandation - */ -public class SseHttpMessageConverter extends CodecHttpMessageConverter { - - /** - * Constructor that creates a new instance configured with the specified data encoders. - */ - public SseHttpMessageConverter(List> dataEncoders) { - super(new SseEventEncoder(dataEncoders), null); - } - - @Override - public Mono write(Publisher inputStream, ResolvableType type, - MediaType contentType, ReactiveHttpOutputMessage outputMessage) { - - outputMessage.getHeaders().add("Content-Type", "text/event-stream"); - // Keep the SSE connection open even for cold stream in order to avoid unexpected Browser reconnection - return super.write(Flux.from(inputStream).concatWith(Flux.never()), type, contentType, outputMessage); - } - -} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/sse/SseEvent.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/sse/SseEvent.java index 6f0eaf6828..1b2f99b9dc 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/sse/SseEvent.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/sse/SseEvent.java @@ -16,7 +16,7 @@ package org.springframework.web.reactive.sse; -import org.springframework.http.converter.reactive.SseHttpMessageConverter; +import org.springframework.http.codec.SseEventEncoder; import org.springframework.util.MimeType; /** @@ -26,7 +26,7 @@ import org.springframework.util.MimeType; * {@code SseEmitter} type. It allows to send Server-Sent Events in a reactive way. * * @author Sebastien Deleuze - * @see SseHttpMessageConverter + * @see SseEventEncoder * @see Server-Sent Events W3C recommandation */ public class SseEvent { @@ -95,9 +95,9 @@ public class SseEvent { /** * Set {@code data} SSE field. If a multiline {@code String} is provided, it will be * turned into multiple {@code data} field lines by as - * defined in Server-Sent Events W3C recommandation. + * defined in Server-Sent Events W3C recommendation. * - * If no {@code mediaType} is defined, default {@link SseHttpMessageConverter} will: + * If no {@code mediaType} is defined, default {@link SseEventEncoder} will: * - Turn single line {@code String} to a single {@code data} field * - Turn multiline line {@code String} to multiple {@code data} fields * - Serialize other {@code Object} as JSON @@ -117,7 +117,7 @@ public class SseEvent { /** * Set the {@link MimeType} used to serialize the {@code data}. - * {@link SseHttpMessageConverter} should be configured with the relevant encoder to be + * {@link SseEventEncoder} should be configured with the relevant encoder to be * able to serialize it. */ public void setMimeType(MimeType mimeType) { @@ -147,8 +147,8 @@ public class SseEvent { /** * Set SSE comment. If a multiline comment is provided, it will be turned into multiple - * SSE comment lines by {@link SseHttpMessageConverter} as defined in Server-Sent Events W3C - * recommandation. + * SSE comment lines by {@link SseEventEncoder} as defined in Server-Sent Events W3C + * recommendation. */ public void setComment(String comment) { this.comment = comment; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java index 3cdd9ad26c..ba131de3f2 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java @@ -33,7 +33,6 @@ import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.io.buffer.support.DataBufferTestUtils; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.reactive.CodecHttpMessageConverter; import org.springframework.http.converter.reactive.HttpMessageConverter; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java index 1c9b514a29..30cbbecc3d 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java @@ -29,6 +29,7 @@ import reactor.core.test.TestSubscriber; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.codec.Encoder; import org.springframework.core.codec.support.ByteBufferDecoder; import org.springframework.core.codec.support.JacksonJsonDecoder; import org.springframework.core.codec.support.JacksonJsonEncoder; @@ -36,8 +37,9 @@ import org.springframework.core.codec.support.JsonObjectDecoder; import org.springframework.core.codec.support.StringDecoder; import org.springframework.http.MediaType; import org.springframework.http.client.reactive.ReactorHttpClientRequestFactory; +import org.springframework.http.codec.SseEventEncoder; +import org.springframework.http.converter.reactive.CodecHttpMessageConverter; import org.springframework.http.converter.reactive.HttpMessageConverter; -import org.springframework.http.converter.reactive.SseHttpMessageConverter; import org.springframework.http.server.reactive.AbstractHttpHandlerIntegrationTests; import org.springframework.http.server.reactive.HttpHandler; import org.springframework.web.bind.annotation.RequestMapping; @@ -165,7 +167,8 @@ public class SseIntegrationTests extends AbstractHttpHandlerIntegrationTests { @Override protected void extendMessageConverters(List> converters) { - converters.add(new SseHttpMessageConverter(Arrays.asList(new JacksonJsonEncoder()))); + Encoder sseEncoder = new SseEventEncoder(Arrays.asList(new JacksonJsonEncoder())); + converters.add(new CodecHttpMessageConverter<>(sseEncoder)); } } From 3fe87ee22551ea8f0176de84b5cd9cae488aa9db Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Fri, 24 Jun 2016 17:21:01 +0200 Subject: [PATCH 418/498] Change SseEvent#mimeType to SseEvent#mediaType --- .../http/codec/SseEventEncoder.java | 9 +++---- .../web/reactive/sse/SseEvent.java | 24 +++++++++---------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/codec/SseEventEncoder.java b/spring-web-reactive/src/main/java/org/springframework/http/codec/SseEventEncoder.java index f06fedc25f..5cbac76600 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/codec/SseEventEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/codec/SseEventEncoder.java @@ -31,6 +31,7 @@ import org.springframework.core.codec.support.AbstractEncoder; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.FlushingDataBuffer; +import org.springframework.http.MediaType; import org.springframework.util.Assert; import org.springframework.util.MimeType; import org.springframework.web.reactive.sse.SseEvent; @@ -87,8 +88,8 @@ public class SseEventEncoder extends AbstractEncoder { Object data = event.getData(); Flux dataBuffer = Flux.empty(); - MimeType mimeType = (event.getMimeType() == null ? - new MimeType("*") : event.getMimeType()); + MediaType mediaType = (event.getMediaType() == null ? + MediaType.ALL : event.getMediaType()); if (data != null) { sb.append("data:"); if (data instanceof String) { @@ -97,13 +98,13 @@ public class SseEventEncoder extends AbstractEncoder { else { Optional> encoder = dataEncoders .stream() - .filter(e -> e.canEncode(ResolvableType.forClass(data.getClass()), mimeType)) + .filter(e -> e.canEncode(ResolvableType.forClass(data.getClass()), mediaType)) .findFirst(); if (encoder.isPresent()) { dataBuffer = ((Encoder)encoder.get()) .encode(Mono.just(data), bufferFactory, - ResolvableType.forClass(data.getClass()), mimeType) + ResolvableType.forClass(data.getClass()), mediaType) .concatWith(encodeString("\n", bufferFactory)); } else { diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/sse/SseEvent.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/sse/SseEvent.java index 1b2f99b9dc..9386e59d57 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/sse/SseEvent.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/sse/SseEvent.java @@ -16,8 +16,8 @@ package org.springframework.web.reactive.sse; +import org.springframework.http.MediaType; import org.springframework.http.codec.SseEventEncoder; -import org.springframework.util.MimeType; /** * Represent a Server-Sent Event. @@ -27,7 +27,7 @@ import org.springframework.util.MimeType; * * @author Sebastien Deleuze * @see SseEventEncoder - * @see Server-Sent Events W3C recommandation + * @see Server-Sent Events W3C recommendation */ public class SseEvent { @@ -37,7 +37,7 @@ public class SseEvent { private Object data; - private MimeType mimeType; + private MediaType mediaType; private Long reconnectTime; @@ -59,9 +59,9 @@ public class SseEvent { /** * Create an instance with the provided {@code data} and {@code mediaType}. */ - public SseEvent(Object data, MimeType mimeType) { + public SseEvent(Object data, MediaType mediaType) { this.data = data; - this.mimeType = mimeType; + this.mediaType = mediaType; } /** @@ -102,7 +102,7 @@ public class SseEvent { * - Turn multiline line {@code String} to multiple {@code data} fields * - Serialize other {@code Object} as JSON * - * @see #setMimeType(MimeType) + * @see #setMediaType(MediaType) */ public void setData(Object data) { this.data = data; @@ -116,19 +116,19 @@ public class SseEvent { } /** - * Set the {@link MimeType} used to serialize the {@code data}. + * Set the {@link MediaType} used to serialize the {@code data}. * {@link SseEventEncoder} should be configured with the relevant encoder to be * able to serialize it. */ - public void setMimeType(MimeType mimeType) { - this.mimeType = mimeType; + public void setMediaType(MediaType mediaType) { + this.mediaType = mediaType; } /** - * @see #setMimeType(MimeType) + * @see #setMediaType(MediaType) */ - public MimeType getMimeType() { - return mimeType; + public MediaType getMediaType() { + return this.mediaType; } /** From cae8800183cd89749a4b41d73eb58cf737b8df73 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 24 Jun 2016 10:15:06 -0400 Subject: [PATCH 419/498] Refactor @ResponseBody and ResponseEntity tests Introduce separate test classes for each base class in the hierarchy above @ResponseBody and ResponseEntity result handlers. Also start porting existing unit test cases for @ResponseBody and ResponseEntity return value handlers. --- ...ontentNegotiatingResultHandlerSupport.java | 2 +- ...AbstractMessageConverterResultHandler.java | 13 +- .../ResponseEntityResultHandler.java | 2 +- ...tNegotiatingResultHandlerSupportTests.java | 137 +++++++++++++++ .../MessageConverterResultHandlerTests.java | 155 +++++++++++++++++ .../ResponseBodyResultHandlerTests.java | 161 ++++++++++-------- .../ResponseEntityResultHandlerTests.java | 155 +++++++++-------- 7 files changed, 470 insertions(+), 155 deletions(-) create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/result/ContentNegotiatingResultHandlerSupportTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterResultHandlerTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/ContentNegotiatingResultHandlerSupport.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/ContentNegotiatingResultHandlerSupport.java index fd0b0fb4b7..ecf9866a26 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/ContentNegotiatingResultHandlerSupport.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/ContentNegotiatingResultHandlerSupport.java @@ -126,7 +126,7 @@ public abstract class ContentNegotiatingResultHandlerSupport implements Ordered } private List getAcceptableTypes(ServerWebExchange exchange) { - List mediaTypes = this.contentTypeResolver.resolveMediaTypes(exchange); + List mediaTypes = getContentTypeResolver().resolveMediaTypes(exchange); return (mediaTypes.isEmpty() ? Collections.singletonList(MediaType.ALL) : mediaTypes); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageConverterResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageConverterResultHandler.java index 5201bf7f75..4394b698a9 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageConverterResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageConverterResultHandler.java @@ -19,6 +19,7 @@ import java.util.List; import java.util.stream.Collectors; import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.core.ResolvableType; @@ -82,16 +83,22 @@ public abstract class AbstractMessageConverterResultHandler extends ContentNegot publisher = Mono.empty(); } elementType = bodyType.getGeneric(0); - if (Void.class.equals(elementType.getRawClass())) { - return Mono.from((Publisher) publisher); - } } else { publisher = Mono.justOrEmpty(body); elementType = bodyType; } + if (Void.class.equals(elementType.getRawClass())) { + return Mono.from((Publisher) publisher); + } + List producibleTypes = getProducibleMediaTypes(elementType); + if (producibleTypes.isEmpty()) { + return Mono.error(new IllegalStateException( + "No converter for return value type: " + elementType)); + } + MediaType bestMediaType = selectMediaType(exchange, producibleTypes); if (bestMediaType != null) { diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java index 08a7114d90..42e50afe65 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java @@ -104,7 +104,7 @@ public class ResponseEntityResultHandler extends AbstractMessageConverterResultH if (!entityHeaders.isEmpty()) { entityHeaders.entrySet().stream() - .filter(entry -> responseHeaders.containsKey(entry.getKey())) + .filter(entry -> !responseHeaders.containsKey(entry.getKey())) .forEach(entry -> responseHeaders.put(entry.getKey(), entry.getValue())); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/ContentNegotiatingResultHandlerSupportTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/ContentNegotiatingResultHandlerSupportTests.java new file mode 100644 index 0000000000..8059a9bdf6 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/ContentNegotiatingResultHandlerSupportTests.java @@ -0,0 +1,137 @@ +/* + * 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.web.reactive.result; + +import java.net.URI; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.web.reactive.accept.FixedContentTypeResolver; +import org.springframework.web.reactive.accept.HeaderContentTypeResolver; +import org.springframework.web.reactive.accept.RequestedContentTypeResolver; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.WebSessionManager; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.springframework.http.MediaType.ALL; +import static org.springframework.http.MediaType.APPLICATION_JSON_UTF8; +import static org.springframework.http.MediaType.APPLICATION_OCTET_STREAM; +import static org.springframework.http.MediaType.IMAGE_GIF; +import static org.springframework.http.MediaType.IMAGE_JPEG; +import static org.springframework.http.MediaType.IMAGE_PNG; +import static org.springframework.http.MediaType.TEXT_PLAIN; +import static org.springframework.web.reactive.HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE; + +/** + * Unit tests for {@link ContentNegotiatingResultHandlerSupport}. + * @author Rossen Stoyanchev + */ +public class ContentNegotiatingResultHandlerSupportTests { + + private TestHandlerSupport handlerSupport; + + private MockServerHttpRequest request; + + private ServerWebExchange exchange; + + + @Before + public void setUp() throws Exception { + this.handlerSupport = new TestHandlerSupport(); + this.request = new MockServerHttpRequest(HttpMethod.GET, new URI("/path")); + this.exchange = new DefaultServerWebExchange( + this.request, new MockServerHttpResponse(), mock(WebSessionManager.class)); + } + + + @Test + public void usesContentTypeResolver() throws Exception { + RequestedContentTypeResolver resolver = new FixedContentTypeResolver(IMAGE_GIF); + TestHandlerSupport handlerSupport = new TestHandlerSupport(resolver); + + List mediaTypes = Arrays.asList(IMAGE_JPEG, IMAGE_GIF, IMAGE_PNG); + MediaType actual = handlerSupport.selectMediaType(this.exchange, mediaTypes); + + assertEquals(IMAGE_GIF, actual); + } + + @Test + public void producibleMediaTypesRequestAttribute() throws Exception { + Set producible = Collections.singleton(IMAGE_GIF); + this.exchange.getAttributes().put(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, producible); + + List mediaTypes = Arrays.asList(IMAGE_JPEG, IMAGE_GIF, IMAGE_PNG); + MediaType actual = handlerSupport.selectMediaType(this.exchange, mediaTypes); + + assertEquals(IMAGE_GIF, actual); + } + + @Test // SPR-9160 + public void sortsByQuality() throws Exception { + this.request.getHeaders().add("Accept", "text/plain; q=0.5, application/json"); + + List mediaTypes = Arrays.asList(TEXT_PLAIN, APPLICATION_JSON_UTF8); + MediaType actual = this.handlerSupport.selectMediaType(this.exchange, mediaTypes); + + assertEquals(APPLICATION_JSON_UTF8, actual); + } + + @Test + public void charsetFromAcceptHeader() throws Exception { + MediaType text8859 = MediaType.parseMediaType("text/plain;charset=ISO-8859-1"); + MediaType textUtf8 = MediaType.parseMediaType("text/plain;charset=UTF-8"); + + this.request.getHeaders().setAccept(Collections.singletonList(text8859)); + MediaType actual = this.handlerSupport.selectMediaType(this.exchange, Collections.singletonList(textUtf8)); + + assertEquals(text8859, actual); + } + + @Test // SPR-12894 + public void noConcreteMediaType() throws Exception { + List producible = Collections.singletonList(ALL); + MediaType actual = this.handlerSupport.selectMediaType(this.exchange, producible); + + assertEquals(APPLICATION_OCTET_STREAM, actual); + } + + + + + private static class TestHandlerSupport extends ContentNegotiatingResultHandlerSupport { + + protected TestHandlerSupport() { + this(new HeaderContentTypeResolver()); + } + + public TestHandlerSupport(RequestedContentTypeResolver contentTypeResolver) { + super(new GenericConversionService(), contentTypeResolver); + } + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterResultHandlerTests.java new file mode 100644 index 0000000000..3f66528315 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterResultHandlerTests.java @@ -0,0 +1,155 @@ +/* + * 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.web.reactive.result.method.annotation; + +import java.io.ByteArrayOutputStream; +import java.io.OutputStream; +import java.net.URI; +import java.nio.charset.Charset; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.test.TestSubscriber; +import rx.Observable; + +import org.springframework.core.ResolvableType; +import org.springframework.core.codec.Decoder; +import org.springframework.core.codec.Encoder; +import org.springframework.core.codec.support.ByteBufferDecoder; +import org.springframework.core.codec.support.ByteBufferEncoder; +import org.springframework.core.codec.support.JacksonJsonDecoder; +import org.springframework.core.codec.support.JacksonJsonEncoder; +import org.springframework.core.codec.support.Jaxb2Decoder; +import org.springframework.core.codec.support.Jaxb2Encoder; +import org.springframework.core.codec.support.JsonObjectDecoder; +import org.springframework.core.codec.support.StringDecoder; +import org.springframework.core.codec.support.StringEncoder; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter; +import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.buffer.support.DataBufferTestUtils; +import org.springframework.http.HttpMethod; +import org.springframework.http.converter.reactive.CodecHttpMessageConverter; +import org.springframework.http.converter.reactive.HttpMessageConverter; +import org.springframework.http.converter.reactive.ResourceHttpMessageConverter; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.util.ObjectUtils; +import org.springframework.web.reactive.accept.RequestedContentTypeResolver; +import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.WebSessionManager; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.mock; + +/** + * Unit tests for {@link AbstractMessageConverterResultHandler}. + * @author Rossen Stoyanchev + */ +public class MessageConverterResultHandlerTests { + + private AbstractMessageConverterResultHandler resultHandler; + + private MockServerHttpResponse response = new MockServerHttpResponse(); + + private ServerWebExchange exchange; + + + @Before + public void setUp() throws Exception { + this.resultHandler = createResultHandler(); + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/path")); + this.exchange = new DefaultServerWebExchange(request, this.response, mock(WebSessionManager.class)); + } + + + @Test // SPR-12894 + @Ignore // GH # 121 + public void useDefaultContentType() throws Exception { + Object body = new ByteArrayResource("body".getBytes("UTF-8")); + ResolvableType bodyType = ResolvableType.forType(Resource.class); + this.resultHandler.writeBody(this.exchange, body, bodyType).block(Duration.ofSeconds(5)); + + assertEquals("image/jpeg", this.response.getHeaders().getFirst("Content-Type")); + TestSubscriber.subscribe(this.response.getBody()) + .assertValuesWith(buf -> assertEquals("body", + DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); + } + + @Test + public void voidReturnType() throws Exception { + testVoidReturnType(null, ResolvableType.forType(Void.class)); + testVoidReturnType(Mono.empty(), ResolvableType.forClassWithGenerics(Mono.class, Void.class)); + testVoidReturnType(Flux.empty(), ResolvableType.forClassWithGenerics(Flux.class, Void.class)); + testVoidReturnType(Observable.empty(), ResolvableType.forClassWithGenerics(Observable.class, Void.class)); + } + + private void testVoidReturnType(Object body, ResolvableType bodyType) { + this.resultHandler.writeBody(this.exchange, body, bodyType).block(Duration.ofSeconds(5)); + + assertNull(this.response.getHeaders().get("Content-Type")); + assertNull(this.response.getBody()); + } + + @Test // SPR-13135 + public void unsupportedReturnType() throws Exception { + ByteArrayOutputStream body = new ByteArrayOutputStream(); + ResolvableType bodyType = ResolvableType.forType(OutputStream.class); + + HttpMessageConverter converter = new CodecHttpMessageConverter<>(new ByteBufferEncoder()); + Mono mono = createResultHandler(converter).writeBody(this.exchange, body, bodyType); + + TestSubscriber.subscribe(mono).assertError(IllegalStateException.class); + } + + + private AbstractMessageConverterResultHandler createResultHandler(HttpMessageConverter... converters) { + List> converterList; + if (ObjectUtils.isEmpty(converters)) { + converterList = new ArrayList<>(); + converterList.add(new CodecHttpMessageConverter<>(new ByteBufferEncoder())); + converterList.add(new CodecHttpMessageConverter<>(new StringEncoder())); + converterList.add(new ResourceHttpMessageConverter()); + converterList.add(new CodecHttpMessageConverter<>(new Jaxb2Encoder())); + converterList.add(new CodecHttpMessageConverter<>(new JacksonJsonEncoder())); + } + else { + converterList = Arrays.asList(converters); + } + + GenericConversionService service = new GenericConversionService(); + service.addConverter(new ReactiveStreamsToCompletableFutureConverter()); + service.addConverter(new ReactiveStreamsToRxJava1Converter()); + + RequestedContentTypeResolver resolver = new RequestedContentTypeResolverBuilder().build(); + + return new AbstractMessageConverterResultHandler(converterList, service, resolver) {}; + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java index 0d0745ede9..f2cd44b397 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java @@ -17,143 +17,154 @@ package org.springframework.web.reactive.result.method.annotation; import java.net.URI; -import java.net.URISyntaxException; +import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; +import org.junit.Before; import org.junit.Test; -import org.reactivestreams.Publisher; import org.springframework.core.ResolvableType; -import org.springframework.core.codec.Encoder; +import org.springframework.core.codec.support.ByteBufferEncoder; import org.springframework.core.codec.support.JacksonJsonEncoder; +import org.springframework.core.codec.support.Jaxb2Encoder; import org.springframework.core.codec.support.StringEncoder; import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter; +import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter; import org.springframework.http.HttpMethod; -import org.springframework.http.MediaType; import org.springframework.http.converter.reactive.CodecHttpMessageConverter; import org.springframework.http.converter.reactive.HttpMessageConverter; +import org.springframework.http.converter.reactive.ResourceHttpMessageConverter; import org.springframework.http.server.reactive.MockServerHttpRequest; import org.springframework.http.server.reactive.MockServerHttpResponse; import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.stereotype.Controller; import org.springframework.ui.ExtendedModelMap; +import org.springframework.util.ObjectUtils; import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; import org.springframework.web.method.HandlerMethod; -import org.springframework.web.reactive.HandlerMapping; import org.springframework.web.reactive.HandlerResult; -import org.springframework.web.reactive.HandlerResultHandler; -import org.springframework.web.reactive.accept.FixedContentTypeResolver; -import org.springframework.web.reactive.accept.HeaderContentTypeResolver; import org.springframework.web.reactive.accept.RequestedContentTypeResolver; +import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange; import org.springframework.web.server.session.WebSessionManager; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; -import static org.springframework.http.MediaType.APPLICATION_JSON_UTF8; /** * Unit tests for {@link ResponseBodyResultHandler}. + * + * consider whether the logic under test is in a parent class, then see: + *
      + *
    • {@code MessageConverterResultHandlerTests}, + *
    • {@code ContentNegotiatingResultHandlerSupportTests} + *
    * @author Sebastien Deleuze * @author Rossen Stoyanchev */ public class ResponseBodyResultHandlerTests { + private ResponseBodyResultHandler resultHandler; + + private MockServerHttpResponse response = new MockServerHttpResponse(); + + private ServerWebExchange exchange; + + + @Before + public void setUp() throws Exception { + this.resultHandler = createHandler(); + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/path")); + this.exchange = new DefaultServerWebExchange(request, this.response, mock(WebSessionManager.class)); + } + + + private ResponseBodyResultHandler createHandler(HttpMessageConverter... converters) { + List> converterList; + if (ObjectUtils.isEmpty(converters)) { + converterList = new ArrayList<>(); + converterList.add(new CodecHttpMessageConverter<>(new ByteBufferEncoder())); + converterList.add(new CodecHttpMessageConverter<>(new StringEncoder())); + converterList.add(new ResourceHttpMessageConverter()); + converterList.add(new CodecHttpMessageConverter<>(new Jaxb2Encoder())); + converterList.add(new CodecHttpMessageConverter<>(new JacksonJsonEncoder())); + } + else { + converterList = Arrays.asList(converters); + } + GenericConversionService service = new GenericConversionService(); + service.addConverter(new ReactiveStreamsToCompletableFutureConverter()); + service.addConverter(new ReactiveStreamsToRxJava1Converter()); + RequestedContentTypeResolver resolver = new RequestedContentTypeResolverBuilder().build(); + + return new ResponseBodyResultHandler(converterList, new DefaultConversionService(), resolver); + } @Test public void supports() throws NoSuchMethodException { - ResponseBodyResultHandler handler = createHandler(new StringEncoder()); TestController controller = new TestController(); + testSupports(controller, "handleReturningString", true); + testSupports(controller, "handleReturningVoid", true); + testSupports(controller, "doWork", false); - HandlerMethod hm = new HandlerMethod(controller, TestController.class.getMethod("notAnnotated")); + TestRestController restController = new TestRestController(); + testSupports(restController, "handleReturningString", true); + testSupports(restController, "handleReturningVoid", true); + } + + private void testSupports(Object controller, String method, boolean result) throws NoSuchMethodException { + HandlerMethod hm = handlerMethod(controller, method); ResolvableType type = ResolvableType.forMethodParameter(hm.getReturnType()); - assertFalse(handler.supports(new HandlerResult(hm, null, type, new ExtendedModelMap()))); - - hm = new HandlerMethod(controller, TestController.class.getMethod("publisherString")); - type = ResolvableType.forMethodParameter(hm.getReturnType()); - assertTrue(handler.supports(new HandlerResult(hm, null, type, new ExtendedModelMap()))); - - hm = new HandlerMethod(controller, TestController.class.getMethod("publisherVoid")); - type = ResolvableType.forMethodParameter(hm.getReturnType()); - assertTrue(handler.supports(new HandlerResult(hm, null, type, new ExtendedModelMap()))); + HandlerResult handlerResult = new HandlerResult(hm, null, type, new ExtendedModelMap()); + assertEquals(result, this.resultHandler.supports(handlerResult)); } @Test public void defaultOrder() throws Exception { - ResponseBodyResultHandler handler = createHandler(new StringEncoder()); - assertEquals(100, handler.getOrder()); - } - - @Test - public void usesContentTypeResolver() throws Exception { - RequestedContentTypeResolver resolver = new FixedContentTypeResolver(APPLICATION_JSON_UTF8); - HandlerResultHandler handler = createHandler(resolver, new StringEncoder(), new JacksonJsonEncoder()); - - ServerWebExchange exchange = createExchange("/foo"); - HandlerResult result = new HandlerResult(new Object(), "fooValue", ResolvableType.forClass(String.class)); - handler.handleResult(exchange, result).block(); - - assertEquals(APPLICATION_JSON_UTF8, exchange.getResponse().getHeaders().getContentType()); - } - - @Test - public void detectsProducibleMediaTypesAttribute() throws Exception { - ServerWebExchange exchange = createExchange("/foo"); - Set mediaTypes = Collections.singleton(MediaType.APPLICATION_JSON); - exchange.getAttributes().put(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, mediaTypes); - - HandlerResultHandler handler = createHandler(new StringEncoder(), new JacksonJsonEncoder()); - - HandlerResult result = new HandlerResult(new Object(), "fooValue", ResolvableType.forClass(String.class)); - handler.handleResult(exchange, result).block(); - - assertEquals(MediaType.APPLICATION_JSON, exchange.getResponse().getHeaders().getContentType()); + assertEquals(100, this.resultHandler.getOrder()); } - private ResponseBodyResultHandler createHandler(Encoder... encoders) { - return createHandler(new HeaderContentTypeResolver(), encoders); - } - - private ResponseBodyResultHandler createHandler(RequestedContentTypeResolver resolver, - Encoder... encoders) { - - List> converters = Arrays.stream(encoders) - .map(encoder -> new CodecHttpMessageConverter<>(encoder, null)) - .collect(Collectors.toList()); - return new ResponseBodyResultHandler(converters, new DefaultConversionService(), resolver); - } - - private ServerWebExchange createExchange(String path) throws URISyntaxException { - ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI(path)); - WebSessionManager sessionManager = mock(WebSessionManager.class); - return new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); + private HandlerMethod handlerMethod(Object controller, String method) throws NoSuchMethodException { + return new HandlerMethod(controller, controller.getClass().getMethod(method)); } - @SuppressWarnings("unused") + @RestController @SuppressWarnings("unused") + private static class TestRestController { + + public String handleReturningString() { + return null; + } + + public Void handleReturningVoid() { + return null; + } + } + + @Controller @SuppressWarnings("unused") private static class TestController { - public Publisher notAnnotated() { + @ResponseBody + public String handleReturningString() { return null; } @ResponseBody - public Publisher publisherString() { + public Void handleReturningVoid() { return null; } - @ResponseBody - public Publisher publisherVoid() { + public String doWork() { return null; } + } } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java index ba131de3f2..1b4a1b8ebc 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java @@ -16,134 +16,139 @@ package org.springframework.web.reactive.result.method.annotation; import java.net.URI; -import java.net.URISyntaxException; -import java.nio.charset.Charset; +import java.time.Duration; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.stream.Collectors; +import org.junit.Before; import org.junit.Test; -import reactor.core.test.TestSubscriber; import org.springframework.core.ResolvableType; -import org.springframework.core.codec.Encoder; +import org.springframework.core.codec.support.ByteBufferEncoder; import org.springframework.core.codec.support.JacksonJsonEncoder; +import org.springframework.core.codec.support.Jaxb2Encoder; import org.springframework.core.codec.support.StringEncoder; import org.springframework.core.convert.support.DefaultConversionService; -import org.springframework.core.io.buffer.support.DataBufferTestUtils; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter; +import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.reactive.CodecHttpMessageConverter; import org.springframework.http.converter.reactive.HttpMessageConverter; +import org.springframework.http.converter.reactive.ResourceHttpMessageConverter; import org.springframework.http.server.reactive.MockServerHttpRequest; import org.springframework.http.server.reactive.MockServerHttpResponse; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.ui.ExtendedModelMap; -import org.springframework.web.method.HandlerMethod; +import org.springframework.ui.ModelMap; +import org.springframework.util.ObjectUtils; import org.springframework.web.reactive.HandlerResult; -import org.springframework.web.reactive.HandlerResultHandler; -import org.springframework.web.reactive.accept.FixedContentTypeResolver; -import org.springframework.web.reactive.accept.HeaderContentTypeResolver; import org.springframework.web.reactive.accept.RequestedContentTypeResolver; +import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange; import org.springframework.web.server.session.WebSessionManager; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; -import static org.springframework.http.MediaType.APPLICATION_JSON_UTF8; /** - * Unit tests for {@link ResponseEntityResultHandler}. + * Unit tests for {@link ResponseEntityResultHandler}. When adding a test also + * consider whether the logic under test is in a parent class, then see: + *
      + *
    • {@code MessageConverterResultHandlerTests}, + *
    • {@code ContentNegotiatingResultHandlerSupportTests} + *
    * @author Rossen Stoyanchev */ public class ResponseEntityResultHandlerTests { + private static final Object HANDLER = new Object(); + + + private ResponseEntityResultHandler resultHandler; + private MockServerHttpResponse response = new MockServerHttpResponse(); + private ServerWebExchange exchange; + + + @Before + public void setUp() throws Exception { + this.resultHandler = createHandler(); + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/path")); + this.exchange = new DefaultServerWebExchange(request, this.response, mock(WebSessionManager.class)); + } + + private ResponseEntityResultHandler createHandler(HttpMessageConverter... converters) { + List> converterList; + if (ObjectUtils.isEmpty(converters)) { + converterList = new ArrayList<>(); + converterList.add(new CodecHttpMessageConverter<>(new ByteBufferEncoder())); + converterList.add(new CodecHttpMessageConverter<>(new StringEncoder())); + converterList.add(new ResourceHttpMessageConverter()); + converterList.add(new CodecHttpMessageConverter<>(new Jaxb2Encoder())); + converterList.add(new CodecHttpMessageConverter<>(new JacksonJsonEncoder())); + } + else { + converterList = Arrays.asList(converters); + } + GenericConversionService service = new GenericConversionService(); + service.addConverter(new ReactiveStreamsToCompletableFutureConverter()); + service.addConverter(new ReactiveStreamsToRxJava1Converter()); + RequestedContentTypeResolver resolver = new RequestedContentTypeResolverBuilder().build(); + + return new ResponseEntityResultHandler(converterList, new DefaultConversionService(), resolver); + } + @Test public void supports() throws NoSuchMethodException { - ResponseEntityResultHandler handler = createHandler(new StringEncoder()); - TestController controller = new TestController(); + ModelMap model = new ExtendedModelMap(); - HandlerMethod hm = new HandlerMethod(controller, TestController.class.getMethod("responseString")); - ResolvableType type = ResolvableType.forMethodParameter(hm.getReturnType()); - assertTrue(handler.supports(new HandlerResult(hm, null, type, new ExtendedModelMap()))); + ResolvableType type = ResolvableType.forClassWithGenerics(ResponseEntity.class, String.class); + assertTrue(this.resultHandler.supports(new HandlerResult(HANDLER, null, type, model))); - hm = new HandlerMethod(controller, TestController.class.getMethod("responseVoid")); - type = ResolvableType.forMethodParameter(hm.getReturnType()); - assertTrue(handler.supports(new HandlerResult(hm, null, type, new ExtendedModelMap()))); + type = ResolvableType.forClassWithGenerics(ResponseEntity.class, Void.class); + assertTrue(this.resultHandler.supports(new HandlerResult(HANDLER, null, type, model))); - hm = new HandlerMethod(controller, TestController.class.getMethod("string")); - type = ResolvableType.forMethodParameter(hm.getReturnType()); - assertFalse(handler.supports(new HandlerResult(hm, null, type, new ExtendedModelMap()))); + type = ResolvableType.forClass(Void.class); + assertFalse(this.resultHandler.supports(new HandlerResult(HANDLER, null, type, model))); } @Test public void defaultOrder() throws Exception { - ResponseEntityResultHandler handler = createHandler(new StringEncoder()); - assertEquals(0, handler.getOrder()); + assertEquals(0, this.resultHandler.getOrder()); } @Test - public void jsonResponseBody() throws Exception { - RequestedContentTypeResolver resolver = new FixedContentTypeResolver(APPLICATION_JSON_UTF8); - HandlerResultHandler handler = createHandler(resolver, new StringEncoder(), new JacksonJsonEncoder()); + public void statusCode() throws Exception { + ResolvableType type = ResolvableType.forClassWithGenerics(ResponseEntity.class, Void.class); + HandlerResult result = new HandlerResult(HANDLER, ResponseEntity.noContent().build(), type); + this.resultHandler.handleResult(exchange, result).block(Duration.ofSeconds(5)); - TestController controller = new TestController(); - HandlerMethod hm = new HandlerMethod(controller, controller.getClass().getMethod("responseString")); - ResolvableType type = ResolvableType.forMethodParameter(hm.getReturnType()); - HandlerResult result = new HandlerResult(hm, ResponseEntity.ok("fooValue"), type); - - ServerWebExchange exchange = createExchange("/foo"); - handler.handleResult(exchange, result).block(); - - assertEquals(HttpStatus.OK, this.response.getStatus()); - assertEquals(APPLICATION_JSON_UTF8, this.response.getHeaders().getContentType()); - TestSubscriber.subscribe(this.response.getBody()) - .assertValuesWith(buf -> assertEquals("\"fooValue\"", - DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); + assertEquals(HttpStatus.NO_CONTENT, this.response.getStatus()); + assertEquals(0, this.response.getHeaders().size()); + assertNull(this.response.getBody()); } + @Test + public void headers() throws Exception { + URI location = new URI("/path"); + ResolvableType type = ResolvableType.forClassWithGenerics(ResponseEntity.class, Void.class); + HandlerResult result = new HandlerResult(HANDLER, ResponseEntity.created(location).build(), type); + this.resultHandler.handleResult(exchange, result).block(Duration.ofSeconds(5)); - private ResponseEntityResultHandler createHandler(Encoder... encoders) { - return createHandler(new HeaderContentTypeResolver(), encoders); - } - - private ResponseEntityResultHandler createHandler(RequestedContentTypeResolver resolver, - Encoder... encoders) { - - List> converters = Arrays.stream(encoders) - .map(encoder -> new CodecHttpMessageConverter<>(encoder, null)) - .collect(Collectors.toList()); - return new ResponseEntityResultHandler(converters, new DefaultConversionService(), resolver); - } - - private ServerWebExchange createExchange(String path) throws URISyntaxException { - ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI(path)); - WebSessionManager sessionManager = mock(WebSessionManager.class); - return new DefaultServerWebExchange(request, this.response, sessionManager); - } - - - @SuppressWarnings("unused") - private static class TestController { - - public ResponseEntity responseString() { - return null; - } - - public ResponseEntity responseVoid() { - return null; - } - - public String string() { - return null; - } + assertEquals(HttpStatus.CREATED, this.response.getStatus()); + assertEquals(1, this.response.getHeaders().size()); + assertEquals(location, this.response.getHeaders().getLocation()); + assertNull(this.response.getBody()); } } From 49bb83c0ecd7ac982da77ee31a9752e2b70057d2 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 24 Jun 2016 15:39:45 -0400 Subject: [PATCH 420/498] Refactor view resolution tests --- .../ViewResolutionResultHandlerTests.java | 393 +++++++----------- 1 file changed, 156 insertions(+), 237 deletions(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java index 700ae0efed..0494253f61 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java @@ -16,8 +16,8 @@ package org.springframework.web.reactive.result.view; -import java.lang.reflect.Method; import java.net.URI; +import java.net.URISyntaxException; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.time.Duration; @@ -37,7 +37,6 @@ import rx.Single; import org.springframework.core.Ordered; import org.springframework.core.ResolvableType; -import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.ConfigurableConversionService; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter; @@ -49,6 +48,7 @@ import org.springframework.http.MediaType; import org.springframework.http.server.reactive.MockServerHttpRequest; import org.springframework.http.server.reactive.MockServerHttpResponse; import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.stereotype.Controller; import org.springframework.ui.ExtendedModelMap; import org.springframework.ui.Model; import org.springframework.ui.ModelMap; @@ -62,10 +62,9 @@ import org.springframework.web.server.session.DefaultWebSessionManager; import org.springframework.web.server.session.WebSessionManager; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; +import static org.springframework.http.MediaType.APPLICATION_JSON; /** * Unit tests for {@link ViewResolutionResultHandler}. @@ -75,247 +74,209 @@ public class ViewResolutionResultHandlerTests { private MockServerHttpRequest request; - private MockServerHttpResponse response; + private MockServerHttpResponse response = new MockServerHttpResponse(); - private ModelMap model; + private ServerWebExchange exchange; @Before public void setUp() throws Exception { - this.model = new ExtendedModelMap().addAttribute("id", "123"); this.request = new MockServerHttpRequest(HttpMethod.GET, new URI("/path")); - this.response = new MockServerHttpResponse(); + WebSessionManager manager = new DefaultWebSessionManager(); + this.exchange = new DefaultServerWebExchange(this.request, this.response, manager); } @Test public void supports() throws Exception { - testSupports("handleString", true); - testSupports("handleView", true); - testSupports("handleMonoString", true); - testSupports("handleMonoView", true); - testSupports("handleSingleString", true); - testSupports("handleSingleView", true); - testSupports("handleModel", true); - testSupports("handleMap", true); - testSupports("handleModelAttributeAnnotation", true); - testSupports("handleTestBean", true); - testSupports("handleInteger", false); + Object handler = new Object(); + HandlerMethod hm = handlerMethod(new TestController(), "modelAttributeMethod"); + + testSupports(handler, ResolvableType.forClass(String.class), true); + testSupports(handler, ResolvableType.forClass(View.class), true); + testSupports(handler, ResolvableType.forClassWithGenerics(Mono.class, String.class), true); + testSupports(handler, ResolvableType.forClassWithGenerics(Mono.class, View.class), true); + testSupports(handler, ResolvableType.forClassWithGenerics(Single.class, String.class), true); + testSupports(handler, ResolvableType.forClassWithGenerics(Single.class, View.class), true); + testSupports(handler, ResolvableType.forClass(Model.class), true); + testSupports(handler, ResolvableType.forClass(Map.class), true); + testSupports(handler, ResolvableType.forClass(TestBean.class), true); + testSupports(handler, ResolvableType.forClass(Integer.class), false); + testSupports(hm, ResolvableType.forMethodParameter(hm.getReturnType()), true); + } + + private void testSupports(Object handler, ResolvableType returnType, boolean result) { + ViewResolutionResultHandler resultHandler = createResultHandler(mock(ViewResolver.class)); + HandlerResult handlerResult = new HandlerResult(handler, null, returnType, new ExtendedModelMap()); + assertEquals(result, resultHandler.supports(handlerResult)); } @Test - public void order() throws Exception { - TestViewResolver resolver1 = new TestViewResolver(new String[] {}); - TestViewResolver resolver2 = new TestViewResolver(new String[] {}); + public void viewResolverOrder() throws Exception { + TestViewResolver resolver1 = new TestViewResolver("account"); + TestViewResolver resolver2 = new TestViewResolver("profile"); resolver1.setOrder(2); resolver2.setOrder(1); + List resolvers = createResultHandler(resolver1, resolver2).getViewResolvers(); - assertEquals(Arrays.asList(resolver2, resolver1), new ViewResolutionResultHandler( - Arrays.asList(resolver1, resolver2), new DefaultConversionService()) - .getViewResolvers()); + assertEquals(Arrays.asList(resolver2, resolver1), resolvers); } @Test - public void viewReference() throws Exception { - Object value = new TestView("account"); - handle("/path", value, "handleView"); - - TestSubscriber.subscribe(this.response.getBody()) - .assertValuesWith(buf -> assertEquals("account: {id=123}", - DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); - } - - @Test - public void viewReferenceInMono() throws Exception { - Object value = Mono.just(new TestView("account")); - handle("/path", value, "handleMonoView"); - - TestSubscriber.subscribe(this.response.getBody()) - .assertValuesWith(buf -> assertEquals("account: {id=123}", - DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); - } - - @Test - public void viewName() throws Exception { - Object value = "account"; - handle("/path", value, "handleString", new TestViewResolver("account")); - - TestSubscriber - .subscribe(this.response.getBody()) - .assertValuesWith(buf -> assertEquals("account: {id=123}", - DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); - } - - @Test - public void viewNameMono() throws Exception { - Object value = Mono.just("account"); - handle("/path", value, "handleMonoString", new TestViewResolver("account")); - - TestSubscriber.subscribe(this.response.getBody()) - .assertValuesWith(buf -> assertEquals("account: {id=123}", - DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); - } - - @Test - public void viewNameWithMultipleResolvers() throws Exception { - String value = "profile"; - handle("/path", value, "handleString", - new TestViewResolver("account"), new TestViewResolver("profile")); - - TestSubscriber.subscribe(this.response.getBody()) - .assertValuesWith(buf -> assertEquals("profile: {id=123}", - DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); - } - - @Test - public void viewNameUnresolved() throws Exception { - handle("/path", "account", "handleString") - .assertErrorMessage("Could not resolve view with name 'account'."); - } - - @Test - public void viewNameIsNull() throws Exception { + public void handleReturnValueTypes() throws Exception { + Object handler = new Object(); + Object returnValue; + ResolvableType returnType; ViewResolver resolver = new TestViewResolver("account"); - handle("/account", null, "handleString", resolver); - TestSubscriber.subscribe(this.response.getBody()) - .assertValuesWith(buf -> assertEquals("account: {id=123}", - DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); + returnValue = new TestView("account"); + returnType = ResolvableType.forClass(View.class); + testHandle("/path", handler, returnValue, returnType, "account: {id=123}"); - handle("/account/", null, "handleString", resolver); - TestSubscriber.subscribe(this.response.getBody()) - .assertValuesWith(buf -> assertEquals("account: {id=123}", - DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); + returnValue = Mono.just(new TestView("account")); + returnType = ResolvableType.forClassWithGenerics(Mono.class, View.class); + testHandle("/path", handler, returnValue, returnType, "account: {id=123}"); - handle("/account.123", null, "handleString", resolver); - TestSubscriber.subscribe(this.response.getBody()) - .assertValuesWith(buf -> assertEquals("account: {id=123}", - DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); + returnValue = "account"; + returnType = ResolvableType.forClass(String.class); + testHandle("/path", handler, returnValue, returnType, "account: {id=123}", resolver); + + returnValue = Mono.just("account"); + returnType = ResolvableType.forClassWithGenerics(Mono.class, String.class); + testHandle("/path", handler, returnValue, returnType, "account: {id=123}", resolver); + + returnValue = new ExtendedModelMap().addAttribute("name", "Joe"); + returnType = ResolvableType.forClass(Model.class); + testHandle("/account", handler, returnValue, returnType, "account: {id=123, name=Joe}", resolver); + + returnValue = Collections.singletonMap("name", "Joe"); + returnType = ResolvableType.forClass(Map.class); + testHandle("/account", handler, returnValue, returnType, "account: {id=123, name=Joe}", resolver); + + HandlerMethod hm = handlerMethod(new TestController(), "modelAttributeMethod"); + returnValue = "Joe"; + returnType = ResolvableType.forMethodParameter(hm.getReturnType()); + testHandle("/account", hm, returnValue, returnType, "account: {id=123, name=Joe}", resolver); + + returnValue = new TestBean("Joe"); + returnType = ResolvableType.forClass(TestBean.class); + testHandle("/account", handler, returnValue, returnType, "account: {id=123, testBean=TestBean[name=Joe]}", resolver); } @Test - public void viewNameIsEmptyMono() throws Exception { - Object value = Mono.empty(); - handle("/account", value, "handleMonoString", new TestViewResolver("account")); + public void handleWithMultipleResolvers() throws Exception { + Object handler = new Object(); + Object returnValue = "profile"; + ResolvableType returnType = ResolvableType.forClass(String.class); + ViewResolver[] resolvers = {new TestViewResolver("account"), new TestViewResolver("profile")}; - TestSubscriber.subscribe(this.response.getBody()) - .assertValuesWith(buf -> assertEquals("account: {id=123}", - DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); + testHandle("/account", handler, returnValue, returnType, "profile: {id=123}", resolvers); } @Test - public void modelReturnValue() throws Exception { - Model value = new ExtendedModelMap().addAttribute("name", "Joe"); - handle("/account", value, "handleModel", new TestViewResolver("account")); + public void defaultViewName() throws Exception { + testDefaultViewName(null, ResolvableType.forClass(String.class)); + testDefaultViewName(Mono.empty(), ResolvableType.forClassWithGenerics(Mono.class, String.class)); + } - TestSubscriber.subscribe(this.response.getBody()) - .assertValuesWith(buf -> assertEquals("account: {id=123, name=Joe}", - DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); + private void testDefaultViewName(Object returnValue, ResolvableType returnType) + throws URISyntaxException { + + ModelMap model = new ExtendedModelMap().addAttribute("id", "123"); + HandlerResult result = new HandlerResult(new Object(), returnValue, returnType, model); + ViewResolutionResultHandler handler = createResultHandler(new TestViewResolver("account")); + + this.request.setUri(new URI("/account")); + handler.handleResult(this.exchange, result).block(Duration.ofSeconds(5)); + assertResponseBody("account: {id=123}"); + + this.request.setUri(new URI("/account/")); + handler.handleResult(this.exchange, result).block(Duration.ofSeconds(5)); + assertResponseBody("account: {id=123}"); + + this.request.setUri(new URI("/account.123")); + handler.handleResult(this.exchange, result).block(Duration.ofSeconds(5)); + assertResponseBody("account: {id=123}"); } @Test - public void mapReturnValue() throws Exception { - Map value = Collections.singletonMap("name", "Joe"); - handle("/account", value, "handleMap", new TestViewResolver("account")); + public void unresolvedViewName() throws Exception { + String returnValue = "account"; + ResolvableType returnType = ResolvableType.forClass(String.class); + ExtendedModelMap model = new ExtendedModelMap(); + HandlerResult handlerResult = new HandlerResult(new Object(), returnValue, returnType, model); - TestSubscriber.subscribe(this.response.getBody()) - .assertValuesWith(buf -> assertEquals("account: {id=123, name=Joe}", - DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); - } + this.request.setUri(new URI("/path")); + Mono mono = createResultHandler().handleResult(this.exchange, handlerResult); - @Test - public void modelAttributeAnnotationReturnValue() throws Exception { - String value = "Joe"; - handle("/account", value, "handleModelAttributeAnnotation", new TestViewResolver("account")); - - TestSubscriber.subscribe(this.response.getBody()) - .assertValuesWith(buf -> assertEquals("account: {id=123, name=Joe}", - DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); - } - - @Test - public void objectReturnValue() throws Exception { - Object value = new TestBean("Joe"); - handle("/account", value, "handleTestBean", new TestViewResolver("account")); - - TestSubscriber.subscribe(this.response.getBody()) - .assertValuesWith(buf -> assertEquals("account: {id=123, testBean=TestBean[name=Joe]}", - DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); + TestSubscriber.subscribe(mono).assertErrorMessage("Could not resolve view with name 'account'."); } @Test public void contentNegotiation() throws Exception { - TestView htmlView = new TestView("account"); - htmlView.setMediaTypes(Collections.singletonList(MediaType.TEXT_HTML)); + TestBean value = new TestBean("Joe"); + ResolvableType type = ResolvableType.forClass(TestBean.class); + HandlerResult handlerResult = new HandlerResult(new Object(), value, type, new ExtendedModelMap()); - TestView jsonView = new TestView("defaultView"); - jsonView.setMediaTypes(Collections.singletonList(MediaType.APPLICATION_JSON)); + this.request.getHeaders().setAccept(Collections.singletonList(APPLICATION_JSON)); + this.request.setUri(new URI("/account")); - this.request.getHeaders().setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); + TestView defaultView = new TestView("jsonView", APPLICATION_JSON); - handle("/account", "account", "handleString", - Collections.singletonList(new TestViewResolver(htmlView)), - Collections.singletonList(jsonView)); + createResultHandler(Collections.singletonList(defaultView), new TestViewResolver("account")) + .handleResult(this.exchange, handlerResult) + .block(Duration.ofSeconds(5)); - assertEquals(MediaType.APPLICATION_JSON, this.response.getHeaders().getContentType()); - TestSubscriber.subscribe(this.response.getBody()) - .assertValuesWith(buf -> assertEquals("defaultView: {id=123}", - DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); + assertEquals(APPLICATION_JSON, this.response.getHeaders().getContentType()); + assertResponseBody("jsonView: {testBean=TestBean[name=Joe]}"); } @Test - public void contentNegotiationNotAcceptable() throws Exception { - TestView htmlView = new TestView("account"); - htmlView.setMediaTypes(Collections.singletonList(MediaType.TEXT_HTML)); + public void contentNegotiationWith406() throws Exception { + TestBean value = new TestBean("Joe"); + ResolvableType type = ResolvableType.forClass(TestBean.class); + HandlerResult handlerResult = new HandlerResult(new Object(), value, type, new ExtendedModelMap()); - this.request.getHeaders().setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); - - handle("/account", "account", "handleString", new TestViewResolver(htmlView)) - .assertError(NotAcceptableStatusException.class); + this.request.getHeaders().setAccept(Collections.singletonList(APPLICATION_JSON)); + this.request.setUri(new URI("/account")); + ViewResolutionResultHandler resultHandler = createResultHandler(new TestViewResolver("account")); + Mono mono = resultHandler.handleResult(this.exchange, handlerResult); + TestSubscriber.subscribe(mono).assertError(NotAcceptableStatusException.class); } - private void testSupports(String methodName, boolean supports) throws NoSuchMethodException { - Method method = TestController.class.getMethod(methodName); - ResolvableType returnType = ResolvableType.forMethodParameter(method, -1); - HandlerResult result = new HandlerResult(new Object(), null, returnType, this.model); - List resolvers = Collections.singletonList(mock(ViewResolver.class)); - ConfigurableConversionService conversionService = new DefaultConversionService(); - conversionService.addConverter(new ReactiveStreamsToRxJava1Converter()); - ViewResolutionResultHandler handler = new ViewResolutionResultHandler(resolvers, conversionService); - if (supports) { - assertTrue(handler.supports(result)); - } - else { - assertFalse(handler.supports(result)); - } + + private ViewResolutionResultHandler createResultHandler(ViewResolver... resolvers) { + return createResultHandler(Collections.emptyList(), resolvers); } - private TestSubscriber handle(String path, Object value, String methodName, - ViewResolver... resolvers) throws Exception { - - return handle(path, value, methodName, Arrays.asList(resolvers), Collections.emptyList()); - } - - private TestSubscriber handle(String path, Object value, String methodName, - List resolvers, List defaultViews) throws Exception { - - ConversionService conversionService = new DefaultConversionService(); - ViewResolutionResultHandler handler = new ViewResolutionResultHandler(resolvers, conversionService); + private ViewResolutionResultHandler createResultHandler(List defaultViews, ViewResolver... resolvers) { + ConfigurableConversionService service = new DefaultConversionService(); + service.addConverter(new ReactiveStreamsToRxJava1Converter()); + List resolverList = Arrays.asList(resolvers); + ViewResolutionResultHandler handler = new ViewResolutionResultHandler(resolverList, service); handler.setDefaultViews(defaultViews); + return handler; + } - Method method = TestController.class.getMethod(methodName); - HandlerMethod handlerMethod = new HandlerMethod(new TestController(), method); - ResolvableType type = ResolvableType.forMethodReturnType(method); - HandlerResult handlerResult = new HandlerResult(handlerMethod, value, type, this.model); + private HandlerMethod handlerMethod(Object controller, String method) throws NoSuchMethodException { + return new HandlerMethod(controller, controller.getClass().getMethod(method)); + } + private void testHandle(String path, Object handler, Object returnValue, ResolvableType returnType, + String responseBody, ViewResolver... resolvers) throws URISyntaxException { + + ModelMap model = new ExtendedModelMap().addAttribute("id", "123"); + HandlerResult result = new HandlerResult(handler, returnValue, returnType, model); this.request.setUri(new URI(path)); - WebSessionManager sessionManager = new DefaultWebSessionManager(); - ServerWebExchange exchange = new DefaultServerWebExchange(this.request, this.response, sessionManager); + createResultHandler(resolvers).handleResult(this.exchange, result).block(Duration.ofSeconds(5)); + assertResponseBody(responseBody); + } - Mono mono = handler.handleResult(exchange, handlerResult); - - return TestSubscriber.subscribe(mono).await(Duration.ofSeconds(1)); + private void assertResponseBody(String responseBody) { + TestSubscriber.subscribe(this.response.getBody()) + .assertValuesWith(buf -> assertEquals(responseBody, + DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); } @@ -330,10 +291,6 @@ public class ViewResolutionResultHandlerTests { Arrays.stream(viewNames).forEach(name -> this.views.put(name, new TestView(name))); } - public TestViewResolver(TestView... views) { - Arrays.stream(views).forEach(view -> this.views.put(view.getName(), view)); - } - public void setOrder(int order) { this.order = order; } @@ -355,21 +312,23 @@ public class ViewResolutionResultHandlerTests { private final String name; - private List mediaTypes = Collections.singletonList(MediaType.TEXT_PLAIN); + private final List mediaTypes; public TestView(String name) { this.name = name; + this.mediaTypes = Collections.singletonList(MediaType.TEXT_HTML); + } + + public TestView(String name, MediaType... mediaTypes) { + this.name = name; + this.mediaTypes = Arrays.asList(mediaTypes); } public String getName() { return this.name; } - public void setMediaTypes(List mediaTypes) { - this.mediaTypes = mediaTypes; - } - @Override public List getSupportedMediaTypes() { return this.mediaTypes; @@ -389,53 +348,13 @@ public class ViewResolutionResultHandlerTests { } } - @SuppressWarnings("unused") + @Controller @SuppressWarnings("unused") private static class TestController { - public String handleString() { - return null; - } - - public Mono handleMonoString() { - return null; - } - - public Single handleSingleString() { - return null; - } - - public View handleView() { - return null; - } - - public Mono handleMonoView() { - return null; - } - - public Single handleSingleView() { - return null; - } - - public Model handleModel() { - return null; - } - - public Map handleMap() { - return null; - } - @ModelAttribute("name") - public String handleModelAttributeAnnotation() { + public String modelAttributeMethod() { return null; } - - public TestBean handleTestBean() { - return null; - } - - public int handleInteger() { - return 0; - } } private static class TestBean { From 95751acb334c3d35d0e57071d3179c2516bf62da Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 24 Jun 2016 17:17:17 -0400 Subject: [PATCH 421/498] Support async wrappers for ResponseEntity Before this commit only ResponseEntity with async body was supported, e.g. ResponseEntity> This commit also adds suppport for an asyn wrapper around, e.g. Mono. --- .../ResponseEntityResultHandler.java | 43 ++++++++--- .../ResponseEntityResultHandlerTests.java | 77 ++++++++++++++++--- 2 files changed, 97 insertions(+), 23 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java index 42e50afe65..561bf0578d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java @@ -79,20 +79,44 @@ public class ResponseEntityResultHandler extends AbstractMessageConverterResultH @Override public boolean supports(HandlerResult result) { ResolvableType returnType = result.getReturnValueType(); - return (HttpEntity.class.isAssignableFrom(returnType.getRawClass()) && - !RequestEntity.class.isAssignableFrom(returnType.getRawClass())); + if (isSupportedType(returnType)) { + return true; + } + else if (getConversionService().canConvert(returnType.getRawClass(), Mono.class)) { + ResolvableType genericType = result.getReturnValueType().getGeneric(0); + return isSupportedType(genericType); + + } + return false; + } + + private boolean isSupportedType(ResolvableType returnType) { + Class clazz = returnType.getRawClass(); + return (HttpEntity.class.isAssignableFrom(clazz) && !RequestEntity.class.isAssignableFrom(clazz)); } @Override public Mono handleResult(ServerWebExchange exchange, HandlerResult result) { - Object body = null; + ResolvableType returnType = result.getReturnValueType(); + Mono returnValueMono; + ResolvableType bodyType; Optional optional = result.getReturnValue(); - if (optional.isPresent()) { - Assert.isInstanceOf(HttpEntity.class, optional.get()); - HttpEntity httpEntity = (HttpEntity) optional.get(); + if (optional.isPresent() && getConversionService().canConvert(returnType.getRawClass(), Mono.class)) { + returnValueMono = getConversionService().convert(optional.get(), Mono.class); + bodyType = returnType.getGeneric(0).getGeneric(0); + } + else { + returnValueMono = Mono.justOrEmpty(optional); + bodyType = returnType.getGeneric(0); + } + + return returnValueMono.then(returnValue -> { + + Assert.isInstanceOf(HttpEntity.class, returnValue); + HttpEntity httpEntity = (HttpEntity) returnValue; if (httpEntity instanceof ResponseEntity) { ResponseEntity responseEntity = (ResponseEntity) httpEntity; @@ -108,11 +132,8 @@ public class ResponseEntityResultHandler extends AbstractMessageConverterResultH .forEach(entry -> responseHeaders.put(entry.getKey(), entry.getValue())); } - body = httpEntity.getBody(); - } - - ResolvableType bodyType = result.getReturnValueType().getGeneric(0); - return writeBody(exchange, body, bodyType); + return writeBody(exchange, httpEntity.getBody(), bodyType); + }); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java index 1b4a1b8ebc..1bebf0fb2d 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java @@ -16,23 +16,28 @@ package org.springframework.web.reactive.result.method.annotation; import java.net.URI; +import java.nio.charset.Charset; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.concurrent.CompletableFuture; import org.junit.Before; import org.junit.Test; +import reactor.core.publisher.Mono; +import reactor.core.test.TestSubscriber; +import rx.Single; import org.springframework.core.ResolvableType; import org.springframework.core.codec.support.ByteBufferEncoder; import org.springframework.core.codec.support.JacksonJsonEncoder; import org.springframework.core.codec.support.Jaxb2Encoder; import org.springframework.core.codec.support.StringEncoder; -import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter; import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter; +import org.springframework.core.io.buffer.support.DataBufferTestUtils; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -57,6 +62,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; +import static org.springframework.core.ResolvableType.forClassWithGenerics; /** * Unit tests for {@link ResponseEntityResultHandler}. When adding a test also @@ -104,22 +110,29 @@ public class ResponseEntityResultHandlerTests { service.addConverter(new ReactiveStreamsToRxJava1Converter()); RequestedContentTypeResolver resolver = new RequestedContentTypeResolverBuilder().build(); - return new ResponseEntityResultHandler(converterList, new DefaultConversionService(), resolver); + return new ResponseEntityResultHandler(converterList, service, resolver); } - @Test + @Test @SuppressWarnings("ConstantConditions") public void supports() throws NoSuchMethodException { ModelMap model = new ExtendedModelMap(); - ResolvableType type = ResolvableType.forClassWithGenerics(ResponseEntity.class, String.class); - assertTrue(this.resultHandler.supports(new HandlerResult(HANDLER, null, type, model))); + Object value = null; + ResolvableType type = responseEntityType(String.class); + assertTrue(this.resultHandler.supports(new HandlerResult(HANDLER, value, type, model))); - type = ResolvableType.forClassWithGenerics(ResponseEntity.class, Void.class); - assertTrue(this.resultHandler.supports(new HandlerResult(HANDLER, null, type, model))); + type = forClassWithGenerics(Mono.class, responseEntityType(String.class)); + assertTrue(this.resultHandler.supports(new HandlerResult(HANDLER, value, type, model))); - type = ResolvableType.forClass(Void.class); - assertFalse(this.resultHandler.supports(new HandlerResult(HANDLER, null, type, model))); + type = forClassWithGenerics(Single.class, responseEntityType(String.class)); + assertTrue(this.resultHandler.supports(new HandlerResult(HANDLER, value, type, model))); + + type = forClassWithGenerics(CompletableFuture.class, responseEntityType(String.class)); + assertTrue(this.resultHandler.supports(new HandlerResult(HANDLER, value, type, model))); + + type = ResolvableType.forClass(String.class); + assertFalse(this.resultHandler.supports(new HandlerResult(HANDLER, value, type, model))); } @Test @@ -129,7 +142,7 @@ public class ResponseEntityResultHandlerTests { @Test public void statusCode() throws Exception { - ResolvableType type = ResolvableType.forClassWithGenerics(ResponseEntity.class, Void.class); + ResolvableType type = responseEntityType(Void.class); HandlerResult result = new HandlerResult(HANDLER, ResponseEntity.noContent().build(), type); this.resultHandler.handleResult(exchange, result).block(Duration.ofSeconds(5)); @@ -141,9 +154,9 @@ public class ResponseEntityResultHandlerTests { @Test public void headers() throws Exception { URI location = new URI("/path"); - ResolvableType type = ResolvableType.forClassWithGenerics(ResponseEntity.class, Void.class); + ResolvableType type = responseEntityType(Void.class); HandlerResult result = new HandlerResult(HANDLER, ResponseEntity.created(location).build(), type); - this.resultHandler.handleResult(exchange, result).block(Duration.ofSeconds(5)); + this.resultHandler.handleResult(this.exchange, result).block(Duration.ofSeconds(5)); assertEquals(HttpStatus.CREATED, this.response.getStatus()); assertEquals(1, this.response.getHeaders().size()); @@ -151,4 +164,44 @@ public class ResponseEntityResultHandlerTests { assertNull(this.response.getBody()); } + @Test + public void handleReturnTypes() throws Exception { + Object returnValue = ResponseEntity.ok("abc"); + ResolvableType returnType = responseEntityType(String.class); + testHandle(returnValue, returnType); + + returnValue = Mono.just(ResponseEntity.ok("abc")); + returnType = forClassWithGenerics(Mono.class, responseEntityType(String.class)); + testHandle(returnValue, returnType); + + returnValue = Mono.just(ResponseEntity.ok("abc")); + returnType = forClassWithGenerics(Single.class, responseEntityType(String.class)); + testHandle(returnValue, returnType); + + returnValue = Mono.just(ResponseEntity.ok("abc")); + returnType = forClassWithGenerics(CompletableFuture.class, responseEntityType(String.class)); + testHandle(returnValue, returnType); + } + + + private void testHandle(Object returnValue, ResolvableType returnType) { + HandlerResult result = new HandlerResult(HANDLER, returnValue, returnType); + this.resultHandler.handleResult(this.exchange, result).block(Duration.ofSeconds(5)); + + assertEquals(HttpStatus.OK, this.response.getStatus()); + assertEquals("text/plain;charset=UTF-8", this.response.getHeaders().getFirst("Content-Type")); + assertResponseBody("abc"); + } + + + private ResolvableType responseEntityType(Class bodyType) { + return forClassWithGenerics(ResponseEntity.class, bodyType); + } + + private void assertResponseBody(String responseBody) { + TestSubscriber.subscribe(this.response.getBody()) + .assertValuesWith(buf -> assertEquals(responseBody, + DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); + } + } From 351e834716de4d79b9203558d33e30531ae1efbc Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 24 Jun 2016 18:03:56 -0400 Subject: [PATCH 422/498] Polish --- .../reactive/CodecHttpMessageConverter.java | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/CodecHttpMessageConverter.java b/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/CodecHttpMessageConverter.java index 75c56cdc7f..1bb4400df4 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/CodecHttpMessageConverter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/CodecHttpMessageConverter.java @@ -40,6 +40,7 @@ import org.springframework.http.support.MediaTypeUtils; * * @author Arjen Poutsma * @author Sebastien Deleuze + * @author Rossen Stoyanchev */ public class CodecHttpMessageConverter implements HttpMessageConverter { @@ -51,6 +52,7 @@ public class CodecHttpMessageConverter implements HttpMessageConverter { private final List writableMediaTypes; + /** * Create a {@code CodecHttpMessageConverter} with the given {@link Encoder}. When * using this constructor, all read-related methods will in {@code false} or an @@ -89,6 +91,7 @@ public class CodecHttpMessageConverter implements HttpMessageConverter { Collections.emptyList(); } + @Override public boolean canRead(ResolvableType type, MediaType mediaType) { return this.decoder != null && this.decoder.canDecode(type, mediaType); @@ -109,6 +112,7 @@ public class CodecHttpMessageConverter implements HttpMessageConverter { return this.writableMediaTypes; } + @Override public Flux read(ResolvableType type, ReactiveHttpInputMessage inputMessage) { if (this.decoder == null) { @@ -118,16 +122,13 @@ public class CodecHttpMessageConverter implements HttpMessageConverter { if (contentType == null) { contentType = MediaType.APPLICATION_OCTET_STREAM; } - - Flux body = inputMessage.getBody(); - - return this.decoder.decode(body, type, contentType); + return this.decoder.decode(inputMessage.getBody(), type, contentType); } @Override public Mono write(Publisher inputStream, ResolvableType type, - MediaType contentType, - ReactiveHttpOutputMessage outputMessage) { + MediaType contentType, ReactiveHttpOutputMessage outputMessage) { + if (this.encoder == null) { return Mono.error(new IllegalStateException("No decoder set")); } @@ -139,22 +140,23 @@ public class CodecHttpMessageConverter implements HttpMessageConverter { } headers.setContentType(contentTypeToUse); } - DataBufferFactory dataBufferFactory = outputMessage.bufferFactory(); - Flux body = - this.encoder.encode(inputStream, dataBufferFactory, type, contentType); + DataBufferFactory bufferFactory = outputMessage.bufferFactory(); + Flux body = this.encoder.encode(inputStream, bufferFactory, type, contentType); return outputMessage.writeWith(body); } /** - * Returns the default content type for the given type. Called when {@link #write} - * is invoked without a specified content type parameter. - *

    By default, this returns a {@link MediaType} created using the first element of - * the encoder {@link Encoder#getEncodableMimeTypes() encodableMimeTypes} property, if any. - * Can be overridden in subclasses. - * @param type the type to return the content type for - * @return the content type, or {@code null} if not known + * Return the default content type for the given {@code ResolvableType}. + * Used when {@link #write} is called without a concrete content type. + * + *

    By default returns the first of {@link Encoder#getEncodableMimeTypes() + * encodableMimeTypes}, if any. + * + * @param elementType the type of element for encoding + * @return the content type, or {@code null} */ - protected MediaType getDefaultContentType(ResolvableType type) { + protected MediaType getDefaultContentType(ResolvableType elementType) { return (!this.writableMediaTypes.isEmpty() ? this.writableMediaTypes.get(0) : null); } + } From 0ff7df8b5c1dd66673381b6c8e2d2ac60344c887 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 24 Jun 2016 18:14:11 -0400 Subject: [PATCH 423/498] Improve default content type handling We now also check the default content type if the content type is application/octet-stream as we do today. Uncommented failing test that now passes. --- .../reactive/CodecHttpMessageConverter.java | 10 +++++++++- .../MessageConverterResultHandlerTests.java | 9 +++------ .../reactive/result/method/annotation/logo.png | Bin 0 -> 124279 bytes 3 files changed, 12 insertions(+), 7 deletions(-) create mode 100644 spring-web-reactive/src/test/resources/org/springframework/web/reactive/result/method/annotation/logo.png diff --git a/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/CodecHttpMessageConverter.java b/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/CodecHttpMessageConverter.java index 1bb4400df4..02a9cf7c71 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/CodecHttpMessageConverter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/CodecHttpMessageConverter.java @@ -132,14 +132,22 @@ public class CodecHttpMessageConverter implements HttpMessageConverter { if (this.encoder == null) { return Mono.error(new IllegalStateException("No decoder set")); } + HttpHeaders headers = outputMessage.getHeaders(); if (headers.getContentType() == null) { MediaType contentTypeToUse = contentType; if (contentType == null || contentType.isWildcardType() || contentType.isWildcardSubtype()) { contentTypeToUse = getDefaultContentType(type); } - headers.setContentType(contentTypeToUse); + else if (MediaType.APPLICATION_OCTET_STREAM.equals(contentType)) { + MediaType mediaType = getDefaultContentType(type); + contentTypeToUse = (mediaType != null ? mediaType : contentTypeToUse); + } + if (contentTypeToUse != null) { + headers.setContentType(contentTypeToUse); + } } + DataBufferFactory bufferFactory = outputMessage.bufferFactory(); Flux body = this.encoder.encode(inputStream, bufferFactory, type, contentType); return outputMessage.writeWith(body); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterResultHandlerTests.java index 3f66528315..e022bbeee1 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterResultHandlerTests.java @@ -48,6 +48,7 @@ import org.springframework.core.convert.support.GenericConversionService; import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter; import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter; import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.core.io.buffer.support.DataBufferTestUtils; import org.springframework.http.HttpMethod; @@ -90,16 +91,12 @@ public class MessageConverterResultHandlerTests { @Test // SPR-12894 - @Ignore // GH # 121 public void useDefaultContentType() throws Exception { - Object body = new ByteArrayResource("body".getBytes("UTF-8")); + Resource body = new ClassPathResource("logo.png", getClass()); ResolvableType bodyType = ResolvableType.forType(Resource.class); this.resultHandler.writeBody(this.exchange, body, bodyType).block(Duration.ofSeconds(5)); - assertEquals("image/jpeg", this.response.getHeaders().getFirst("Content-Type")); - TestSubscriber.subscribe(this.response.getBody()) - .assertValuesWith(buf -> assertEquals("body", - DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); + assertEquals("image/x-png", this.response.getHeaders().getFirst("Content-Type")); } @Test diff --git a/spring-web-reactive/src/test/resources/org/springframework/web/reactive/result/method/annotation/logo.png b/spring-web-reactive/src/test/resources/org/springframework/web/reactive/result/method/annotation/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..10cc93fbcf9e2f97db78ab6f7e32c1d07f61108d GIT binary patch literal 124279 zcmeFZc|6o@8$O(+K}}&4Bl{34F=L7(yDX)YN{TFlp|UHolWk;dktJKDQVAjZ&e#V@ z*_Z6H?>qBeL-q7L_uc(G@B9A#{Cz&Tjk(8nzSngg=W!nAabCmk(naNcdk*c{wQJWt zRTV{zUAriP$zQY(@Qi!07;)Dw?p>;ia#tTvOq6cgvirJ>tS*~ikyL8pyQI&zBqY-a zxN5LRzkDw!ad){t;AF(2VU}XURrBs^-PdkybhaeRr(1eYJ>SEA^xCqx&J+#w?R?(c zgNqZV0^=8q+Es-*{AfRi7@tQR(Foghk1DcidQpvvd*#4hDvxl2YvEv+$}5exU+(km zDYY&=w*KIWyIjqYkaOUoGW?ue{byf9qWPlUl=_>LAAZ- zuGgRpM)Sy+5z+s)_v7hpUVUda8kD7Lrkw{V_N*Elsy|EK{l%oq)%4A#!gH~Pz5PyA zMfDgapZWK=-y`z={IB?vL+I!f8MEij*CcWvkNsPK$6@sz=sjOaxs{~Z_@z)XcCvj* z<8{8Xq`_TH?57g`KVSDJ8f3{x>*3^2q`b3u6s)=K(eF4Ucn;it zI^uU&rwRYsuYwy`A3ieV98It){mEhy2QMXECj(OR*R{w%Z8`kw_sPgElv2C6`^(rh zu>6RFzph2b{r!J|Ik{-QD|=-s&9cc2WWx_PtO6K z6qWaWIbJzlE7`d7xRZ?M`C8m8ME?~G}5ey{$CV?J4zpvo6!U*BR!iW?V9q$~k zQO4^$YfqCQtIsL(r;6gc?Ry)QqJD)1?zhVDNO&XuXPW(X_nA+By{eUiN>QEw4IX8> zZtRX@RpBEXgmXg~D5?Csa*m>dM?-O}+%QZOtIC3^&XXt}KTcJh0P+t6CD z=EvKC3(`UvP-wm;0fX&3r}p8YhCCi+f^)+V+rL7-Mp_v$STTq%oP1A;#9ap91tD4n z5Ex1UmNAdU9~7T`MZ-0G{*`aLVCWT!OvfhxOU7q^VI6QC=GHHKYbE)SoMeKwY77}D zh2L_fiS^erSq46^CG7Friq85PruV=ZH252nX+?B2*~GgP<}+Ft9M97@6t_FjnqxKTeMvT#gscPue3# zR)`UmrY6M5!YD;)eHL~M%W@NcaDF4AaYKBo&V z*mQ${fj#k)wr1T10RvsyBM>&QspRP6$-!bogP5ErZyp&JL|K>=ECMa7@2(g>ijw8? z+J;5LE)N5h7s|MUATR(&Ns)gaegG~4ln++6i+s(WkxL_P{|Jy#2IXtwP22vzWEgH= z`aKdvm)CFm!Z=p>AMdB{UV;QqQ}4VT3LT6Dw}rrx;h;bZt|EYx1#g6bz#z099%SI* z0B)t}0N!K~JV+>jnPU782*9~f`N(oD>lhKK6{McyjDc9~;8odVU2>v-XJYXAme(%} zirLD4CiTC#CqF!d;XK|;R*1=kvV2#Xnvy)XCYs?Gw27COpW&EdkcU?qVKfwP{6d*K zoEE*t7Y^rkmi2rw!}CInWWps0p3_TfXWsVN099HWNQdX#`& z#zpQzE2_Py0_j5yu3mz4^pbTCVW2hOBZwxbbC)pCDiW#tVM;+-2p$-JlL>bkfkCQl zSDGS0F32|m0nW`zm-bXqjd+}hJS|!oan6kp z!RZ_sAS!Xt>W!|kGK__#-MnJpJf@r_JApXZj|YDp)~^rqu3W;22UC&P@%0 z(OF+3XD%aJ7Oap=L4b(JJdktfCm&IgxnRK$6h62QpQD$S!8R4pF_NhS#2N4zASVM_ zGDZLc?Fs>a2EqTyFc4fM>*LYTpS0pdA+OIJ!f*_r1FM4IAzPF2<2M? zKmxEvf@i^1x8II@K{z)CtMWqqCzyZY4x|%6-i?OLeJ$KNyZ?XvCYwEnyEnft(E>y9{N`j*GGC#8@WSULGd7b z#T{cP4jW~3>3>InrOCUeJ=Nk*lkcZYXxzofbl!`w8-hFbSnI5g(t_VH1{Zmq5Hht8 z?)1Yj2YG~f07FqigS9S@3FbUsilPu>1OqK$h;S6+r^Ro5_Hj-az@HEk>^lD%;SOb! z0AUX+3loALz()%~MG$S`Wnf7Ql&jix5c&u^n6H#_*9cl|Jc>o1*v<3dSxogHHO?3? z9&VHo+(eEnCMrkY!aBSu1hqADg?_%65rrFOBiuu9-U* zCNlhZKY(8!P)d1r-VW4DFN(*bi~ywa`fvb7!1X`47uhEC@t~n1f}Yj@a!ZaWrNvV+nh_XM^C%?R=1}f! zL3K^-ug2qTDz9^DY6h$Pqxy6-+tKLBiYux1*u4HsytQlJlcoNRW65^ehJwckM=9|K z<+%X^TYG2$Anyr5>Wjsg5br-&ahnHUi*16wuYs`iB*PDL1AZv*7`b;@VgI?OAZB1i+#v(F0LBOPKo||wq~_k24-D1;t3~Pz)MjA{K!otp&?%JPuSiFp(7Dboxc(bWCGwT-J zr|;cOyz5;ol{FKjx8AgYjuq;oy%X<%I89nht6)t@^COALEjArx8~SurcCuQo?30yF zn$Vtot&B8BrFGj)Ku`8t2XOb~FNK^E%l^q{_{7JHkAWsXn>EGWUSK{_-2w?e@~o+M zJ)pmk-t|i^sd9GWR^JO_(wuM8by7dq3mHp&+j2o$u=Z+rPeoBXS(QzS#p(}dj)7I#cDw!cZ7I) zJx7i0fie)!R=zp5e_~PM(*KH0k7O<`;fb7H>wMPA8y_!-2b%a!J2rlDn#y-tc^*`h zJyU_vjq-kt%KzlD#xl~`yD}Wavi03>z+dFv-l+DF_fHdxSqfdJ+E*n~7<-rL9=ZqE zn1W&@qXK75NYN5wVNDQbZ)3B1_K)1M-&Dc&&`kj{S}8lFrOy^|-NjBMKEMAMwB`G= z%tvTD?GEmZD#)^r&XT%wk0n;d1+0E^2g7GKAxT#&^r;dHRFxl zT)qA@waB@sIg5P+&C&Bae(&oelKc5Zd@>6!P-_aS`~?~NuoyQ+9& z*+6q=1maiL-rivEx^`zgHSSE$gDqc?6S~+_=xCPt#A3Gk7#BNfjOS10TXZj1en<31 zjy>dYEc*n%FUl^c{=cJ1U)w$qZ`pgT>oaV%pl_>XeQ&nR%<~W@i}uIs-Ay6HO;)+q z92`Y=%7tQPJGYrku6nl-HW}Rwxhuq>3*Fb_{d6VQCpN~-kUyaPhSO@LT5ee0@C#@^ zP%L(=w&;mo$x@H>Mqoy1Mf|cHTJHQ*Y&klT63_P4-!_fq1LgqLzUk_|E-}94tml)w z;!yLw(cMeI@U6|DgEFh_u?>mYIqgn)mB%TeNOHSUZ4wo$Jz6xAUB$e$U>S6L>#JEg zB5!b5JzLk`wdaiZGA1L_>UF)+tV=vMEhtfA*X}64^LpNQT7ChSyDDsW(Pz_107%9E zu}nQnmQ-#lO46!6@Ui=8mP=97Eg@#nibeqVPa}2`2NR#Mv6+hsp@hR5%P@7raZfG2 z6gw4IFAU93ikJBqvwpt&abhFNW_DA*=PmqnTm>W;N9EzcG@e$q*$i71oC6cKx+?F zknQ{}rC6vGB{o6W&|-VfKD7Ri#mysY@(^opQBuyUBXbVro5{=cy;~(T%2T<<=a+Wq zikgE8Xhgg;Iykrl`qW&~&MsyBi+JGiGmFV?x7qJvBkQ9(OyU9?;x}DSl>c^LjOx&qcr8N=g1VqWJe z)EBmq0hf?(l~=D_zr$m|<|4DkPE5_jCPc@KPF;%oFLw!{y|hjDPq*owyVEXUFl+iK zGupv?QmkFHcS87-H?i78T6zwE?lc*6ES*MLSfzMY6*$YW!BM4slA95v56H#qq@0x% z=LAVTAtN8OL+qs3>{7CW-Rs&9;ZxE*uxUpfZmKfWwY%TnA=QJ0N7UoY- zSCHNNRN$GzHr=M1w^mmNEK3d?x60ouW;fo_wL)XTn&;BW;oayw+VWgOI!Qjkd;xbD z3rEpG)!tW|Ja13;9&v42n>TUm*r-+4Gmk%w?z~kE9e1vLFtEU6aBr>VO@;Q4Vz9l5 zQq^W{#$W2)o5aO?jfsrw%WI%i{aKB(Rf29ajr|jI!0m*cnvm{dVm-gqU6Fhtq~3FV zTKdFBh>t6LzHX`2}x-b~^#|T8Xcc z>$KZS$;`Cm?(VM^o&1hOz(%H}im zU7!r?FH)xbw~f3*W*U$)qTC54VxpDZ(Dh}t4{oV{Cg%ds4U!wW7wWPAl!hIqvD{>q zHtW51FKb#j&^{w4YJY;bWzDeb=If2Xph6K+1%1zBlDu@CX}+O5MH%oOQ%Z7Fgv;47 z{Fj}FwYUjj`O$me|45?KuY1lY{> z?adS*k3s&vmGEF?2~Cp+HqZiHHHEpU8e1|GXezSPLfC9_vWHGOD!bawzJ8v3IU)LT zbl>{a(TxrPnNtPhg`KwXT@yxjSoE7_6h4v~!Y_gISZA-V#f{h&p**-32-szmsc@&hG&&tZ28$n#h zTrJEL?X&XDFn%FO+4mUGY)8~!pLKf!J|``#UYQh;S)Lz0FL2R*Sn|fU7~Z@Jk8|L^ zIe5H(N5+8icgAjk8*LC_Ev_I>^x~-O7TSiuH1!|7l5{NaFHkVbG_b$GcC*npH@*0C zizo9Rd9%58vYPuhO*z{;uWQ+jrm)#aPC2>t&2za~_RX*IWS!`MKvW1sS%M;Q6fIl1 zhnF9N-(g{%|0vpiLfFFG1b#^Lk>qrB z?*r1huiNI5dHZK(@%3r+!$1#;{yl&SFy3IB-FKleOZ{x~-6KVIX0vQ=%Z>E{Pc^iV zr^w)4Xzp|P_q*gW=AS@LVQ+{r*}?5ICVJ7OTZK%2F61J{I%<`B7@1H_0-A6LEvw5R z%DZy&UQK7@fM{epYF3X`p01{s%pMY#6dx7~f||4q8^x?o33rkF^7oQlub}4H+VK|r zEw8Ged{AmQcQ~BRBaMipR*FX=;k8Zpr%Q9an=fZq#9cqHOxs>SoE}VQuFtR)5o@e3 z{A5}mZ;__^`yOxK{jgEUXtdvHyyT6g=_q|t58a=6>;kZQe)GaXSAD2iX`UL)gv?Dl zQ(j~k+FKVym)G0D3mt65mdC}1>^Fv;=Jhtd9aqq+9)X+R&xrwBtWybS0;5z0rCwz} z(J@1;879pW{957*HXKJ5-xx^DIR=Es$}A3cZ2(k=jSv2g<;&Qz{UF^q>98;M4_pj) zA*Y?mpVKb)C@&9rkAR6m?L&cy0vzshNnr+D;U}z5M!}C)BzJC zeo5aDm=fMtqvC@1Y!-3k(uhe$#j?sJohPcPz+E8KS_S~wyF5HX7^EN&h>x1Wji#{@ z5#4d;O^}f?E>k1PZc^5-HxHFZiKjCZZ7gSGB*q6aYYFTq<{j=(j*6W|P8od<(dQon z-`lgv^@IPbKkG+_-JHy9?1CtoAx3nzHGznE5yOf%ub^^=Y)JMpaHlxMrS;W zNooBfl}h(a^2_;!jRAoi12{)u!2?u66;f73ACjEKA<5El#f{V$kXR(yeWs-rDf#7< z%UDhBJh*S{@AsXS5Cn3vZSWV`I|16I{sHZK1~8(UII^RaR)&9!%vGh_TfRUd3kuGJ(>@has7D~0MDs_Qxq<5Z{O{-MOwc+AVONptw_ymaz-gz zY+W7i0yI8?b!*{1{S|ZRM?(x)+&4(NR_%*HX!LG;oH(Fza*yCbPJ}<2Il_k9LpqpC zzvzxR59@yj?43&M1c!&99TofFvP>&!C%LMotDSctOZX#EY3`82H&5>%=dp6$Wpl3j zF7dEtWj-VoS$&8qk==0^mKzBLj%nS&^m_@A#NxHC{x5o5ccrC!KfZ~)F0K73soN)3 za&b^+{mT^|&$%D%P0&?`iCN{S?bHpg-y!!UP~RK>Ozsc=ZVtmT=#}~0q02OmTZ?4T3i%(4SOkGwhY-7^Lz!0UxISPBwkcWMt1fb}30+MPG%6mBU= z+jM?<-EGf2RWR?W0i_flFTO(4tPn=o5iqth&rWVI=pDR(M6_NfoDCSk#;HoKRqmqnI+= z2Z!61Kk<^6@z<*L?NVW_GwoRx@}P;4$pzzW;2vy^^3Xy6)0JlpUqockI=@X*yid+; zjvdG{_N|-}JfLh*AH-r=9U#HWnw86HvK+hWWZ!f7fqr!K>I3P-PQFpQQe>#=!tW^p z(qaJ@*`9;3u;-uFu{jR;9rlITz{Nlpda1Ia zzcSYT3%l9hk&iO4%Ys?5vxcm8m_zdXlEQM z&bVYXEof$K$=QK0sDPFiTWfDoN%ltcx^!lBO`BPxx$0><%#lPdqrXd#N>M5%DgHIZ zpeA(KE2-X(&FZmcE~`!gH}xFM(E2$F>uv-%r(R+YBss$CBdhdT*?#p;EO^-+H7&*7l%i1GaJmvXBCEFrg8Q z`nXdFqcz6Qzj}%DkKX&%rL!C@5;MM0LGRWgJsTR) zX#%e)3PJC6+p3RhzoPE5_kG9@E8Ka3+iio)W9F6q5>P6_g!6w{-p&Z6U*kt5!5jo_ z0@!Erd{=J2X?g#waQ$sSWhX!Wh*5OS-7DF*2ligLi$5Uht?rUDqa9ylGI%ALqnj%4 zmbh<%fO|GW}dtkPIk|*sdBdg9R%R%azy~eXa~B?JxC(jI}knvTCba20)Y6 zsL&GbKrr4sjz$D(jQ1!i7zwNO$b5$C-iZ?g#0lSfqZ;3RqwOk)(>$im=l$*S>F&7v z+4AiD-J4CF^UKAg%C1817z;CdR(|Y4q=(mz;QCc#ex?u+O05*sWC+-e0p)2He%*v+ zVx^b;?$%JN+vl~LY{RkaxuaL-yNG)OY|$b@u)@#%^}p&T=O9_qNcLihH9YvZ?WC&X zk~&dZNf->E+^zeDe#8?y8wws@+q&DG;zMJqJvnc->E~{#ROA1aeaB$9jv_jb^69iIuRMZii z^1vv?Gomp*JZsctqbMqHl7mWNfffmk8fAhSKX1qOIp>xBBdOxr z+BIsm5k?h(OvV#N<{@Fn>tOe_SZW~o)(f-J>zun3>5OW25uXyiHwp-1A9XNfwIh42 zwQ7$oe+w+j@|_lu{@`$H$zS0WQGr#?6nTUSfzc47#Zwr_cDbvzxtF--#QCvOmAT_0 z!zjRk8iqqDM6X`Q$#W|aZp${yWimQro9`mNJ~3-dZjFzQa~()ct)IL5?(3Z|3c5Ngv9H+GREtxb$qwsd(|X5Io5XrC*LvM*BqNo96RHB z=dosU7?p>tP9x|!8AJVO2{1;qO5|>So~W~wVJuT+U1!m5O$&>Qo5KfNO;~$$iC42& z>Po|%jZ^AmPfdH&^R|EMxscn`+aB&VvvqvcxAt(iz()_9ADm55kpP7-g_P>2$yW0b z5uRs>2p)tOts0S_Mo?5#gBW@N$-hsK%FD-MG`^s9ZaF^p(`u=$%=ng}NJLbF@08A( z#XxPUYg^24>(!*MZ(7H@j#gb%x7nEB3(yRz)!)y=)QsjhDi^mI#NMA2#E!{4fKTI3 z=BGA(6WIFZ;+Gq5tK#3y6W*#my@0;TOlM66@uJ{Cs=OpzXy*T2y7J`gi*1i!H? zI0FP{fDrDhl|iT>gn-CM6gL!yQ)OkSgNGvh@GD;zVnm$By&LqLmodwovG|ms0NwST z1xd?|#RX&63z;_q919zgZ6+4@sv0jgncja(?O+!CtZ59X(U9TR$g?D4gZrw1tbF?M zDMQ%v)4B<#FPpsLbbHspBp`4*i+Je3x@R3tcphoWObF&Zk$bE^>{1WM0Ra1zVVh}>qV!tspy3OtEDW}TMO^O)r?XqJ4F z)cdBn%0;V{8=cJ}jfKrC70#)vO3^_Yzr}5Wc%T`5%g6H!;$-*Or6VKl#Ul*sToQyk zN8pARZeF0Ry~L4vIrdG4Mv%<4k{>p7XIxO*QuPuxG zu-*w57nfp*>}jazcHo}vGw5U+KE40E!w_fJ<+i1n%?cW^)H+w|H(?Gc4?pRHpw&V= z$bn)*(y=kX8CuaOLs>WjW2h*%MQvoHxmSt%vtUmG=qSPR8!;*$3@DO2_flfgkYdY;5LIr zQRYzDvM3CTJS{3J$~>0bC|ZyZ_SQ@!oJM@)nC&fkuX)b4Gmq(}y2H3!^tHE2)y-;T zPF(#gz}Kzxk_DC_tB;JtK_ULT<@pVmS%omnbc|_o`WSvJf**kqh4X?lHfm);D1QFE zo`=KgWW~MCN0b-mtY}V;ixf0D4Gr0sCmpKRZFu_qe6RTl5tnRKqw<@d++xBn;W|u9 zCKU;FHJ?f~-@a0IJ(t1W3{r@~`|yXTmX}|>odGgd50ihFd|JZB zoVB#_RR5fMOc?!t051)E$3k%SHY8Jh9$Z5O^6q?~-N!~CDQhw;0hDD)UgJLDluyhR(8Z@RX&rkVOt zI;oH2m9w!E3f>hX(rABctlr6LUWGr${)+2@hxsavOxj`6rQDb5MlU;Y3-X98Jn_B} zMv0(E-~%*?XQcB$qSzSrLLs~uN_@y!VBzu`Ao&eo@_f{l?hN{#gXaToklcDt_iiRl z>rO~@)|sRnR9OEyt>ZFXUz}_AB7QUcl)V2*>6vi93)y*BAD&rXX2*a6 z@i)aG^nPI6)4`j~&?jh+AfAfngbZaBp)m3z=g569I=V0#d;KN(>W%IAIfD3kgZKp> zG37IJGD;cvaP`tuDTlW?eeZQ={rS8Jmb#sSSMt(~0S?Mr4n^n(3J(fvDJC?aWk%Ey zs{Yq_zjh^BD9)Hj8Cq>D)%kSfvv{L@qszMdG+XTZl!NjndQMrhOLrSD6j%Li@T%ad zCvI{mJ)~S;VmBhi{VgcV&sdKk>tD0(5&diK!S+JIDbrd{N$k?YsN(JLrR;6@48DtQ4a@P}!}MhH>35HoF**A1f& zI5JH`Jp7^}{UUL=8ht!CnTevyAVfwA!7zR(ZX}$c(g(92D|y0o#b)~LbnzFbu{rjD z)m5a^H-~D_?{*uqwgMcx`*e8w~g9h7yL>uMA@bFmFLj1fG3GOO>XT?UISuTA=|Ehptj9!qRG&&yX zwGTv$4fTsg!%_TLHXbCPQxIrIHtJQOdyX^x+I`>S3mD^^zsJr@X20%!^{j4eW$cv+ zTXJiWc=s^>FEWi%y%_V0OkXtJxk9&RqSH2b)9G)>lu3Yoo=~HjQtt(&VJ+cKtLEon zu6_xQ6G!8;_*fZGeuuOCj)-)5ra!akJsldl+K(I}D;Z9x{0VFalx8=mAk|w^f0LGep}rE1cOHM& zthSOU_hls~CV@Ky#)b7%l<$WC?kTy2a^f-C!l;0Frdkd(aKpPMphqa{7Ry`yo4GW^r5EC=y;manYhX;S5I|VFR!s zi&923gOBU%m{6n4dPYx1uiFvlqUdY9t+vyjGp>sOdi@R&zxp3K-7^2;KScOfHd)4Gc{T0=? znQ5cF2$WO^enu23+KUfUyVqWyTs10!L46F;>$$m4*7%oAhM|vCgX6Z?j zCNx`SMwjAucdHWJ_W?8lb%WX#IpP>hrTdO(6*5#KiplHCc7>_7b0frXI0^#|2&goB zZj?psFJ2p7ox0Ln1dsb}^i{}9SjTaAOgo}Td{gXi1L{se2Lj70=i!o1pKkeg)YDI< zo;agCR?!1S+v@a3g~UY?V4W`&)i8d1+eO4VDA)gWYN}YT5e1|TzX3RjjaQ-d@@se3 z(nrD>ne_473`^&Uq6-BFkC6~+te+oiR2beNo-xu3sWoVe>@{9ej&KwpO-1xwnI>8d zb*NP@+?}+4trzFCTzfuaa(LW)p691d-YFL;o^M=Hxpdd{Th@K7bGVf*f5jH~CMx5W1 z{l4y+tD94wx{RB8U-6zjc~w}Boi^jo>Q$?fOuOWs65<7V5wftY#y(>!8^2y|S+Ee; z80&_1o*Y^}GvGX2sMZ>AJwCT1Z^0~~-uKI$lVgjgvukHFuEdoLK55Dm6rsO% zV{xY(A`=n?qAa|x*0PrTEU6Z?Vj#f%*x!*o?ebF|9e)SQlIohe=^bOUY*D*AzFV^` z-SEV+rF53?y5%DYa|%l2b}9XzQx|abVgADZ=`7UvemQ1X2X0WK~>2aBLu|fl+;drCHM}5ud8|=G|4Wru| zyJL$7=%sZF+{Vq@Wms8>&OFhx!TZRRY*E5zCaVv;7XsP;0nMp<&JjaQ7Wl*^4ZkqYV8d(*k4k)(mhy9 zpFvQByyRdY8(myTuzmPpcwQ21jvyR5B@{-J&HQjx*silZ-?GtsZ7_UeXK$XcOsn?*eP(^2)p>34nCp10(4-g~EV*eynB0#(?ue`tu7 zaA3&U>*rYjMo$#@d@Dwcz|G9cl!j}Q2gl12tAa!tS8|J2y-7=!db4{AV^WOr9{oj3}~V^P7y$a&?Iosx&+{L-`#&t5 zi>=VN(l&V3eV;$Z-mY0itWEl9=YtWSE|<<06Pfk2&UAV+!ExzQ*?gMHNH4j^s*z_Q z{*h=?0x=FT4F{dOeLPt0g zQ0PXhOWZt>7MTD8?fL6}-VNYZZtuoWxf!otB%ej)kH(n}N()tacIJ~u6pJHKm-hSk zx_Oqh$ur=6NNazwYFnaR+NEJoG;V>4uW^}5+2C-SneE@ywyfK#sn#Ff+m81&LjuFY znq$ZQ;N{1H#)~Z12%?n)JR>C#Y&*Rc1o&j2AZ(8T$ekFO`nMeuTW_I0HD5iKk&2nO zCazQH*!NFY_;;ng z>54CK7QpC0`EWHRIt*yk5CWpp9em9B1|K6MTun`-KpiaGCu&{_E8?}VxG8q@KS+S- zm98&l{}tgkISzdJlPO@-|Bn5Ye8uG*&*fuf((sAt|x@ZG=R#2>^ytSMn_@rY?85GmFC8g`(R@3qPBTk6A0 zQ>RNnExhrU(a0#d=%ddjd`*BEQBajZR0(|HXl^9<^dk!NXJ)}FD%%SFUl90cZGt?t zrjHCe{)Rn^V?jQ>9WSbI*gM*?PSbUIs;qeHy8Xeek2?L*rThrU|F8593a zN|BzSu8Qo9j$;u~);#lZrwsBHk}@H@S-kzCKV&6b8s=cy8!(iRqD^yh^>zk?WhG zZqA|CPX-r0wLLYTkf;||G*_Vg+ulf)g_040RyY1YO-a>^wuRChUYZ1uxb+W^Xiv~+ zfQ1t3+jeG1M?8Rw2~J=HQh3aL6DAgXGF)<2(d)JJ43PBP7;Vt!*8Za3X2+WEbv0%> zPn&3B6Q6&~t%}RJ-~p+nGu=&8G;&p5Y=DQ2EC6+?w&h?K?q35yJaE>$$gFP+EH_}~ zD?(`jL!=mLK&XL4AOOE++kn%HSEc*F1Kq?J29AKD5Wm@X>`ZT+@u6Icw)I(Y-J-4) zQ~;Hp6d@!bwCgBPLVb7DWt0FI4hx1boVaOw^&!m3ozWQ1Wy9tm^Q>rN^|Jnjz?!Fi_@ z7_T9B^8}dZb~nEd=LOn%6;{u)@&FES0>TghL?!(;B*0MB+}KnZO9AFoYOmk) zeVDxeP42lywy*7t+jV(nTLaNH*={zrQ_J@FVbc9egGKl@V7F0NQ}G)B!O`!yT_*ZZ zgP8xcaD)H`fJH~{_R9c1$dkuyb8g%60gq;zej*0v9B%cxJT-1LH-4?>e9U(&x?ZQz zn~S6;Qv7u#tyg;Hs{*^k>6#ylUnC2pH`IqQl!uZfj>`Ed{}C8qa(PjGXYndT@N}D=@Ub1jr=cFfsYL(M zIJl$I!6H(=EvJ#}O7y5(wjnWSFS%AMuj&1KTw>{>rt`Q8L>j>*V4WX_C zCLaL-Tt3snPsZESAwvVq0{j#-?1&(3Cnp5LcdpIh<%C()q=)?L8RcabX>fk`=w|7f z&9L5(Xr2gf1O@rHK+gZ#R_Ii{gwm3QMHFBT@(i?Cn%rvc;K+t`)Mv)uUc#mC_{FU` zQOn}ymrnPwYm*`RD}^F58&7gcQ+*_fL1qc-DO$btHAA&*GE_={jZlA?4)S2(JWX5v z_^!Y7l^jx1HM8g|CvM*Ip|SlN`rx5P@c5rSD%%D`RUR>p9*C;M_aAu@`z9;E7VVE)oJPLvF zK>pkOnh8zm3Fg^gv#g8Ene63lXjj2xdoNUhMjl~)^2TDVZWkYVp&pV#ed>GJCHiY000STbpt zo|Gt-T@7l>%wV%}WLy*w-Y77qxf@vfZC4Oka@?Z%>%eW4JV=gu(KMz(X9EX+b|#V{ z1hze*0lGs8h60lY20{8M`EhUIjBWXK1*pzThq_tA&}W~NXHi+{3QG3gPmQcIlF2w( zJTb1f7rzP>(=~&#G`^v3Plg+wwcoR#obF|IqNl zlf|Lht_ZHJa|3nsZm%ki&kg4CI!}%VHAO}xz`~H+gnM_x&ml_^^iG!~xPU_uRuJ)QlX5Z$M%(?Gk(QS1k!@-n0!@P5CqH0PfzB{idCRapo%pzJX z(L^3N3SM}MsDJE7q-DW;1b_w1fsl+*VYn!q9B4?e;AYg+_(3-+!~mR(T|mbJB|@74 zCI?Db4-{hv5_GTr_~XHDD5*p)HJCU9NA9%WQD534I9Q zbG-uEHCnP_Co2*9a0V1g3Vr@C{9BU<$G4_`gp8<017Ngs`oK#9r`CXPg6G44K{2-m zB^6~D&FN2Twyl(?@fM*WAQp#rbQI%j@uIp4VXkhD%y z{6(**GbXO*5@&<+>RvsE5dfSaQII_9Tzz--++vi-fY}A1JA}@-`jCQi^YixCU#6U= z?HnH)n(*KL=!g%?AWv?+kyQ3(!(uMS zcx_ZZWUpEH378N62`@3Y5M!Ebq!*a)q7Xa`K*eG_{8&BYgpfZDTj&!P!Mnq$Fn&Ku z4Z?t$pGTuoAKa250+Lo8i91W|rvm;5BNcgu$P5QC`Ux^{6)H*(5BV1G7dqB`Q)j-= z>ot8}zB%9eC>Jxur0u#q+1tD2o$Y41UcNOL-a8~dS(6wvlbOh6;)o6J(|14HjLra1 z0;J3x1?s@%xG6I!YAX#o{$j7roziriT$(Bm zFrHPA>nEAe>&Bmyf#!d3D=Hqbv1_zlVxJtt+z3A`&;@(OV1pv~<$j!A*i+YRpkzb* zDBJs`K|e|~bb#fv7E6$^_OOhgdE$w1Ly6dil6#QXADh)Ey<{;p=7J2Es*BE{g-KG% zEpc_(#Usn?$t-*VAuZ+5XN@M-&Yy*`HQK`U;clZ173MNltsClF9+JbBz3sJJ@${=j zZ(ckS?7#Nq;@CFB74#~Xi)gkz>;(g%7l}wR!uC3LGx$I)Qui3h1YA0&_ue&5-D)6*bZp}L*{e;J+bIEF}q1(XT z(kW*ke6nCDu)Dq6vLm{1v7xYCD#FeuTL@wwm!*9?@zp#2!>Sj?j9;a-L=tW$XR0KB zeWR4deDxK|Z~Ptqt2@U7+|Iv2gxyTagg-g-N(_^%bhzbQLQw+hUFMfJcdnXXl2LSz z;0IAhUo?OheTjN^S?%Z-KCyQv_)%{&#f+q_oHg`TG+np!T#w7Fvo(&?=@s?Q+`V43 z^mQ^h*>dBn3`x9bvvq5}*kR0RVnAj&GEsWotIk)X<5n!oD;E@bq zG`=4nZDaN1>;856GiIbxY;1AFWqAw@RlAf{T~$7RTz57E_0_iyf?n z#v?TIulKAsEOQks$I7_WFSk{>O`+7Kz^>4UbZEyWJ|PU1vrHyuN!=qA(HE^N=%&7( zvc0lDCB4IdR^ ze%fbFdM;0|f6JE{T&x|F79aX{G>K!0>p|O=XV8OW>9q{5CE%gwI$I5f$9i8El)nbM z%h-DGxr$mpYkpOZ9&Y-%(he{%PeiyWfv~Cv$=+#Qm>LCN0caEQYmnUcVJ98a7-11Hd_4ZO|+ndjkgqJNxl}^4p z^z|j;dE_g-+_lG~5tgm`8GE-`ao=K6y=UEZi{AFd)~&MMjY_?>S<{MI#!DBZWn;D$rF+*yz$gi+wSI?XDMBV&0 z`OD&rS5f;knkF)g*#{~yz+wo`j)n}Sa1g>qiFvkoRQQFIjZU#Vb)l2^ZH&p?sXUn9@jtg0iCF6V=W<0geN14a~~wZi*Rqq zCd|!h`wK6!pOzx;P!}Gyno`(6U*0Oifhv};BjGW>cenXMeQAEoqp`$8apj@bXUAT@ z9L5YB;&bDE{iE8>xSTM35&#(eEiUfFF?G5|S`JFfD8;GF_KJL@mRxZzUE6h2yNMYo zapJ^wbKJ=0@TH;KP{ul2WNrrjvUNeS)y9=LzMVLZr@;y7nqq8TmM7Mv(d|$Nry(+` zeITrWUF$WTO|~}hHL)0sIfEKp98aw=9e!XuNcGwhezR--C)U|>UZv!06{c5-5y>Of z={Uxvm~`U;k)jy!6H6!43jdjQcJ-A!9z$|KM zd24v|W9~Swu0V|wEx;J*)kMa2EZ;vCa_Vo{{g&6&lXKEzzYDw8dKJ$e;i!oZc@t)N zGLOv$ydU#J4Hu5oDK2lYu|}j8!ktxjkg8=&Lby{EHd2)%h2?euD&4*mWv0zq9kY=r zE;Jc58L~y$-s}6WxkB&?&N- zMviG)hq#N@IJsm zZF@r?_bXM|r7zS-a9y>;M} zo2vC-;XW@b1tn2Pq2@%~GP&Jj!iTPs^n& zt$wrw$QK}V>yn}P3%rO9raSvb7uCx>!bd%_3w*+2`{N07G4l=kAYo0*Nn0##<?wXcuQPmX)k_s8yxVr)zJofG*8e#cSe24OQD)u+vP`i}^1{jmW5`y? zdrNeHc(o#ZnO-Uxj@CZz7_xKlZ`hTH*@dtxiZyxc*bnM9)C)Mhj92j-Z^M;mFT*=O%mTC$R!ZLFi28Ca`3uJ1 z$aMJ+4ksXh7quhUx0p4XN9Z^M9`kQfp*prc$$EUKsXXg4hYbZ%@G0s1%)x9jpaB13 zED_6dvrlo<(27XRrfWEXmq_W|=qiVmOZo9HX5*7JM*#P*F-?Ye0TUB-;$Gf4kD#bn z@?Mm~eY&$=_0M-u8djP`N4Mv_|IVoEY!|s^0r9D=O@P4%eb^ z{P=9+iwmbX>@|tE#NY^)zwr|u2dE}jaW7`)d)d5XbalWF&iy8MuX4&rxrhH$~+a`elm{Y;Ff z^ZduV&*i5scp7Y}j*Ru%4Li1kT5QAo+^&q0OTVj*D$B`2iXj#}3B|2grPa$Z>xo8X zd2S}j_BfDviX(>1Lc3rx`;#atSsuD+E9p;&Eazg9DpGk1lbpboOdi{F$>r?1=eD-W z&ZLt8B zC6F~Ap~TLgyt>jWQINSUc_mOj8P{)Ur1Zi!sam9i2EJ7-+IZjr&Ry%19j4TeISke`kJidO zE|HnIJb>#Q>twRd_OJ%M6&D8gZwmX{^-yH9Rl)-th<*{WSy5-?Qle2vgyl$Bd0dofnj|PT5|aoUCbEdkgV- zO#wlXe5>*I^yUkjPW}8pZz}&<*uAypR8SO$oO+uUck{*D_q{Jwyo%2J+46)#;L)J) z#=j3x7arZ~A6BVVTnN#h==19&E7nYR2KSW=w0qWan*$-D%Ucb}d(~j85 zn{8%`FOK-^KX|Xn>t?2sA3V3rY~Zf^h!o*ZBs#DX#H$G_Mme&T5^3;Jlai7s8$p_? z+h~4iA4`|%+POcPM^_cCHZET7YFIqd73|@4duN|c&C_EY^5{W zpo0#GR)ESI2@fKKI4+05^(5xPmK`Lrz_BBiyV;8A}|1;44>)Tx|y64js zsQI1$lhPq|D3aI9W_xCvN^4B>_gnhY_c?J*taj9AYz6)3H0H>|!h z7LZW!7uPe#MjA=BWw{Iu#P~=4l=6wla#9LR7GlHJ5t8Xq-uH9FGC6z;UNHYnaCsQb z(bPLKEX*bJzldUHBofL(>X)}C&G}pN^3jvj@Aen0_H@Y>taOBTP=y1AZ)dTF@iV#u z?GSCc6L^V+t63I&;}b)7!S@cH?24z$T$TnIlKc#jAp^RI=0h0uk5E96pa2rQYGdBYY7QtQorhei#~bXh*>lvA{IZ@Sw%{uGXy4CSl7R)z9nu9 z0mMT9M8c7qODzkj)15(`o1dLa8vA@AxMq6uwTyG;y!5^F z^(jtBzv4^%h@UFUovx6q{y(M#QX0dC#6s^c$L`VlzTCGP2;xCq%YC=z*^%#uX8!YPtxNQ%+HKXK#`al3 zEPMf;Cg&Qg$1Q{~su?R#JBZ^C^X@g>2n@zvV>Vbv)r~hi@OfM7>2qUq z!{5~g_Z2iC&Jl-D1AqewGZ5QY`I{tTEn`zZizcPKTylbbxp@TKEcY>R* zIvxR<{zky1o8JzPc7D}$mQ%SrJ?K4dqp*LKOM4F?(DkMcaeN5;tVMKGr=CA z;enLf!Exr_V)uD;277K!Wd6NZ<=Yr9?^Msup@gY;e*VCC6N02oeO{Zg7d$&Vq|5tW z@P524<6ZM``NAu}h_$_anq9&XZ`7G2id&kz-KeRZBmJgxv9iKiD8leMj&{D5%V@WQ z7&w{Yg}vrg?v#esC)x$b_qZOnW8T#QF=b8M>q;jwv?+4Gb*471#;1XKklqBup*Nw; zLHA;6cV2L7J?F^VEM5UHS$d4o3~Kg#0%V*HbG^*Ez$^b z!1E+5AI3d;43>jZ*FV=I9{Kmh&4OPK{UZ=m)*o#FBreccNmjSAmE?Vf@tmj3;H#^p z<0A$A=KwK*pr`!KpJ+M;hW5T*>OIFp>MSeQ3)kt-&Y0fPRC0eTtV~T^2yY6&&kYYK z)%p8uuA8~78a9+09BkCyiRQ4z4%T%@DFq#hy41Vl()1DzbUsl%Wr>4y!m!=fqdg@| zqCL_m>9k6^Apd8G-d%v`?)?wYD?qXRbrCow9922x` z)5>(U*duhvyk<3f`QXBfxWT@6wi7plYa8RtEeQO0yvpNWC|k&XD%+%d;zZy+WrKXR z>^km?PHw2|fayd~`T5ik!*NgW6}~T~wLCk5_S3w^2RXcZkyD)^f|1U9{eD5g`@4AY zPgx=NP^$M;q{>g-L($!?e`&IRZ0UiQYKh$*(ngXMN=`p{C0&KWLNiFJ8Z6AN)5&`y zebdk`3K;oF-!dl*8Le-FDsxIBq2=$eyy!Q*${iuYPPuV`Wb5lr$$p}-hKZ~LCs8T3 z7jx-0;H)9bF(NrJiPdhwXe5EOQYeC-*m!p>@*zF@aq`UeopXzcOJjt+3stmzOTWZ9 z2>UF~O%LAANhkhS6H=kzR16U;(GC&eVNGhP_?bUbV$Q?TKv5VsF8+Xe+`-=|~*^(W6L4CT!#k%lD!@ zJE3FIJe%r&Tg<1u_f8ehEhj2%3G?+e;lI8ruA{BKHXY_G7{ALXNO;6vS-x5yf0j9n zX@l7a1WNz5c(oyJkgyVwBkOm+$tz_oMT+)3ZsqtTo%p3_2Sri!@mF953ry!9)wGEY zQ6dE|mg`;whOsgXo*vYT4avUQH($9NKULY+eCXV{!Eb&(`yKhFQQQ|Js~|3dIG`V!WWlyf1H_1Y1&j z!amjcfmUSVfmiA%kV{^zUhzl7gvrupgmI<0w)^6Sn4!|DCvNBIQrJUllxEQY=E6_(XBVUlj0 zUWTf^bn&QMqMLjUn6re?>iLCmGajH}bg2b*+iN&-erf)=>Qv0D6%5DAcbu1H9rjt9 zd^!Gb2MPUBeOm-E$a=d|jRdd#7&m$$C`6x=HR|a^Pr1wRXE_uWX@?z=gtl#EP9P8q zgRGl>A~`{nP=BJs3;j|)QC;$hx>g;~8@p?bQX4(k(ZPAqpF@* ztUP(N!1ifu>5KDQ>@?+(^)TTUDN9Dsn{ZT^%f+}|$aQTcCaJmBz*nCNbaLm7 z7SNiP2K?jBEVC2N$IXq%8t7Ms`1gxC%SOG~77Im}4J?+5Xm5fNc&QGt;|-;^o6ZiG zb!@e<4e=kWrbKij@4rL*dnaSvTl_d*gWJIie-7?CI6j?twaKYKBxKLO3of{qaH^*) zEwXrH(UuF!+sc%!TXbx`q*r`tcdcIQsn<03{=%dD1zOz%TXCw+P^pdU=UJ@lF;pKM zIMzD^IcN3Y03}4fy8b>}PswwzfpQSff5+Tf7h(`*77-FL)sLyomht`;n}G7>57#zE zvS-S@z1x&ZS?>2Kjn_}=%6PY1pU^{>1kxz}JbP@n+m6%6RZ5baHj&WV5)U}qrC}H* zpGOpGI#rP@vD*^ec3e|t>YYyB5J{9WB0}r-fD7hF6<42(Tt=aktI=mp#flMyHVSsz zwbxR%L$1Xz;-r0NOQwqJI$e$t=`T$hh%mKn`fg4X!l|J=bDo z_@I%t>xCN?`cvII#cgVpOY=QBC$^W@4NgUtKn{Ur>XX3@JX_NrN)kS|{ zmb<9fU+7TTXj`y4l3^}r-96*e_ziaSf)kxrvSatvg7>c7#(y z$EX7ip@Tgsty(T+9hjEbbboMG?jr5n7B_9x1^@h_zwO)_{FP!UdudLLry$5JB>WES1tC}M~P~&r@M}S zC}MroXaG@0<$m0$Hh4Cl`{hW&Nb%K1&%S2%)VVhYZ|ydT8#fA8iyur|c|TDw3y*sV zbN5f)YGdeCmf3F9xuE%z>c>mwI>M_81s+g=8tWWDpTa+RF(V;ITY-r3%{1K)&@nl z8I!HzdS%2l)OI_Dskh|;=0``BDjiEXX}is6M>Z5m#50VvvR?%rzBe zp_I{INf1#N{U)m`Oi|8Mxf^6evP|AklA?@~LfOHXMFy}IOc%o24ILR5yDKEAh%Yj8 zTZvy|KF>g3!!$f-OEsO7r8Q3KvOQv_RI65GED~C8g_DhM6SEl1OSvvPNVX@7ldu3I zmB{1CwwvvoYkb_FnF9SHj`Xwm)GlBP%F` z75o*HOO1bg_uekEK*30ix9aZXXPvD(;5mMzIQZYL;7tjL+;1O5*Y%z-@q_b8}hBT$J1LVyT5mZXAA zGfpuoG^O;Ryh(TrfsoVM7?4Bd@2~}!*qEIP+ym>-X5q1vN2q<@MWemv82ir2gwrO| z$qBnzo_BewpJXCHk*lSPsynacUP8>=Et()N%aArZGPz*UT))swR^4q_u>AVaL}&c3 zMKWRC!L+5jT?y|_)*4Rt_8%-e+Dok;WjwK!wV3^POtdv0&&#Maq@uA4H~y7`h)(qH z8KXbW?(N(d8ERT!sAiqM(muYoI}b~HppAl| ziZfQQ?RJr*G%?OF9;EEjsx1{+n5n`fMtm{y5BocQu8AydDe?hzBa-JPjPu6Q1*0ca-jPxRW%KYFfLm_Mq_RT?jXQ zN?V()F7H0LI=ws8KfaaqBjA$nUYTEEd7q;+4+sV*7@ zTc^K+78j^l?Z&!FI};PKdR%u(AfxLZ)igLX#P)waPsUXYt#mf^a0h+5eA{}Ia6=21 zm;>CF&_&)rT{bRshyO5znmeFW9T8ceD`2|Wr;(CQqwXecKxro{8o~AkR)LMEon|R# z2hI1y+!7mw;%HZI`O99Y)D2`6jm#(Iko|q1dl7n_iSotHKr1&~wC6}VX-=b#45Ia< zW0|s~cjSnSP+{Z&Qx^_6qU<}IQ7KNF5y3|%8WEi^&c(T3+oah#(hHLgxn6KCc1F7; zGW0&sk&5dkjALCTU<9HYd6G{Y#pC3scp6|O$TH+V##I2L!SyM53G1bs_urKEE6V`l{ zc!Vt+hHG}kOM)#hFW@kFb0QxxND^(4mSNM{Spv-Dy%Yah?vLbMzDW36OciOQY}7PL zJp?Mt_LzLn1JHJsa@KV88jfa^c*63i6|<@GQg$8wAa-Bl#GlhIMq1ZDXrkfb6RKD2 zxx>OT+WUzU!fD^S3SmRqVrx^6+hBvoxAq+=jxZSXzsgEIWdN#N$_1DW+=Vz6g$Nf| zYKhZ9xuzk9HX_&kPsH)xLIs(^$|wg#=!JG%!w~3a35YCRr*m=)pI8VdyTkFJtz%{! z6Ia?2PuEhxKt<#ay8N!IU>O!pxt#71S4CRA(6kVx>vq675CJ+i3ixr zynceX`(oZgc<-Q>pjX;LG_+SO{#R?`cgO;)g9zSuFZo)-FjGpy*c<7cbF&*hDehud z`gX^U_wByj6y3*qV$?&|zHkF|c#nL~pHV3$KOWeY2x8l#;uqiA0ybs!QNBeefGBjZ zeiK8;ob?OX5k(Qh#x&rORZm+!I4|u?ZACy8tGha55A9GN|Gu8`IER2wZ|Fi)#pcCHrp}oGPJdfh$&8+CjYSqTB)P| zpnsozxP>G|cG~0>c^K_dqs+K_bO*^2>wqLA?gid3(KDzn^-C^dydbq3K!VbXDZ zuHTWVoo4&H$Y9UErK3q#uuh52){q)NQUM@SOl|Bp!R?_go)hwVa`4<+_PXt{3#yYF z!W%{=xJ{VQ62HE^sW6OiCFMq@3cYYL*G>+zZPHP$#3|9_4#x40R*4O;Up1BJUTGp8 z3$I*NqAija#q%S>oDy;XR+Za0)s-+Y!4b^j@EodaT#qj|qBuW>vj@m2Gtb`iL|902 zAFnmE6sd*39D#cHNt*=9;Y|;D}cHKqx8}at_k`n$p zet*_YMIs+zBK#OiPPfG>>k-Z`ojkhuDtwTiA6L1w7dPYW%Nk}PU^j88At@^WNCA)% zy9P|>2zZ_&QLJbD(%37kegbbmjGnQe#WPzM6;vk-!wCZV3aie7_x)8qaML--YCz+w z6yk7aWqO%-$MUF>s0k~k-R_fJGuk!l2{A$& z!%&YTx_ly~%LVJA(h9z6OQsH*q0G>RApSUFtiYesi*&3I0a$K{QMBs-!-GnKgEyHf zE}LPf_p>&4ELTU&S2Ijlp3;!p#5$Sn!MX8dvFpeWMs|#7+TNRqnLdTXL&CII?b2rRTtmaI4r$T z%72jX<~qRY_(!({cTYGTmMhW-iqJ0q?&^8}KJdQz7W$>^8fwf+!c^(~@o)Jd-^lqi zHxpt45~@Wx?6{F&@pFL#{@1ipH+)#6CfA3aEZ+kv%e9zlGxBLy>Zze(jj1P-97 zbVm^I0q&5*0XkPE;D?adfk}kHVbE}XCne$zWa3)j0I!T_KKL8ZBy#Yw77f3XeSnM) ze^Yg!)~jl|)ncjCUMx_nSbB5Og!&|*&VMnOGt93YsA`<}PG2c9(gJZL>)dWAc87538%KAnm zA_b1dn<}`U5HY}dqG%2NDHMCY{jpjM{V}XYP~ZZ-s{P_ZNdD{Tn{BrfW+RLzA;g=# z8C>unVGdb|G?hEslCeLiSZ_bNBgIlX+42h7?QohRwwmS+pz6cpyJ&iAOm$zK<<*ux z)H3MPgdJNNYgz5BnP-{yoS-dmD5m>ZO5q10LgvPC>?TodK?A>c;KX+CLERHTP+19H zj0zXsJ6}h-T*%!fvFZPNe#u%JMS}eMpUIu|6_vykXrZ_i|20y3eJ+`Z$$X*#O8k37 z!?~c6HWY0$Do0NJ*eE2f!6g|{c^g3nx)j@WI4_bQ4zw&&2Z~h5tp5q~5+OY~*J>OE zfA&lKsI0q?)sr1%(3%_|d|~F^!l&wTT1vu(h}IZ_g*Mt5!gvx^=RbAWK3>inkCa3f#XsyvTgT^|i=%-Q75$!P;7$g9H#&5J0>eQngsj89qM}%%w(- zbrqOU?>7m(OMtvEK~F+1V~7e2S;a&VNJLM?#MiM^`WdA zb!9Qfow_>Tn#au>?(G-#N}J=~1gI@7<*Op%dzs=d&YVddsNGk0MIAa6HI#_whJ?3r zkWD1r%IXlb7$|kt#Qabbyz@NoVcQU~6#8Jn)Y-c;sW~B`+qX)~8)4DT^3pms5J)J~ zHG(wDJpb&Ox;oSk`6?`lUlnkbu4=fS8Fl%juIdy{&zOf9oAh%(vBO)BW706#Jo^55 z0;J$8odpfSt25l_S1aBQ;RXauq$X~r2hGJ$w5=VKFM6 zGGo|P^x9O$f0>X2{dXBQ+b=rsDrgNQHoo;TJEHYj0CTnQ>&aaC43*5gK~B83J?0;CrDp4Njzq8NEIz}JB*mdO@uuzHWYlccc z*%Ck|k$+?NXD0Z~e!x`t`S${rLvTkjr?0oEyOOLCPECkh9O759MbrXw9=BOA93UPZ zj~%uOLupF_f&bsB%EIP2%WZOA0Y8Pcr!7+y*z9CoK{dNEum(T3!##d!#+_Wn7;6EE z_p!-@M0-YXas~nVTGva_H6oBLW7?F;ju$nR`-@h&Rc7MHsiuZQ_haVg{RJ{=7W``2 zc;OF#sGR(HEKP)HhP-|gYdHy`V%87^>+#8+Q_Pwcm*f)T5ygcza?;M1(VyF)KQF$a z;FMwVc~#2T0YTGk!Sl+Q3EU8oGLXHGc+Rdn3g%0gxS)nXt9GYrS|Q-AJSs# zae;5Z@iGREgIW2WL}@!^I2QvcN@;43{G-Fl8`3{g$)W&Rz*tc$g&qAS4l7<7vp2Ok zm>{H&T}^mhbRLl#T}_rC+)tYGOCXbY5Q-hm#6}rFMXVmnQHy3EYNO-%_^Mxy%tYV8 zhkbjm?iais4Pyl7Cp=oiQZ!&`a}N@gdu~cOZHvw{aiJG#Aas>|p;cvQbkNYV#1N?l37#Z$=y3y1BHP*nt3dAw3Dyw!twnabkfv0GO9#?)N<<`~q*EJxe7&Ev z_EBhBZFgNe6}B76Bakl#{fM|JU^d9*tdQrcP8oN3?aS@)!EtZuXw>&j;`nzX2CCS> zb7=N0wj;M;f`p@;VNc%YRoeU}+JkgN5=_8LLOKFb2+~#VuM$rPIU_jg{7Aoru!UQ- z+6nz9;cRwVMjTVLYN7LPEoCWYWJ+aoQH~g_KdUr!7X&NLN0wKB)SMC@fEcDZcQoTexR$ z^ulESc!KV&DV%|^*rNNj=F^45ds|@~g4=V9e~f?h3%ZKNaBWP*pS-W*TGG{QXMR%O zwPpYR$iBY)+GBb2s%L^?$A2sCK{a&^cS3vcEq1qV2>MTUSg*N=t3e>yQyj z&%x#=iq|cP6JBf&Xf0Tsf-L2(9vVOSGuKP7BxJcsXvXEUl2Q$ zjH3xUR4wq6^pVgp#zV`Q9WND)PA55)$!Goq>E0Naz4SuXh6UKG4!n-UUd$0e=Cg z{b1st`3sP2`=^O>mYBG~!gUjOWbNhQQ`Qf-=T!Z!=Y{-}>l5l)lD1Yg)ythfB1>x- z@lU`qYnpthDu#YzbQNywD72Fsxa2C+vw|~j&H|M7$Yn{&s0XcN#C6)QeRI_aNuKFQ z@wjW~)9)QWdX?HF%Br%cn;YozZ#>IkkzXzMn#tn10sMGjOw~&z>ipl}kc{Vq!AyVO zV3B47r@L~7yw^O2{n&i@jYACOeq>e8+mXN2o`~Ryj@(Xo?Nr5p0&oN5&8tq|#&3hq zXE2;MS=nwvZv)LOe|e71t(z+T;<>roig^F%$jCC&PWsC=r6^ zW$o@wjReY6cuQ!gzzi2MD5@RkVK1`=Si=k111z@q(%y25>ZA;J{$wGeFjT{)1hYrW zwfIBM*!J6;@#TjYUVJ%MRJ1Rq_GnEVveLF+fC4cOLy4}xk+J?iA#nKs?3hq*U&x} z$M#rkVtYiqY29exADEEUG)$e~ww!0Hp6G!I;R}gw2tV4R;IwrSd^raCa4_L`45Rn} zLavLo(1w8jh8xW1(CtBH`7f{3sL~dMV=@zA`(qB!R%-rb9r24169xq} zEe)ejm)o0a9rEX%$y!WjsEIP&b-nqq#|`|xnc5c2CD8k%cP`o`aO|(fjFdT;;v~q* zP~P^-b-H%-KN*s)QO$6ZPhI>K<+s$YAX&)181WO9zS>}~XT<;q<(*Ttb2`t*zd|$; zXUv|LWfLY#Ink^AgtIGepAYHEl*&%ppmeR6L>)NBr;9H8IFB)Sc&+zk|FQQK22=@K z86uB0(3$*%a9kUFWR3NViIQe=jthi}51?)GPcDpDPe;5Y5g}mtcex2d%3*i3LO*kr zOJ!J&3KghLO|t^F&)g!-BVtTN726!0wRKAP$bkdBaIPsO;`QsF3zK$9?4&XApEP+3 z5DWlAzs!B0F?m!@_nk}*Uy;339BhnVz;=ZSI~Q^~3z{Mq-j4+E4=x!?ieB7B>TFim z(#WfV$Td)GCb|L+_q^v~V>EH3y z_GC|Q`8H;u0<^%u!$H|cb|}*R`e)KuUkR;Yt@OkHiLllrC1w*QGZ!W~(wvYB1hmm* zl5va|m>+X5I@driYctxpSfxbDHSxeU02KtGm@=*7du+kViT=c18Gcl7ux7-ez;mgS zuUI)#Q*GyL>bojNK?5< zBoFWRub=x-1(&~6@%tcq+f0gl17*r|{Rh-X>nhD`JUV!J&@R{ge zsh1PxseepayQ#t(=amcxh6gj1>Do{yG`6%xUh}ZqMAA%FZAB+0mcv@43|09?e!F_% zf|=ikS%5{|6Vx-V=*_oOEo$#4j2tf?!0J|0p04bu@{gN>KJ;pf#VRjDExPNLv51qr z*t$a?FpK%0*;~{tT}jz!X#9dvLuucUia5qBJbc^^?#@aiDgOnAtR(}rt=g4Xdq5LQ zI|z09_$O#9vzvZx-Pgg4g(lJ-!|Yj0A1NQEH5}KcHHsWDipAZKTKw0+!4iqDn}Yk) zJ`8J&9Ay?7VYZ(H!CIo_nsMW>ql)u{-xc=if&xvD<~m{6_W$(oJiXtf)oqV%n-L5z z1PF#B2EJ!1RL)#RSkUOR$h#OvNJJ5*>ReN(xrOPR1=$YSY5OaZCDQ0~v4(Yjju$@M zt14dP68Ln&;-iv<9RIq=*eTjl17W^L{LVp~(`5oYWAQu#mNmnFwPGO$7aGnfOj*e3 z>N9T`EkY=apYXgqiyYUYrDS+7GNp!ZF*3d3YHSG@!?t))`?@&PFQz-gP$4q7fgDkE z_O`up-9yKU^=_TOnkJui3XBKmAEvdUhoY()0>y>5bW?Blnm3r#FVrsP`A2t+6W+@X zw=5UjK2RMkupB}pmL%R+1yTXHeaHqQwyw<+8h)H7tWVBWK(yCClgJyAv%SYYw4J5a zx@FgY*Dd?NiOBT)10>x?5`Ul?5SU;9KTIzU9+u1v;(M)jeBJ5)rfEVEFTSNq6)EXf z`UC`ImB&?7&377e;@EkDVP4~gy6_&flR)|x$d1Asp?kJAZek3D0f8JcJ`6yVGxqXk zFcX&^bHjuSgw^R}!kn-~CMRs@V9W9^O=`lat3wN~_xcs>rmfT)uIt0$ekTP)ALe!J zMJP;mh=1=>y25O(r0w7wqcpGSyJtLBw(ScTluPowrc95F^pQUT$DEvtTWz-ckS7S- zlm)`Mj^}Q-V|yCObrtysK^n5~Av=7g#;sz<-;U7ZxF&O3Ef#oNaV-<+X>pw9(3w(* zO&S{A^~p|$uGnQ2eob+KR*FmF_0w{RV4+~##b)yl-OAgF=)`QvsM9VZto=y6$^#f{ zuf(Fu{*27v5O*P?P%yPCp8);;Cl*TLJ@ltKv$(rg$ne;`OAZ@EyNLJRs)AsX_zI zU}h#L0=?*4CuZ!mNSNp{6Ff8};KjR#mrnXsBC8445DkJlJW((xXLLY9W)iiJtmvow zSL}%W)n#FhMF79s+SJ=S{egy5)*8Mx_uHW}>DS;slT+254_&F^IKuVqD<(8=%IpOcM6PC#*?Cq2=0|FO3RXiFJ!_;+T4 z6M4kd4FIZ78m~3hEn$XeO7m0+k@!Wz5|6V&5Wi?;PWaW|zmMazF0hBpBUa+nS_BVu z;TKE89;EUq5vFQ7efudb4To$;Kqn4(o9N6b zGnHNJ<&FPpl$>k*qrw(m%v=@EXL;E2geWg7V>j2sEHaW-8e8IBHQ?vlGP;N@9bUOa z!L^L0O>xD^#-YM#Z$BCjr<5AvH>4YRSI5=fs0_*Yuc>TWdMa5$ za#8`;0W*ndc5qYU7h66Gz!b~PBq>zc4djwD#|t{>G6vDP#>LmYNDHRk5ez2EA0bJ> z(A5oKSDnQ=fY|BJQobnLS2+VeJm-mcb)sCvG2VIRLDOrq%e_}T^uiPReoLVHTv@cAtm#1XPm8iJ6xUR5DvW2){wS^!Y zA&B_mXW!?%dsb!6Np&Ew8o+-U<6evi$9xBN;|_ku{tqkJUQiu3cPEdmZwJ%yYoSJ{ zrjZ@GY@;cSEVgZNuWh3-iSQcG>mRli|swcH=AZYXSMic zq$>3uXzdfIMex2q(&FrJaPh|O{FN&UIff8Y9i~?t-89m8z#w@SoY5^SQN92_Go+@K zl%{u^8cQD7e|GW(>-k7$PgS1i=YbRPuQx!xbUwT>axrmWw-T5BoMM)8!==Q|G1p56 z4i=CsZ672dCpsh}5SC{kr$@Wk!*MC6)@wH?nh`#my&{ONJ*aB<0b5&`dW>r%#|-U~ zbeO)iF(TSRP5^hGqA+ro?bgN&p>TdYcSZUz(ea_d3|AAaPjrmjfSXT&@3tjE4DxZz zaIIHXg!IaaMAUH%5bH~tk%47%ip%S}w(oKgsL>IXuUdVWGqRL-V&z}8! zqS63A(O|FZzdX)asS~X(Q@C7e^qYpft)>1;zYfXziQpGb%uTq)IKn@Er6-TpJoSD& zU3*N3#D#SUBZms+Una=LeJY#U>R>j$;4ivzKF6THum7u5V5f-0+Uv@i5qC9>V3N#H zBL&JwWNP$KR!d@98l^RvPmKv!;NZfj%L%x!_@&TrA8{5%p_DHR)R9fHag_vXQZP1Q z2}d=Zp`rT|GCKij#UOGpYu&kkk&$E^Ks|7-66ge(+pSy&(%f~D4RR%%L>htvGI2&2 z>^Fz8n&bZnVhBW$Ryaa>TCHd&rAoSZIb2CpW#Jw9#@fyOj^>;Gomk!GQSLOzKWRk% zg%4I{S6}qhizLEohk|&VZRj(Uy2w|kf|D~qM4zBJy*di859RpYmo5{3{qatO;% zmLTcb4n(YWiF>I=s|;k2ek)t*3l(DwhzL_N&(`bfk)>F%#iI9JVw$Toag zR?J~li5|t(;TP$#C;VI7V44E%ltL191UAUILv5H21fL}7kmY>rkU<)Vqyh-%z-%;} zUo=ACQ-2dViiMmb*}MiHO(gDKgC6qH_16R;COj<>dN6DVX!!`N^?&VwrVHHFh=hwF zp(T|w3#DBK!do=IIdbr5xSvPwKyaj`{Ugfifyf{H@qE~TUDU1E#SzML@n>anbl7Ou8h38Du8jt zNF-<<9yGGO-Shw(cfM`6`FjVm_eGamK9vO~mzvK&F?%3kqJv}qLP9;QU-<61mAf}N zkN)soM(QGeC}yOMurjlogXiBe#xJM$RIR>vu3qU^u3q9L_rDYLMfz^FU+YWlD1&h& z=gAukA{=!5&FMvACJcZ;134SH*4;TH$rkAxlRTkw>;&h2Gn8SZP(BgvRr+x5H+>Hr z(}(B+V(z0IaP0SrU9l9fpJb%)w@xY22PSdtiqkW1Y)d^m_gBK~i~`(|tk6|(6&v@< z21S`ITRikUyvuvS)t00o^Q#GG*VR56!#>Eqqm`!e=aL9tII-;Vp(`~BHJ zJO>8fYsRGdEzk(kxZ&E`g;eg4FpDso;jK2@|MV7LHe}_?|6arg z_yS05cQ1y~chF?fwvyZd)Zp5Y5ybj`Ocx+`@T2~2su029daDY_yb=CGL>%ZY4d#>7 zo~2tdGBg3#P598*9VuiwDfjBSCjVN+-apG4kZtB37vM*DUfyL`0nBS4uhq7u;3AH# z&1*qmA{phVx9qs1W8mj23}+J}7I7ls#YKzN)Pk61w1U-lp=m1MWLDS9>u@^-UV?YRTZ6L2Pbslj-tapn6g>wQ%mSMSR@Fy8dv5Ak5Hms#4K z*`C69S`r&LFs7PxF(ux`+tdh6R;-FZEg5` z$8KrsfS{rq2X>J`1?2GCI3Z*!paLR;1O$Xs*n$QTks=BtIn~NoU@HO^1!O7^ASuQ$ zlWditRRM>0kwPEp7VYGx&H6k7p9v)$eZ_B&%M^Y)^p$QM{c*m zy8YV1OgMG6z zqBZgnX!O1|UBZ{&x+){@XnjhnxZBwArI0eoR;>wXNoN#!l#_IawVanCb}}jLPewrn z(Az~#D8WsaW$HOq+hT!V+2eFYosb)pyBKCpwrD`DH9R1KnpVtLOsVLz~N^{t%x{8fqdQ^_6GsmY_m_X8p;Q0GtG|2Rb&FN=_+Q?$`{(}cxjUOxkhbpKqr0z{JpRi# z&d|@uFEF&IEI04a)|4%0hW~wC)X#1fI@5<+tY36Lbu*p zQ)A#d^Ri1Iu&}QXk0tfMsmIyi407+5YS)?JGiOi3JfpfkB#Du+$1H8VQ!4U4=P|9z z&Y9m0?w{*rH8Bl_IFgnY*^EB*(!kNqAp89XgnDi)LlJ z)W~F&W1M!R`>LR94XeYXR-~^Xr@%|Wt1h)dFQ8#nV5OGi4yT);5({-;f*@;jCp@u| zRS;{f7RunQlZ-d=J;xQ&`ZTSYjcxqshZZg_r<&;e*TbHm<=-PRHm?#&50n(I;6={A zVKsethfmX3Ite^@JG|_zdgza!Z#pmwVy`@;^i4GJQsq-Trm>>k-;a1n*3nZm(ALr| z=E@RBDTDHZjEiydS5#_8u(5gHbC##+I@1Ktsw)3*pGLop7rXo@L!z3a?KQ7FEAKYG z(O9Du_Mkp0bLs+l{Bh=Fy)m^pugLXG;29givAWztgO*FPqQ4TPtaY-gTQRSHk zTceh+(bt+00S*riSTT1YFUMcpqIj^Mgb47qrN<`dr_5 zP2dBe%yQDVSypIWUwZ7GZjk%3l3mYf9%;KAFKZ`owYjA<2j=FyBi2Y$iCY}0N%PhK zb#cL=Evu7&4s^R+kd{nyYtr@wG1r4MjY2CLoI}p?H35{?hZqy z{No7=MJW&LU1mKb=$ zf@A1HHA(6RHJMMfBhiIeo!VW1Z?CSLn8<_)`sbOo7iNYS#Ln<(>0ITy%qqpRZu@S< z+tq>3KmqS`3wip_sf>|5Wz4or3U+`m{7$2h;H}B2+uWeRm z!l#^Er*r;5EwM&182|9rIzQKDX)D=-GtsF9syX<(thpuP@jC@+&Q`Z!U6mJF5thQ@ zMeAz|>r_luG%WXO__FbDy$;XkqnN;!IyUvY9HL-Vkh>imFcHyW$E24asSB&@Fh>Aed|syzHj=;Ej1`$+NT#8}HtI04~RiOsB^)J3usIe9s>|Ix-<9>*izez8W2^1O2z#R}7SZ~t?i zS?4+rjjwziw#goqpO$4h4b@8H|vZ7U?|~~owuWf zf67g+c4XS>!j-r>hwq)@Zr*UR091?N7AGV(2iS==dMV)0U}rhX}lPkwI-~H zf=zjPd3Cv@=uKH%IPvRf@dQqga2;^mVs8!Ro#?F333V|}Nj4h_HN9OHB8sOC5g6H! z8@bB~5|2E}p`)M%cypDB0sQ5VVyC)dZ%E|%Kft`&YDI5&k$lMCV20js!p+mfVEK%j zXRmBPRaU2>DtnN)LHvt~YH`U#1LcXgOWazLOYRY8?1BV0T$aUHcdJ|7i_S{mIb_Fv zoxodRp_Zg+bJ(WvtJUiTul|Ey5fMC{7g!rM(~l1ikR^8yg)>HPcUO%UO0>FVu;F=) zYsl`qX3liEW?q=>(CtKf9s9zsmD3wCXG|j}g(6SCkqMcsmF9y@F_~ zdpf=`9$5J7uRT~J>R1=$Kji1KgE|5O22!HpL920unC!2ZoS7SB&~ul=)JnjIMeXoC ztV9(S=86zV*RWbz;${o5(Y4Pgc9#-GOt`tArFCK@kM_ghD8Vwrkhx>7wz8P#4*=Oh(oD zE3D*GO|h@u*p0gPWvFft&uZbvl7yyHIRm7SycyLTgpa=uH?SRiLHNvSSZl7K2M~un zJJ_DekHYb_rI)T~haKA~mD?GHD+VUNBCQ(!yS~TjVJLqs5PL&)aZH}qR z`y<2wW|DPii=CyKB#Iq08xJ>Zx&ymm?XF%NwrFsc>9KQTjgZKv&>7cf`raHj4!&!UKxjrZ!;dJ?4*0}<~GGRwpJ z@1{2e>&0Y!Zk2uL2_O$aJ?V3+!_6_1J4Mx(+T3?2o=I9;DWebl@MGhdUW#%p$vBe} zE=mjDsTzxAXn-kOBLN{+Vtcp}A}Tx~R`PKE)_^xhAreq@ImBUHp315Ex=jnS0D#P~ zyGl^Yl1ws5L4>2wnn`Komh9u5l@A-Mu$MFy2O?^Ztnq5Ke?7 zEMfZ`%h#x`Mx$OOJ7@Y5In{zhxp$L76n31tX}PMK+rFj_o6XIEW5(uTFZ9jNzk1^C z$xqZi0w{wajS?#%>qwZ!xBgKh`2TK;ILZN|%t!w%`YFWJ~l z9%G|~EkiQ+B16%!W*&g9pL^FqABo?P=y|crM|JOL`lC005%@Bpeo`l)-rVJ-yO` zES&9OlA1+REH_3I?suUT+M@HmPOv1oG(uW+vmW|Nz%-Detzla;5JhSM@XrrlX!}`{qTir6mxc2y|fblJ^+4oo!Iv)<~6((iqf0Ue9#2NA?Yw zg^TOO78lt|$?|T=N)_`N9~BqZ3bvNbWdxc8b$xeM^rjL`^`t6YD5fkn6lGbiKE1x^ zt4M7<+zmj}q_nRS>U8w-g3i5Nml{ZCPnEPav!x?#ZIEWq^%R-TdQVr`H)i01o{ojI|@t;VJ-@7~4 z=SKSgM6|k)6#v0ixJL}Az&TXd&lFb(J4KcL%iuZm7$0u_?W+O#v#iVrewJ1@9d6yO zA5hq%q|lw2QdbH9R*hZ`zzuzKSDuD4*L1JDpA|Sf!eCD|wY3uEGebj+8cKkWH#vmu zWY@X@f2Lx~-&QirM`ju1`~CjV?z`YnmsbRj&Edlj{LE(l(brRW#%qjzyxk`_E>6{~ zSMbn2tQv~V;0)QLquV^YnYEOJwc2N6z9Lvk|GrjmY|5~V|1$LK$@=Cfet27J3q?Fi z6&q-^i${oag-q41+LWesJ5^bhHzIFx(V;6?${K(Pq|a@9Ht&zIPT!2n)mz=@{Vb*K zH13|@P|?}b0!!e-oZ{x|g8$(H=P+rr$(H!4JQJ=C&N==0&95V49q_p}O=(VbhS|D$ zR$K=QCGtQZTH2}8yP~m-4JxOa!Qe3z)`Tcowi)lcYc z1rn9lG;Ola7G~jMvIzOG5fn!IU>6LH?$b~9L&>whDI3yF?Yyr|VnB02?MA~svJV_0 z&I@Fh$tN;v+44uD)3vTGV9tw@?P2qa{2bRBtCm{vy#ZoGL%)COZ4bVNNoB;y_26}; z8>`3M63xacKE)9D5yiKS$^9r-I1}#UaSgEedzy|O&|BYYN6wg8_k@qc(@Qel+NIN( zeDRNbaeb3SaX-A35&-P0%%Jz@|36C|Sil%BpsayVjz!ZWU_e1~T%DvXr@P<#%$gM> z)1xxi7*oS}T8jF#bvt)6BU9GG`IwjZf~M`#M+av>hbpS9nR^0_MnIBha?jTY0)PAs zyod$h+Q;Y!S>a~EtEW3>W?YGLZ~TYgRYPUTZH$rHxs2(Ob(uAy55ck~b0&%?xoqnS z175=p*DJmaK#7_?QC!@(?bAThz9wS=s!g~}cSqs_%DINVYK_|8YY+bNW%I_?a2J<} zO@2Zf{6$K@5RrUlk|;~WbH;BWUS{{MDf1+!fHV+839^9%-#1SWYLu;d`)mO{ted=D zOkzAbuHWQ7O{>d}Y&A3~v|5TYaMCA5XGL2tL-o(!oiq<>7eF3L0mf`Aj zVD3(X<{Oety53-T1X`+gHZj(`6$dd;%89(m%k?hEf|RY^5ZC|+3ZlK0NiCPAA9FYVaJugjS&|QNX2g1b-r3jx&uTg1gOPsB-bv15p z@45iF4#*F#%7S$5RGV(+PSvncIx9I{l@l5;GGHF@W|3klzeV1eC=<8y&6sbxnifH( z1lu$W6>4Omoq0J4lXLDluH3d^qeewMqQ)St!jd!_#JY{<&L?IScD!7qlTZ*cMDun6h=3o1WZ_)@0cn(wF7D$T zn;z`1sMLyzV%>$*4%dnQCLmdhEX)G1dFzM&_Em? zGf7*uDTkH@8In?NexE+E$u)3_&LXVi%sq<_r^tNPaW1x~2<5Iq)AG3%SC`kX{*(4K zl0us8|e-h zlmwMFEOV13FrO8adn48wT15~pO*Xt@Uo|GESgynsa5!4PlSK@$Xh!i_dWBhoH-Knxu8T<{ysvJeH35VWjTXJfY4A|f zWfqGxd%u4*b(Sx%XLANksT&JKl*Ch23GL04_F5?$14lOn5Aw$Xd)?w?>&pwLlBS}yFAzcsD)Q~XUdmyE%U z{MT@;b}Y3Hi$ihKLLFJMjo2A-maKxSjF4R)nGVQgk5&B9jwex!BDnb*zztU2817fI zFo+x%m*Z+o+}ruF9cXV|NQY=DA7g+~_< z0t*z?7Ko)CSTG=Jx&nGapIhB_NrFxhP-iuT7#~n!hkg-wW;JEhK+ZQGG$lpviD^`Q ze0YMBYt*%sm))G-o^%XQM^}#(4cyzH@z!G|>xi^k+|L%?va0j4pv95=L#eo^?_T;j5B>Zpi z(C?Kr*MNU+q8;HDe-d%59@x+-N?Z6|F^Z{LAfg`=s$B)I{xc<+t9@@PDnn1m4%bo# zp5m(q{y~Nidg3|V&{S}J*E5`BMdBWxm`U4kaK+Yb!}ozFLv|xDpOx>j#(Zuw_-ZsI>*sm;`9`M$U=R>-dEM$m{6#@05$-DOcEr92++TC(# ze}g%YZ;cee(^@?Fl~xA+UT~SqgMB_u7XIoMMMr?XYZ8;p?L9HvbDTULNaxG?g~IgN9wO0fat^HCsXvovy0H6&{j&&e z8Zo>otTar2bsn$XsV{gAm(fvYsgMBohP_}-eZnacb zt;Rm$)uZ@_SDLmP+)}*BJS%%6jF5H(3eOaG!8Ak7CLpr1fE%7Kqp=3&S;(`FoK|)} zQM&}y65MQEh?p)o=LZ>5ku?MotjHc`EMo)u3Ob*t8H)WNMmaMvYt&X=wk|4CAfBKv z^wAT1$A#ACdnOI=3*NLD~5H4e}^5zphv^BQ<~fP#PikoQ-K}79yWb(EIZ|!JtI@uNB#O7pKa6$6uJRy!2V>43<2!AvGHMHExDXg(iPO$&ftuhU)WyR#SiM#Xxd zxALC63;EZ+k;QL{knavP4ORhX;A?X4dqBQOP4DZVK)Zy%1O-}aAB`wD#twa>YzW00 z?m%LbqM^wOp%|G)c;ce49asjqgcSJN*=fV_ ztt;k+@OC+D*>ATWB%Je1mV|h&)^8o`@QN7kPv*;CB=Z%6k5Y+l?VSTJ)3Q2l)(z1| zc%=Hu?b9Q|m!fMTp`=&X5h^(5*}bI1z3w~III2bS5TJIbOFOh78c0bjO`S)1XdrKm zAEMQ*bKauHbc!R8h{r3cW&~}L)ADm)-EV8&Bm^`v!#ME|27CNsZB(&|~~p(na>W6t9xb^773^OESpv)98MC z@V|y;sG|;0BGJEAy{tJOqImLaUr|$HjpFqPbx6`2@#-P}f*_wWGUFpEDXA#4N8ki% zv{q_69kD_RmH5~M06f^i3bX)Vi*~=K)X_x!*X_7M_)Ov1Wy=1m-G{gYMtqlZ9BiHb zNUe-t^_$`gej^vC31AMGTSc#Vq6ESs3bOhFptH`3(O2 zQ~$O>v5EOuKsaTrT10-8E@1~Gu?Qn0QLV+gLBQKrI2y&?#M)90@D8$-3?8JyNE4h; zmxa2#D1eCX14#IB0JWs=kqpX0&1xh}txMvxz-Jt>RDZ8XpbsDZ`Ozmo4V4)O><;yP zks~r6{iAzeJ}rH*?RW?L%5L?8^*Z0*bXIGxGu`IZj=wN*afe)47brZG%^5hDt(13z73@ALMwc&yapt>sfgX-$ee08;X zKd;!i2?lGIAf>S+8zZObsxNVISNP_&sRT@Fb+DH5xxHY6p6hHTg`)T+*b_8BgG zL^I!>dk}wbz(=cNvU%B&Vl@_#d2cWpZokPEm}4baY=}+6`DVd?W%(jG>MeOO7PbQiH)CAI{YIhAC*#NtnQTx7=TQld%+ml$e(Sf4~!U@8ehqfb{$l_ z>sO&nae72Xb592Fz1xi`&#!hH`PpY~GHGw`J`W*u5Td4qFR3WsIwZQ}v2FBc_WfCv zjC1n)`rR~p^5~kqk_Ru~2Dk5Jo=yQGE5-`M;f04L=(esiqL$n4=@NT<6@}j-ec7IA z9_l-N-#`2;h>#(PKgT+(KtlKxkN%Ic=ei#Ht!DwxfLuT^10!^>;2jrY<`ymO9tfhZ z0|FeKK;Z0s)9+d-1xE~~JDP_`ga7!FP+l^b#HTjdM=01yjYE%k|5Rk{gbe_VETAGw zc?CE)l^q8s5VAjSbqbGXcPKWWD-K_J9@_bhzl5g}4$!p1Jn9TaBVlp|9Hd1VRh*_f6D>Dx zG3d;G++SG^Eu*$iOe=(4ar#5h*|P#lGFTq)0WH6g<_s1cE0_>mR={e^_mnh~_N=gg z?Twd}hQq}`1`b<)8=>#7d7tCH0Ph<3K^QcEq%nc9|0oxlmNg_7t-|W7q3mdlAWJl=!2J#a@qN>%zSvvc zqKWprf^Ji@FInBhAOz0wGG85=@$4kd3a-9@oW0Cm5sH*Z9}RL#RoO#P?DGSm@+OAt zmr!X*t!k|#DxwHb2Xr#yT3%Mxj10fCi>vs#x*s0#RH4ttT2W9Sp;b6pcD(BZ`O2i~ z-d^by`zk`_T$izUYKeU(`(A(7T!5a*s#0(?AGEwaT@+qBA}+1zs~+7|HnRv0DVgEt z?jKc^K9wAicg6VBTquD~<^Lh(5D@RM>_e2m8edO)Ut|C3j)yt zTpeMyPDxSF5tjjD;izjG-RB zXQaPKL}U@1gs$}RFRX0{0dP71)fQqYKL*NMaA{G9c85&@A~-0guRSz`+P{-iId03D z9&F%5goF+}AIaa@+(m7(AhH$oD2_mb{AMz9tjjj@+Y_OWE4PYI1yFpxLEU@WBSt#pKPEN0+|rW0`ZZ7yBg9Y2v?7G#nPH+z zmf}=Ed~K~J&3YNkUt4Vsa^3=6qY%%!Y@?VQ(y99ITx#jCAA?Mn(MMetbP~^j39MMG zTQqpzEy{B~47N|eaie8h@HLu?i^3`^FW2`&JPzQWn`LskW*^pSMYd22^9o1>Jr{M2 zihx=~^9mrCn&(SkV)F_gF?tSze~r-wE`biM(vp%V^y6`Oehgpy5XTvh@-N%g(6iCV z-vHS70vl_%U-3t$p9#As0d|5VKkoTSea8P9Of2c42_I5{g(DLSK^VXQiFW871F#a* znC{<~)fL0Etvb`m!a*}i@YmJ5*&U(|iu8?vALw*Pg!C~Pqocz;L18{JE!kVV?AW$F z?1fo?3F^c{MZN3#QwA}dM}A%r4QS}`!DtyXe`id4Go;e_EtjS;8qLCleLb{d{y;!w z#lyM916mg!%&KI5TLcNiQFGaQ_|c$g-_Y^R>e@K^vi2@#Pw@GWnNic0h^c(@yW^of zmo7{{BTV$(@q&UX0qxdki03S1TM6^CrD%2zIPrz*3iMUJN3H85z%oQQE& z`q(uzbX)g@u)ZgdvQ70%gkfTXv(M0n??;@=9kGQ#Xm{2*j#4)O|ItfNazuMaOCHUU zds5;|iv!Qeztg`>9kVVUY#+#+7@oARM*G~4!SuMB^(&Z%F6Pg4cUDUn>3cI?h!o=X zOnDTZG9wSpcNJ1c2IaeBTD4)nN7I{52NBJE(eqykJ%`^AdVg_ZR?)wPshz_h`i3ja zioBWYZ1rS6Z`}WPh~Yy2Xtg_H#?By1QoD>Ch~B8OqGc-L+9YuI-us)=5+ zgF9Wzj1N36#i7`(;;wL|-z^g?5_XHO@M{Mi!ee^5(4*vHfU$dB792cU?}%DRA%FQ4 zr$;YR4|K@0d}6Z!`mctOX}plUpffy~fux8bn<*MnJmgSAWW)eZb_MeiNmkd=Gd-b64|Ga&eY?6j_HF zuIjxR7Aojo12!C9c$T|D+_*zN-a;83#=GHvWL7f>P#wf5Q?Z?>1SH#e7;0Ge(AP;*2&M_g+^&V#< z_hRO$rz~hDSb_`3xF|NbQKUxP*GVewJM(C8A$wz(h=7- zP}?}e;D`tN(?>dw1Qd^QCbRGyz!_ptocW|+ySe1~e0c++W+UCe4|Kh`R;@Mp)-3VT zC-?#M$wQ)ibldP*)6lS_UD|>wdM~$!9a>z{?1w1PN^-%)xmnsCjxETFRA*}1IK>q#%3FQ|hF{kX zR%yc7x&uy^&yCwiXEIBq)p)PTd?N8uh`xJ*mO)4XNj=MIc!c?7qdnKOg_lScW+YO+ zX?fQd5?~BNONY@s56>Yg+c(~Az7Lj#hGPW|UjTd0`aB>8YN~o27$Um!_44}f(s&LQ z1RX-@PgKgKz!8!BVJnE{nR*OyszUk&GjD#K?t!#`UxCtscgpT-_`5%^xq#xs`pE_C z@!d6)QUCtkikM5y1oz{|Z+`52vR^H3ZO75;@gTVtG*~y70PnsqP-03QnG%s(suQ|H zf!*O$yO);RqkvGJOxTH{L)r8XLfuxFBwf3(^icV8kcZ|;G0agmjrM)2X#^6Lj&;P6 z_jA}#n}0N%z)?mEs+M3NPtQtfh0U5;VDoU0If2nrwd?F$^CON-mdqDFPm6Os=3(Wr zAI+?KHql3d&cutlN`;H~Y@rG}phd(kN+=oZpw#>b(1b+I=g}}y8{PzzYJ$ZuaEF@7 z5Xk5ZBFqfNMpLFIdOC#C=GM6ZYG`;Ll{!7q+zM6=bK|BkdWs;1rtG{9Ga{bPAR?{n zr$Z<=?s{jJ*d@D(5av^)46_B_|H63h$OA;Eo!!2~F?L1LnH!zkIIT(f_Qhu<;lQA& zjIt-6B(nV`U)pcPr{sDoFGALA3)~$(cN@;cc5M>v zee-+iKnrztB_eyu*@e+rI+52mmmfsASmt(p*MZU6$tQeyA*WO>>M1RqJ2An&18ErQ zqP{=i12h)w&%ioCCT6MZ5P>x(I>)-uAzE00i$6?U!@}JE{}ovF1J&&#+DhcR&6aaG z6YGfX!Q)^07oD;_1-i3Fu}`nB9qjtE`An}rMfworqFqGGtd0X1BAyN-?B*{7fD*j% z(7}iTEKK3(IR=Zrddc3@`!~vk#%CBXB^uhQ=x*&#cimwhaC}yrt{Tuy9k5u_oju?c z*5F6&UxU$RQ+ie}docP8v>2Xj3dMi=LxcWcahAxeVt;m_4M#)MuECBE$DgiTy* zmOe==Q9O}pb&8{I`MJPkVpG`C5X*nARyIJA?*Li}(58&eg6y8;WC79e+xcpTq%U;~ zZ2FcVifjXl5fD+e+sZn``iMIIpRHD!G(-M^bMpx5Cn1hmF_v#Yly*FF<@`BZvYxoT zD9y59kySxE&&;LJbjJ``*duz#2f#&-6vp!dYaJ-W!o4Z=nC~q+yy**1+eB&nzEjJcM_)NZ@%S+@>k6nc@In{nE@&weSP1|pw0}CmZV;Obf z#$N558p?T=d^^5uJW;54*)X4cC8Kf#;RF=Pf@o#xg=&}ilpnQFQR1>0YIH0MLn#;T zGZ&k z){?zyS8-;-{pFc_A~X0$77k@+A=3wdv~*A3)gU6bWoS_pY$2tQ9eNfKkPs$7uYJG3 z=wXM65Quf%fiRh89u~sHig3K$Lq_a$O;iL@&-G3P%E$AzQ!B`q28M3s@3>%28L%+b zf;9@?-!F!}b^NRMr(~T*#j>Eb! z>dw$8Ap|Ip%uejw zRy#3}pO|@Z8n5_;%8~?Y$?{#_Z*WQT8Q4O_a;;kNBiiIXl?LP!FFwL6`t4t06xmN; z*LgT0X1gnWrYxy(l5WZm6>OkRKFsGUvP2sd??gS6VK!@Zc=&G)@(8LhJo$L12Im;a z!FC>2KTMNstY78&-+gK?v`6N$C={=n(@6SiT1kqNvf-$t^%<)R!KJIweII!Y#;qp<{ z4EYd$Ji&)b?c)vxGN3)2Y~xcfqz#97))OoYmSg>k$p2o^61d_$DYnZQBgL$q05EAl zIrF{~)3*#|V1Pq^U2Ov85eh5hm9B@z+?1zDBjJWKx8jQ6K;Lg?slRhlv4K+)OG7-D z=|bzM4QFYWP*Pg1MV+yc%3WP4!p8Uk;@oySp*P zr*`u-!7$j>`^&*wp2?(=TvIA}Ibju{ zeOF)EZN5+6*M3@*N9_kQW-iEjI1vp?jPXNVS2uVJbsmV>6tMI986k1>*XbmKP!@s6(u$ur>QSU+MllrW@ZjKfP!7m!<$|Un=p5*NLImiT3ld_$o>>rLSdFy9G0W)> zLJXKm^~Db=K(_C#Xi)PfSn1G-Nt+;pPF2hi0Az%~8V!cXK%XH;TfLbW>&mGT@EG(k za%z!E>1Kl<&sxF;+OzD>TvZ3Bwos;1mrLV0Rp;iMLryq3Bh^>|ydo&%DTPx~khBTX z8iGe8bPjstSihGBlY*lcHRy&e(%$BGsq)Fn5!56x|JK2-wYOY&hL)F1^7_+&eNdS4*A!VU4+{ zI2iqCIk>FdIMdrKd+h0HT6(_mM@gIFO<=?+8O&^k_4l+AZNNwMgUYvc(c&0Kqb>Wc zfi3~~%z+b3oF@nME!aYt&p!5$emmsx( zjHI#)JRV9R$O*Jsq{8C<(l3{s!k zi@`#-WS^&5^!J%BkBi}yv4zlV?7Pgum5qZFJdn$RR!7Obk7gSmK|A~%?o#duTyDp^!JmgC_XMu7ZrK& z`v>5#wc#EIbrH;<&@XYXr zC7I!^qw<6%mXWpLl2BI)N%&mwsAK%h-YJ81)Pa?XriO?4_9Zn#0$zRT9c|wLYeUSP zfn*K(!T??csBow4x63|&D{upa;Mje3AVWgC`F(+7+%D%FtVl8fGSb<|)<>Ndy(#ZN zGECz{TZ7t2F>-~52=m887r_)5^z%XS2b57jw;MANOy2sjiQpahd|)#NorIWEF^*~w z5iv@`bc?*@$)JU7Qs9r|B;%{-K{_SRN(sv~#{KZo42oZUDw`#+_lmQ1)i!Z6*_y2j zHt$A-R+~+WNMjj4+7AKqji zUQH1Ow_F_W_e-z-*~g#UxjDw)U3f@3b=5X5Ze7014$6@04tb9S=k7Q@;P#U@^8UFK zumM{urEy@($F#D*IwA{-CTwAq79W6-P3)Nk3Aqm&HUAz^4)Ga8dp};t0 zz3bS|RSz0E9+*<^$~#)LDimfRFb&0y1$qGKA}=WRqsWE|j)qG*=i6h+gh$YLCi1KXWa=*3&p*^v~$4S~75k9(c>@u*;ee zQx}(-N$&Xin#ly8POWOc5uu`lb9V-xvFBP}Dt(pJJ*8@?EfQEGS$`4By`~9UGFUYK zU{mNAuNei+8h=bJuHb+X=+T2{De%!r4$~TW;1uD3ss6^lU;GF19NTbA$j}!!MbQ?U6gni`B%B6~?40+qrbyR*@Q@u?sI4+N1si#rbDsmu3Va!o>svT4 z5xICZQxkOQKr3tOuChlU)y&)}FaSZ9yucE1gMZLFo4PYVfWsYHhG86U(UJ9F%_n-G z(i8k@fJ*BrB^VSMsDO7Fh%>%NM=%COLXrf{Hs_e~0w-;~yc9Uv5d#~6^)gxvXm`2A zaSQfuh_T2!6iutB`lYDaHBokpF%lr<5IH}~^Mcp3aMEw2p%YFv?Ysx-T?F5RGvCU!=BaXm4K7TLkp^Z9Wx;@~8I> z&nQ&f?63pdcFIKSAP85%VUHYbI;L0;VpTM6pp+ri>avVG^s;&t100}>h%f@b%~jRA z^c!fGSAo4_S+w@s%iq&VLd+-PT*_^ofZ%K)eB$HN66$DOwfWd~eTt+OA0o~2 zDLTIUKVXZ$;i5h30 z*%B`{+cF{^Ve!&g@T{jk7)aIsa-*ZjlbStWma^CZH`Y$Ab>n=S7kYay`@D!O+#NJ< zkkb1)oUcgm(V`A)XlyQ~p9|TiX6v4mf+QBP{rR87vrpn6zn|Z^Nf`GAsT!t4kHi?2 znHOYKXeUqnpq^ct3*UP$6o)@(hx(pKqZDRe4_HZm#$Es|dyv=8Lh)s7cb(MtU^Sal zF2GBFja7)_R}XA^dSH!?GWj}XecUzaF?eg<4(|zn^-zc(dl71QCevG&bqZeWL%}qU zgfIl;QU^N6N}W}uP$Wtd@y|&b^b7Tw8AJ)69dx2kE4})>wCQmik5~J+nr}`%Kgo(3 zB~J~4^6#Ka?G(b9xXioN9G_sa2cd3MzT3mUb7T0cFA~kfxgN>!+s{6pHQR9{c&0c1 zHu?4Gz&6Og8sdzp&ruFGZPaFYq<5;m$c#KFeq=8nB%06maHd`DyC=7YZj`W^Sjn7H z_2sdxn^W$Q{=SxX9c&~w;iC7c2{DeKZHF zG{EDMw4Syq>p&0)_2SUkK2Vm^0l7Kk0=UHea_)iptB=R0+XRpK?$hv49oWFTa_ozJ zmtkXTy@TGJrJBl;%PaMV^vhA9a!wJx{0yD<<>ejVNee2|o8S9cECvsxzO=358U!1t zj;`MU5GyDzfE?_%_;S4MdO^D;R-CEMoTH)MV ze;7R9L{@Z*DhS546Sq^nF7@=tp+xb}Ie_7PK)Ccf{k=24pBZGzrTXceut%~>w(bwm zY473G?5ezQzQ0*9vnYR*Jk~Rv*j3A#c-B``sh?#9xfoOqR%kULz`3pzE7kq2AZemA zZa!8PU<5@FZ0++p^Tgv#?x1#lW!{7qBIPlZIfzJBIuYSPyA;kutwp}(n6cj`rMxgy zDX^riY4J;3#XVk938Pr7O7sYks}~tOyIw(uoy-Umbv!rypFTx3ee|m$wtw#;;hr&h z_0UTjPfz4By?}}TiZqBSFa*OT_!)>)N^x4rF&LXDmZlnyoRv))3j<_v>2U+0<@N!^ z=HCi)7-mqgFskj)r`|#tKdQ{bZs}k+1KSk()c^76e5^+L!x|z5N)=Q}s<)l7ESq5% z9!cK=gNF?A7`Ilnr&``7>~Npu`4m<9S+RUe+>zcbj5^bxfqsSNkW}EfjFbY!KA2o; za_?-_`wBQ9xcvQ)n1HeMU98QNScu*jpwfs(ahq2`SO z>q5sLkfQbHE>2(6&psY!2^&y_GQ@WqAJMA2ubse11Ch}ZT^tQXD9+;>+26=x1IxP< zlPZ+3>K*nj5AMUkoi7!9O~L%kGDs`A>M8=#agAIfvh5VgBzl!uSsh7ytavbw@&lAF@F zG2%HWf_$>5eDJtMJT-uShf!XkYjqE-d#MZ$?@xvYC=()=E{F}J2?{WwoN-3ra2JX7 zDxbl8x;`!pps>fv5eN~`02VaPXJ6QA#V}(bZZdN)qb{Z3XdTFFL#_uE4U{VHy-?7f zP(n%h8im}zK>o1JC7n|@Xmd{odef*{Iqu5THq1V2;QWJAWoiYZs1aD&YhF;$sxiij${tzX@j6> zxdo*nk@B|ZAV1Y*kE!%*wgp9Cc?2}j$2TWsqY9>or`43fH zH8u8vZbVGi_^a9>>Ldu;&iG@r*I34#1*b@-gCFm;BqS+H--3L!ax__oFiFurL7f4< zKDo|zHa|d|fEOeV{CW0)D-c4R1DvhS)K~#kkX7B$Twim9F-}Ybp9A2JCUrnX1v?lecTc z?J5H!Oik8e9BtpS%dz4Usq}Ma@_Iz0S5?`5y=!_Z$LOXSE)VjqfJnh&%ax;@#@y}9A8j(paqH2{ZjL3Z4`8ikHm_ozqNT&tk#I)XLXmb=-paVkX$-+@Ol zc<~Eayh7@?rj;{0Cm#wd-e6iP!biN-8kNsPQ3tX+r=AY}84w39 z--FQ2vAXhR*sV&$4+D$E9px^bm5;`Kl-U;CO?Y$nbdFcLuge$nIOjn~yl6?jJz>pa z0xG8F!y3W@$^XJjD{%=GEXP2#8;c<;=`ozY!D6GB)vOhPUgmORcIYe@uV}76N}ZYO zKMIBcBjmtI?z^4Tp`L@rdjU^e=c7b1G+(mBUw_$+_X`f_%+U&e06SFgr)V$1GUoeeH;~Aa;zGsQDxd5xJ3Ce-ZFM{jG45JwZPdoGjTFzx4((A9}i#eq6VgJ~CGsx~)qQyo56~dbO`8WR`Ra2juQS zAg*mz9t4wEfV+FTdISiqn>RrFdK@x3nInP@`*txt-^XUr|gtzSwQ?Y&)(NT z=Hi}39O;rJ!YZE=l2rzUR}Q?dkx<-11J)TOSD399W=V9b5XsnIPg^~{Y2I$-PBhO@ z!&Re*KbzBCLY?cM^iZ4`5X{z&99^eRfe_(Hp4KL^-*B$CuqSHxinfKa#8w^aqLiD> zmFn+>uiyLVjVW?Bln<8#ex($7~M7h@HU{Jt+Ht%xeW^g0;Rd4W^W191nja-HwQ4Fqj)cS=3^JEtK)d zpa+<&? z&?*Dcy%iXRXXovN_-z_$_=MQOxtJ|1oz3yqO9zdBMj#2&3Qhv8_hpFA@?`BGPuuL} ztih7!X*vb51ajYV@BT!^slXw{9OPVDx2rG8r5o~hxKJbyIg_<~@jv+C53|ayR}eB~ zf=P^kslM3!rqIZHMfjS~+vcqPQk9IUw~ZO+KGE+A18$f+r)qvWl!}7HD!c2)3V+3O zxK*xY@)=ovv$P?q70+g+h&;{CMWJj3s$5uNh!KVw8dVSr?mXk02=U4w`5oGK&4+S* zzL38QM>o(&my!=o1ZNPeTt|c&aLIy0AF*6bu&;&mo|ir=?^ESe4&LI&?F@BYzy9Z{ zSS8PeetvO4JpFjT*rZ36aFr--zeRkWtjOE%c^4wQ(N++vI#-^*c=2dC}_c1u9i38G_E%y`Wt zM)U|3Lkb?0;3cepq@Pjou0#-oBA@D3)wke>USukAAtgiA$d$ES*1~xR`BAkbefMP6 zbYdW*pX{pIb;6m6dWneM+OTC>@nUhHNrqPBrDH8SnW;afud_OupgXJmW0|^D_O>HV zbwSr!bN}7ef0c9G4%NRsi@F|Refhzvv?xQTbI<;AX@%}{s}s*k3l{%!W$52Zo-Mw5 zs4@5QXPbZcw{2-$l7nfi;g8otzDgAgH;*n2^^+5+sqMYPS5vHQ2A2L`gse78@C#3P zU-mGz>{DB{#rg~V)XK6~Lu-H;@S5PQNsZ|1ulRO1-77vfC3XF2V4t&=12z7%H7M@t zuM;sH$NOaXpjR28#?l|--AxJi<=O=>2Z59foCG*XPxpHWaDb&ITX3J-^ez4^W+fCk z2pl$SQ3sPi9KI zsjLk@QBD_|&$9UPA6m>rxiHW+ROC_cthvW(U6IGe*|+*;6VL3!n0GIdeA~GKZ<4*eBdn&0nTC#TV|S(+p%L4=`dK*=MnBl zkv%BN=LEld^;(`+WNI9E+l$)2QnWW>cBNLtYnm{LC>d$FI5(^B+0k8AT9sF#vu(ul zUS(B(uVmx&F9-4OnjUqy1^(KQ5OEEUsPLz%f%5D`^hAaND;XvMEFbqZzXd@KvTW<&x89kDmC1AXF zRa8?;x#;s%ZwJLW4~&r)YM-vZDAT(u2R5uk#ZSl;phD6rwhdOiFMclCVbFEpv>lM* z(fbIue?AvbIwfek(54t*c)2O`8OJ@(7#o)u%3gbFRgB#amG>_Z)lB*^ddO`mgOb;6 z6v=jTQP;`~Gut|jwdAlNCJqd)~m zcVdYVxbf7`F>|>Js}D>WcxVAkAfF&8bfoWo%YXg|_ z=k^=Kad?7%ei6JSl#>Q1;~CAvpt5R*vJ5)D(5e|X;}w^zy{B^)Dxw2chAc)+)iJq9 zF*@-^G1G5wtL2Okjyu3EEDi&1oQ1!$3C3y=F8f+CHkqhFMHyJ$7Au7 z-E6twI)kjwp6)(&W98$|zCeW!Hwy*~I;z}~HS2i(w~x^>BN`t_DBivKl-!=_vOT58 zB`Usoi$N-2ldjiMtnfJt2hR@MfY)oqKCCtKo;~YB*LpDaGozR)>muLN-G6xoGXYz! zYdo8q7`;Csq|82g!x2A9j;FF@gU$7(M;Kbt=|pK$Ds^0@;8{h%-dQr&6>Kktub1t$ zr^q2M3@^--1r(nR6SM8ZUgu;^`JNmpE`L3-+<)kH_$!lvjeCXhbHf;j;1@vT`r59T zYWi`{sV9YbO^M`F{lmtTi_`L^(frvfdJF_j-R-H5I~RkWt4W zRyIwY_HNS{em{}?Gdn8cLom3$pDauE2Mnc(Ruf|fA3aJU#oG*?c1`l;*+1SNJW*wW zA_IM?SM>d4QPQjf@A^`%VmSg2>Wj(O1!8tpcyB2zQK|nlR1g1C*!Zil1#0_w>_t@~ zueK=f(&h^W@5_Isnq~h_s@ZPSh%FEK;}cX*%#Eb^4(kNW)Z7rZvjW5&X*ZHgw8=dw zvRlxsuBEp;7kMLLA-55Iu5VI$D1YvuTME?-%hZEtm+M>2Gj@7r2!wY7!7|QvyV)ga5(aIJDS{^Jnog>oU&*nwWOlnx z=g?y_9Gj3V)IacdcesQ2vC#lZ}~_|o)y^g@hfvFp9p4>-qK>ptOz zqnOP;>O4>0AffC=1zC93!HNbCSi_A1#c~4Zx}8) z?*f_rE(IwpVRJ)VH~eqYam;No^jz z=Rf{RamKCh#K4V?`@m~wm2EZR*auPr3porCtJQ}I&B6aeF+US53m;b`nok;cihnFO zewPJ$7pybglXY+p7t|xs9DuBInJ2UKz>ay;!T3Mb%D$3#fVBHK(NI+;A@7+OVY@wU zPxZ>k4fo3J>J|BfHV7D#EIq+VV=!F04m>gpPq~uaeI#hvm7B=e%T1)_hbH?s)Z9Lv zVG!3|cbz{k6QK*g3d`_f_v$}E44)%%4}KJMF1#5;?yPT5z%v&_2o#QllIq7Lcvmd_ zZTNBQ5b3fo(Qm>&=>0vcW3=WUAH4vy>5-(wg;y;$Lpt)gVDn&59D1u#t|eZU|Yr%>mh9-}lHl_I>wcyws)- z#s$X`7Q-xm3Yu`iZ6jdkJDb$+rg!tq5+;VZlS?Oti7Vew5Ny+gKjDWY8@K(SPNM-B z^6&8Dm^sYuM5G&?J~uaY5Ko_ZGGyR0T*?mx&CmxCJaC^oSr<@^^1?*exjO^U%zgPsL@b z9N~d+U6G5xwkcAym{sY7w9p{ak>e)|=Y6{(ANkDE0vmeA$C%S=@s3pCpvk9ln~LJ<$V~`6UbSgma3Vg4>O5M|^SZCN`pNrY zswRAilD^8X_Qp^r<58>I92t80_jG(OJy9ii$i;9vnEbk#);&zsu z89=N0p>bw4EYL*=*WOijU7qpbvw0Hb28Tc8A^ra|a-d5XCDV|7Y(}(7iLiyabhoK` zpg@RHiealgi-A2`3Inwc1yMeODLH=}@km0x7C_U@vJH+!R3$qE!P3*8J zn{A&(*v&IQ%|Hpr-MFHH!5dBLk8-?cFE=FO<|fgAQx{$4M z?meO8<1B)P0k$c}ezIJAMU7-!)2{!obB>R$ddIKj9Cub%9D+EVu?rIRzvUbTnUH{= zGl?)xHJ(&9fiQAhEww;35;+NEb!$vz_rOfQ`p9`wLPEeynv^|(SIMOh`-DNWzb|E(51%>}RZi>;8*m1&Yr{FAKX| zKT^t8i~l3QfXWB5+#_#3Y(-)R4_d#AvVndJ&zY2BEHsJBQ`)(k?EIyc?4s%)#K-?l zu{u1EqFh$o_Iz{Yge?au4p$6S&*kP=*=5nuYWh{d3Ozdn^#34OvEmW^AF@VLf{2Yz zpAS)L<+c$P^>KFzC58T8PWiZ*H_3PgM!0&ZzG;ZAj;9h=IFeUzLB{a5b0srJlqw)S zcl6ZPgA!(FfgG}k5ER=tLRv#dqkG2hMa9DB0T5hhR+B z?YzoL!ootMNO}p*2S1Tak716FSQ30GjYACgR!RqRj{GTB4AgPx-IvgV?>TuF{g$ik z7ocG9HMLiRG(8$bDuL~kv&Ai)3K7P(m?uTpqI7`#0g5GaI+KS%OV*+%5PSb@yJyp? zp^f?f2}cT8QtYbn>+djk9Yw5S>1&PTy(dJtwU%~r zi_zxS;u#`7Nad|V`m+M6-NW})fI=1jf0u>;j+paxD>$*Cq+)4GY1)75!lwSEdfy?r0d}d? zrGRhdYt?&@lN^kFY}r2iE8qazeq33`B-S+b7V@RYj2Q3ck2l5KKTX`KWOp@>Q292L z8?9LNI(-+Dkb2fHye{QSPytCcczq)(iR2-pUZ7Uc8~q^*clF~>i*#k??_~YB>%M|# zu&k5=EZAldI#BE)Kx1Gzj6FY@zy=RTH(6b9B&e$rqD&s^$8p^n_b}v zT6kN*tR>$opGd;W2mVa|(B38`ZwRUPqbuwhM@a@NZ3kW-zoigr#h)r4p6twH$a*zx z&xI@Wb5A6wU5s_g7hoF)sKji2_uu6tIU~K=uHekGJF$=^9AEXpaJp)1q^^R(KLuXi z5kS>X{obuWJg=8)gTK?mjT>*-D_EmS1%7}1;ZW^$Rc+%To=Y5U~!rpN&oDwE7y zAaOHSR0#97VWPO{Jrixovc>@f}|AvZ*krBqLSFnI4 zR5iRPn>w&Ru%?eWa_pY}ggqrQ7QbZcZp~O8Z)~r^V73F1S9XTxNLg0v%9lCHzY}(m z@;h=Et!lps&aM$-8Sv#pO%M!|$X02L25ZRb9L<8}u1u3UA8<`V2Nw#C%#$FA{lyi= z&QbPA$*|-tBZcEOrTjy^(cHqL^_C-0O1z6eX7tT3At0`l=Wdi8Ip=C?2`hRcrmVuU z96V1w33%{R_tY#_zLKaf97M|vt>|+3c}Cm{rVf76w^-`0>Wy-3FufeL5%#h9)Vfmn zJMx72q!#W9SA%8P8*;8pMRcTuY>kuHC206KF*JC5GSae*_R1Z`-K!|sKsB-T>GP&+ zclDxyYz|>&rxv3(4Nt#2)Z_DxMrqi;)Z?cmb4-i*unj*Cw9K|JzdU^FOr<|$^}mM& zF-#CO`&y~wc$?;|7^9%Av=H}ld{RhPMw&m{l8kG3znL=31LL%l@WUGEgNye{l!lnK zcQH;0paQCXs_&PxJHCWgn}UH{Aq|XWf-yjQY?ohs(Z#~*G6B0iTG_OX6*cIB)q$Em zsE4d_CDi0)H$b9~%#KiN0VZW64_QYw&4JeEu1HOFua8t}$hN$GUhY2IW?ZUkoyuyY zIqYJ(j)Oy9DZea#u2n2I*^hh~7j$v>t(UDj`n4lM(9wcwB*4uO<74S=AA^7BcuQ}D zHcIBH%nQLK6Gl9>`MpQKh9pO+tjh+$ykr+MV0WWpES)Rn6o*yf;T?&t%%-QFOBV6v z{)~}@$P#zasyPnA`~W?V>D~r@!LqUoE-p>hWB6t1EWXnEvZacv3mp36lsbVcH85QlV{mgR+dns&L{2*0cvgtP%|dm1uTA ziN;4y?!n=W&N~VZN689h?J%kLT$EOcwg@60TT%=hyNgQ*>TX<^8B4A8R-(<%Mz+xx zU6eG*O1UCcb5YxjT;1GydwUlwDE*e2@4fB)dS@9vQ^JY{4uus2S9*<940AG;v82z; zRgrb!!5w#-ep1{Tr~i|!-_K1`r1yB5XELSz_@$-XzEa=UG8+^O8jtOwJg@4j@|zEU zjh9YDn3qOx8zf^bEPK>tknRSDzh?}qgz0VK|?PN|-2U*=`q>AB}yB zJG<781GOvkj(L5?mE){Z4L2anYR3%g(v5i-y{((<8>G${^PZ0)DpVA2mfq$DAq)|N@RIctED#k)(?XhbK5F=v zl{%+|DS;d;;CQe}JmhI3IqRAv z7jvhXd>{=?z_@tD>BpF1u)R9A*5dS%y{mWCaY&u0VH8 z1pYN^+^_j)hQ9oaXEY|+eyKmw(-rspC0`R}19StxD};Nz>|z$@m z5m&tJDG!}sh3fz-Kz7%(GJGIVG#D>nR>r&7!Hh>5(?`qln|jqOMPL6s_EI6Q8zD+E zWu#$d+P&{YmAqd5wd#$;eywYrf(gv_wqz!T`4aw&QOX&w)o(erE(dMHl{7o*d>?hn zK>8nUnri9*?fjeX3$Qvdp(he`V!}H1a075~!qPRX43o5Tnf|)a@1J7E7rVvmv-I{~ zLCMXuG^=m9!p3@AmWPchOGT9IMk!MmyPoKoyPi|=&&$q&3FBLz2ha_wW1S8VaXben zaDgvOzc5kVGC^h1Np$}b3`D{)=?$~?d2g0Z7uJQph%Y^eOq@P?N1%c93-#^Mw^BwO zI@eRAOt2R^<#W_wc-q6(^ST0=+6p0Hl&RMV-(2+grelNG<3L|Va;5L%mAh?2<;(g? zqK_DFU@BXb_r7WpP0Y=n-bEx-E`lVtSVJfucKeqzZPwkI;BnHyxPsc(rDU_P3}}LA zz*iAPDX8Cx@Zlyp{)Cmb)u(Wt-8o-O@3TFrkzB%TY|Ei7=rfxs_I^~WAisT-*D1Pu z26^?YW zs@sf6or-<5JI&*DNpg&^U9@!xkUeOIQQrMIP6qBnk9FmPUHba+O5PVndxD8suzhwX zVY%8-uF1z^%l{pe2=0-Qh>qD*K?^1hSW%-U+}vrkNwslP9JY2=WxlhMPg`oU?Ws}3 z{^b?ndlfUv7ypXqu-B9IPG%=u3tO6cv4vOn#M+%1o28h~J$%cvWn+4%0V3 z;uBO_@V0)Sv|L2Rt^J7r{|ze{LGWU4+;d@S6L=;)vx1B55_iagZ=jURb+-E zkpK%YS{;eK8E=-f1w3Jk#^;`Z{A=Y0QFfFVp_YwGI6@Y5UEg}sIMoz16j&E*VyOE-kD~#Duf!EDs1H>XG4Oci(n>c^TT-7^wXGKI*(|80 z6KHKH(O#o;W#%qv>rW~wuCA_r*#Wb!5&lDi2f=N$Myl>SxvQAO4fy}R9BcFi>FH@( z>;=*j16aT@5&LLBZ90u=;l)hG4J{mFRA^N#coPGu?X~Xq)+?Q99yY7d0tAZX1Q;=c zgLu_A$!1uiUnWPP>>?(lO}>KcLF|Cns(nv%k?Eel` zPGr2kp(X_HIkPr_#{y61D6u4XlRSLf{Yd>Ssa3V~Jo+=N9Rmf+tbR{_e_>J4Sk6M3 zlh%6_ECW?jka!%2`e%vjBfEv3gU5Vn_rJ=%+5&)mEvA9f38ghRRIjV%ADUI)KS`Hs z@_R!kE7{UsR3wPeDC9)yT`hX<7&GAP?ckkRx*a;5$W0%Sv3XUGme$$*Z{)Wk<5a^I zQ&I$UXllcVGiW=$scNo#9`(+gfcpHW_WoU9?#xg#N4+x&0;r&~_b&ww=+2reiPt4Xr2|Wdlunj&J zss;n`^nP44?L9|Kl}l<@>nktcExO=E-o)xs{wK`YKsGY9)H_@1GxU^Yb&r6J%!C^? zVGg8${x-|f-%^qOR>TY4+Jf8&buwXOM>kJ&0%Mt4(jByjE9t_!P_sk6AN}YBn4^*U zY%k5vOyr4)ZMS6(Fz4^8Fy3&ti7fMdjG6t5)6yD?)2}YM&zvB57+iJ0;`>%YF{0FW zn?>>OzDmUQA|e(<0ox`@SZ-VJ#~$Kq6%iv}9!sOW?G20;4WKUcF{*g(QF{oBm*WUN zjO`^nnQRg6zr+`W9;-xb7T6XFlT-;KR$?O$7e6#ShoB(X3zVs5!Bj+~!MmH%t)xh? z9I{^+5RUKf8GWbff8@FdzFy3R` zU1={*7hn}NGzrDDk(s;tO8A*>f;Ww%w0}bX^~cC*E=c}B$qQ<1Wk5-^N$)Ak)`Sto zy@USE4_}4&+e(4`PSqbE)=-8MzE?u;sGs=!KlYVZ-XVo)7A~l0E_ll^Tk01n&-?T> z(t(KagKiz7W}=K~F$HJjM#Dr{gORZ(!^GJ%_0Es0Q)5D9bx_Km4IP`XmV)gLaJ~sN zZSoKb7;p!KqJB)(PSk~Se6_w~bQ1)SkNucPc}w|G=)tdPypr30a_b-I6}7~Uj3xP= zmbU8&7cLoHaoaBX?ELN`0cq#^W@zF--NPL9n^`7xnO!;?tzcG~_P+fNyGg}SE6=Gh z^yc7(pp^kc#3JQ@6PSF_a`qWsj)T&;)m$DzCZbNxir?pfIo8nSM#g5?Z1f^R)f^PU zjtsyWnA8i)bYa?`bK%31m@z8Q<7I1DI!et+aI;??oSml(EV@h-CwNyLUES=Ov2pki5Dz|@THxOegppMJx%*fSoi@2nN= zIXNLN*1Ne9TUMZ)uS~Yw|Y%x zc$To;W{B+}mIp62o8_^F1ZG5;ZBd37!Me=}*&$Gd&!UF9FWhIulBvS@Cs<0;P%nYT z+`fHcsnN78+1@&6{>}JHZhI<;IL&-2d{2GJvG;SN9HY|Q@r|>{_=nkb+6mwH44qbg zZiZ-KpH{zhOphxHcTsr$y7#9Zi@@Y7*%XFF*J|_sHZ`i$mA9cIcd64J2-J{;a4ihZM3M%P8$n zL!FWdRZI24Zg~uRpFUXwA2idS^6fRv4VsRP!1UBrsF5F~Rz3j9-_EIn2kd#30zW#f0|Rh)iFr5)127!3Eon$|&- zknRrGHW1)*R`z_~by{h#fLs-hE&6?svmZ*9u7U zw}NuaPV-k|W%DI+USFw4xGG<2o%n_ckNJ0{?)G*Ityow2oS-+1G!7<>+6G-u%r^`8 zvL_eczquj{^6VYR-?zdzJ{HybyL$lUo*PCPhM$DNm&Py<0P34CEe6e#F_wnWeP~*Y z0;{Z(84r^Jl4Kz>FkmgA4d+MR0kHE&-hqzh;H?UwX^$(_Ea)s1KAQ4N|AlhXSY52Lq=y$ldjeMOPz6 z+v9J+OpdD#rp~-KpXp3)Vm18B8nH%srb8~Dc4addZHx*hjZo7I>_&l+ZOCf#Q4r&k zMpfJp=bdJcAIhp=V=Sr}3r|CbD1$kqA?oncdc0qd>)0Ec==h+yu)gkU3Qh@ykA0O7 zsZV9f=kqaVnY=_{K}#eOyD?)?8A zWh~`0N1t`xrFl3#$L2lrwoC|M*8BRYM@OHq(;LV%>R25lQ>xFd@HpIO2a-^9jLU9$ zrS3kZ`6m3zpGwT;{35F1MnLetD`Q{KF_x!!03ro}&Q;_^{TMnYS?c5hmVkAc5ep

    yM=GbVxD@%)uoN(LTzrS+6$e!6+q_te*tv;C2(sI<=+Imp7Er7%h9pJ*xLYqFK zjONmH7&>!Zx)SNg1e1Z?MZD94rvhOcA*=NA!9Q@R=@?}?0h*^vTmwl@RQZB$nIDOl zB-YR?O8PYk7fE>x5Gj|sgNFO@MM3sKlS|hz8*;Q+27>U6&5+vZ8vvl)t-QW;$l?RI-cs_LnJIv8}dRUu!1HKh`ooI}-2z`B1w z=QwKbKR=KPB}~xhUk>xtH-5X#g>(-lO#QKzw3n#4G)Q$ho4F}wq|`iEV%OJl8J`EB zB;PE>u(~Q0QCDTyAs9IQPk2TjJuc{ijtJ!kDrg98K3SZC@|j-V*(?MnBtZT$ zcMEt~Ix35pSRZ27eTpGBncRdd%7Q-WUGL_QqHLSRF=S4dtKKOV^jPs;o-gXI{0|E* zt0hX-E0Y^}v{lz#^b)0FsG@2qrW%W*t6+I7bHzUNw>Q>%Q#n+4+=C#2SzHhc7%rO{ zeE2bBl9`jQ7*E}q8tkg03I8Q>Nlu>KLcYCUO@rn_2ur3kSB~7Q%=QjjvF)T0ink)O1{W_zOCu*!Ur>jxGvA6#QR2ot zem-LcE@c>u`8P`qcy9K>RKI_%xUeQHC!yIH;8Oc~#YGwhn+HRsutQ$a`R~k~dA8J|s`~KkZu!uVv=lhZeDrqzt4y{hVy{*G(;W&^ zEMPW&a(T#eY+ zNMK3tTZ&fyAft;|Rme(ao^`qtMf_vGj2ZaQRF+C|?0dxbl;^X`m101r(P76r=(1%U zgn}hN))*MDvX8OHA*S|Im0dD@fyLCI1;-~r(e6@Kj-;MJqI1%*M$r<9=h*#r1r-o(H7bpU@roNMM#`>yv=W1%W` zCnQCF^^(1R(b&{R$~s=+#y_MY{=so;_7!<{ah%%P{p!`)?;O`k{pl|Xmt@`TI|80Y zMWgyqrN zB79k3!2FVlQnR96)nj}i?g_PDIGI3-!ZT`I@p?b~^b^S1{spvG%+Xj)^7P(us&#H& zuH21VMEA1l^4W0xdmk`bize=7x%A4ka?8mh-#PGNJkM$7xwyV!P1&H+R?vAj*yBz_Ab1mjxqum-M!FekvPEV^ z0vI$P%^?$6NU>%J-khAxF35+Lr|LbG7nFR|EHlpGiBl>j-zGK;YN}H`U8JkEVrJ+) zubrwJ7^h|A_MST}^iU#xGV$XOC_~5Ht%W}NaaIu~g?6jxX415#p`qE-z3L><&$@~{ z!%g&asdB7x%eCRMKV!9RK>6)XoV}#M4<@H>|j&1&8ZhtsONw=pz zY}2A%ouMmqTe=(bL+EtnKEjQlw|}P?NpEbj%{t<^GMA^md?>sDNq#xDk?9QbMgpzp zx9%yZzgh;YV2<5|KnoiC4X5j1jIEdPOzQ8~nw+c`?8Yx>$Pww8VCTa07 z*0~=h*0yJ;8_IX4NGgY`nNON2)QiX8xDzVACu8*4;Eq1(o}n0uW(76BpiQn~zR~U? zv7uFSv8qS?RKEC*!-sFJhau~z+R(KR!40T%rq*N`^VBQqR!+_@B=kIuTz>Ydd2r@g zT&F*IEs0U0P>CWY;BlAbRQuKV(RPWncn1Lt8iAf@0Q?d9cxJDt!xTm3EOnjm<7rop zqb#au((ZzS)T9|72F>pT|BYeBaa1Q-2Ob0VtWxtA&JrExe-Gm>&?@sOTp`-4m9Io4 z#3(Xb13cgJI9V!*$lv*D=sf#)+y%^LCohzNwn{tI)H}AVF6Gju;vC!R+3;jahCTFC z4`$fV{T#r`n=hbc%-1~O6R2>{|6=X&FGB}D@4S8weT>6ys8>pbKXrDPS$?RZ_~`Ap zy4%|t4zW8`GYy0LsQOw9#jlwGp5rUeYv4fNla+=Har5@MyH*>2pwjt8U{N8Skkp(9 z@z(JWhSyeF9YtMlu(S;{h(vi+)o^R|O4Ve_;Eb=Q%Xoc)-&s)(eX~MI_M{J6pFB0# z>v1K%&K9oqBdlx+zPn^z(T2#(R#rRt{aw%ZUR9)B_|af##tMf}uC1ca?`U4sF5N@` z5K%?X&l$_mk@<+yh6G2_AiS&p0~Pj*tJY(u9t#Wc<=U7wPEOiU9D4 zpUR%P=lj;+*!~3;;w0|fb46Re{uqg8YAnHSvag zsr*nA^3j*^%xXm->1nyG{e~H@d0AlLEqZ!Fj3s^|Ej634IPx-%=}Yw`g<@QUjTFyU zQ*qYnLn|xfl4^=|$t63+(rmuU^JmX~15)oE-xf$Zj%Wf@@ocQC6diul>UmOJKARxw z*S87UX31Du+fAE^l%sG3Klp!u9IEV6jPr!J+ss0Hz&NgC zG%e>gv5G&hdGyTF#_v#kKo{PXy*_ye%h<`3!2DKL8CAmyqYz#(X_sSS&EL>qQ~F26 zVD;QiqU~}M@x=0*V4u?#4Ux#2jY0;1C9uO3#CAGMzk}GAU)u>aSeMLyPt<;uh!&=l zs7GSjf^f_g_rM7cx|Rs`($71peBbvr&JNE$rvdIT8&!hnp|0*vqM|l}iZ>{qOr1;* zV7RmaLpy10n=ZL$Xz0nMvjdxXwB94O28D)+q|KQ75v4l;db*b&Y(_KnRWwKlbY9u} z_ofp*a4pjNa_vuIEwAzim+s6oNGy*arEYd9xJ|<5PCVn$`VlvDtP-~rskH(J*UzWx zdxs5nYezX}?s1l1ubX%rES|8BfVF7=7J7m(x6vU}kG*L?76d-s44heJ+V8EAuw~;y zhlo@>*bPi2GfZ@@eP4U=-1|4T22x9_nq~%i)UQ^py|l@px~=u}^!D!odV(N`n7=qk zNj6_I8xb{6<@Ct{t-LeC$A~?KFzrQ`hGD17NOafjt%X-i5?^gil7F{mxV_$gs>Q!M zm^2$EG)Ovy_?g}AvAK}=-#bjNB8DgvwKo$7-Ep0DBA)tat~Dzv1UzNIb&zQ>AeWPN zC<&+}%$`FQo$$SA3>)-Tm_a)q%zlx4C(ftq{QCxJ4qFNydrXl*%^7WCw#hYQGl#og zPGXCV6bS)-Gd07Z@<#0Fp{?XlR0u&XRJ>S?*jG^CBYizL%p-x}#WVylg_KPPg zDp?%6ZYN9m=ALJ!Llx-ANb^!R<}@ckKuW+ zy@wkVDoW;#Jgr-c(;ukt+GEL>4YOc8WzHp7FuGk5Ru1ZOTe9Y>F};*8grk8>Y^sJ1 zZp78l^iF-qFMvBCt_DUCplY4Y?SOoeozG~$SXa!~G|e|CwN+M%^i|{+H4UDYZdZb?mhH0@w_G@ZcDu>%g)f`bDRoJ=m4!H zp4SjMwYS7Pa4K%Zd>k)F*!J!9FZ0u0Ull)mtNwOYnyJt4hyE?zTp)Ocee?pXVvhp% zcClApBr6S%)WO9ARph6kSgjN8RVJ27ouQ9qq|uK)_RBwC5t+Vy;FC|S(Dx0e_?+SIMMJ@<^~hk>EKJNU(^l1tSTk~N1C9#_Ih zy!erJ_ZK8DP@IzUy&uo?j`{4#qC71w>IB-boQkLectnGCw8|mwwhNeX-(q?7Pm3L? zV&u=Xf!-*6k?4v@x5>y6jDxYkkq1)wH&b#W@9Ile`OoWgY0jO>WyE2d2N!GITbK*8 zcPU|Vd!vN~=97WzfUynDFko>ub}j@`9$dU87$oH($_7aVa5hMmGJaZ$>DhOcNL*;& zUyW6-CMbKmsE^}3ed`{)w18BL&B#=tPAxZbWFQ-jx#Mx=Q^x3rdx@ffU6ol`87Mzze#@3Yvu>{ihCpi<6uED z{W*4QBG+yqRk~$ky4BG-k-w}UFpz->0?-L0gIZD{T9RN_nlay`sJU}y<|0Z|wkjLQ zLf#^KS_*RG(kfx~uxZi9_$@dN2%{zK>FrOvc^8o&Kono%IVmqM)vTkF_^s8U?+oKo zTFs&)Ix@e=cH(uu%yulT8|S$qf3O~&bgrVGtD-*jf2Y9BGw=vI_^8AVbaAdo)6e-% z`0*mx`ewEnTPK2S37sPM>oQI2GQlC5Zsn6#7|#z-m-bQ5YefwQ9k*4@*`u|&)DG8~ zwsgFCTUsEzNk|UwL4CU*R_zrq#$&gy`~wvj#E@E<$Ncxgmj3e#7qOncPZlA9dg)!= z?lUNigi5NbrrP>GaX2k1;@y@lJjIW4?DVff?(@jjj=T&IG%}1FS=3OdPs@ZG^y~BK z)-45`4s;!Nj0x&zWj~tqwDiCwx#S;*2#CZ%P#Ze%D}<-j-7dbdMQV>PFOSG}>?Mj$ zF0F*v62|D_IHyJGauF>H*rO%b!DA7-+HHq~1D0Tw5zkt36Uf4oEF=&BmjXM_>$rFWDgTUt~QLFAd*VG5kTH@Ha6*3NLwQ;s`n-^eq~yHf+EK2W0acipw0k%Q*WG)ZXJ9l zb^63;(fHsC`Fpg)$efOrP-R5qT4YVE703rJWN_?PDoz5t%Pq6V8Z$5 z6Mo#&Qqri?R9?4VFkEiz*-L}Guqk)@wq6dg=87QNKmmbyzhsPPZ!_PTx;a7S0XUUd zc~7T!yljUll#oDF$TIj2Jfq-(n=lKp$}%ONcNPG}z>t&w*O1rE?N9gM!Y+!{Ed@=B zKEeONI6W=$Y)R9an(vL`N#rqh3x~ev&%ZwTVdr7l9eO^N=gqH<>5z$Mb_%r?X3gbX7= z7`M)!XNp>O{NpT+wwLQa(3S7pmjjtvT?T5biwC=$xf23X)mK09r9XHe+t-j3=sodv zM5V+?k^XY_c|Ehx+v|Od-M(ttNPy>sAWCm&te|bc1lE2qaJa<&wxh~+4Q5}S?>x^q z!{znI!;b=o-=aMZ8uW@?61t@}M- z@{jUb0;XTa;3i)JnFHCauq|jTwwUZz5j3^|kvPeCeu4Y4;U*yxH%~gmX$tsFF|OBk zMwWeeCDwdQi?*{M+18#E>zjO>b^W1*Y6ysS1Ji=hgq%ek&|fE3)B%&$=C`sa)eYy{ zVsi_#&B<={$!=}^cM6I&mlQiz3rDUmxV<+gZ56~=Km11-(8~vLhiXXK0q??$Xr1I_ z-|nb#ztEE>z@cT1G-}Wd`t=oxfJ`3yC`M};!)q&y|DQyIky6S)Ree2ksyU~%>~h9FMo@Y+3n>Tr*x8Wp+~HfH;yYCBJbd|aVa+_Z#Oi>xZR$}! zL;U{ICP$oBG5NK|Rr=aXJgTY@FXfwc9Cja;wac*zhJ_%y(C5H4d8AWDCyBjjFzGmJ z3!>xhLZ28g>6NDX{JufKL7*c=UZ~U5<^-J1!(3@!1^?{Moy#prTK=O#IB5BPD7pXCSqtgN($vqg z8&Jr<^S7^LK|tg}AT82@li($|NG%z3IsF7e(A07xv0u$9@IrNIKxsy+#47m60r>PG z@EM&5qOZrl{KTSZ1#Qbt1WGnrw3hpoExs}4!?Z>|eFRX)z!V9j@;aFDV=uM{=i1-( zo6P@wh;bzYlWr!LJjxlorfQDK7x1*al9pj0UvJ7;mRNeSstkK(Pb3CUnJEy1kt3}z zr97DOJ}j5It>DA>%W#8iv$_-=h5Cn3cG+Mai(0v1Lyou+wESe5hKTRTJSB8)?@h|- zi!=4DI}pNvK)pzZNNW8?J*=7R=%jUtr$_6|%Ps2*cqKW(7?cE4qB_;23uW~@Ejvv! z)s9PmDBGG1v)R$A#EdJJE>qF6@ZULKrMJ$Tq$N{uT>Xx&HupWJk>Y6?#&x97pc)Z_Te~n3(Yvy@?ymrOh)&YE<0C z>{o*O8nW@F-d^)}sgc}68kHV?#O0d$QSO9u4I2CRt+WzhqD2j{@<~{g$1=^GFjB8| ziyC?ElX6N}x~dc~V+nCnc8O9hsizKAyPRActyXM)xfYZJFS@X!X`N)mmYa2wc8)I9 ze?R}EX|Z^^0lW573&Z!bZft?N&2>1yBupe_!e1(Tj-GirC0Fpsw&I~XLDQP~981Az zEw$46<09#&{J2-sameuX0EJj5Kb-$kX1bNV@a>`@8}e7f{`Z{R5v9YR5~=#38{fsu#pi)}7E)!`YWs&IbMF4-dvzS{BD#IyR01s0soobSM7l zCozZCeQ&-kXwk9-ML_O0DWt=J6e@;5h&iWUX;Q|BjF$!B_sZTqvQTZo^i^Hy^7zOTNXU;SeEPKsoD{J+~`EUbM?Hc(wk0)>DBroy2-gkxa4dggl zXUO7i#QK@4o_$xU4#K}{T@BtD9jyQ<&o$oIkYdgVV`sB@=gzILfk6T0{TLdd2pUppI62UF!GwQEVT0Iw^-Ct>?(lwQ1N(3-*|U81kZxh&9u7OFm){!XFs7bd{wa_{8??V=38Re0&>N5JWqmz-6t@t&!;xsHpaP;^uanuJn_=i6aNq3;M`Kal zSa7Q7BfI7?0x3v~4uq~PH*dwDvQ1=_znP-J38gWBN6rG~o*s3rrz9)^5g1`Gy z4I|n#I_s(aV12|t!>nhogy&CV;x^n;I(4h*w=(0iL2EDnk+3G}5AU7X6V-h}847R1 z?`<(C6}eQRaPrCh1BqErE{(PJr<3qbIaNOtJc+3J_KLXLbl2hw)5|H>Jd%E1SHl$- zf0*;IzIdXs&^n!iYHPj;U6=Lev#tF;`YmDjd=K);GRMsp)b{N2oFR@93fFnBMITVz zA#&M+`$@`y1LFM=g@?pBLykY($vb%cXEU9HJBE|s$$o^t8(oh+fbOiTwq`g4e$jeI8c3qh2Xcw@`(H+4>t^)i$$=|)U85ns=6&-)trrC^(d&=aux-ZBX5`WF( z$VpCpiJalX#T9+tW1_vH(L@&XP2Z8ZpgL{@vs$_U-v#F^f9m1&3MTBAc9sPkl?>T>8_d+Jik}VJ}s${yuza4K?<$)^EWETaMk| zvS!_3$pbZfTsacLSKB_c=XVk^%2J3%5c-GFS<8UT+0HFyA9p+whrBU*Xx|f2Duk8;vG7C zZ`Jrhc!k#OlltY<3C@ituN{B5V^^UL#^|_s`;A|C-6S|Af6@81|Il?Kfy*9}+Lt}n zUXa-#&$;Hl)VhaKoFVJEIKNp}!>4_X_nPB3vXa*w6x$vtMq!9iwf^r(CMW$VDR%zv zQ)Zd;(a+14bdu(>-q%W>)5I6p_LzwX^^*cKeL3bz`qNwS@X^1kl#XnIq>U&HJn^Y zzZ~Y|G&sQt4|bD$EmVBt;*p7Fixqi#LB;a2d%(3JRl-wdY5RozI+keWlP$hZP4&+poueyAIuaz;_#pr9U^`Tpt=Tbw+!~W!WeB8wktX%3@{N zbJzR0vB{V19x8rw*+b`{6n_oZd(L$bCT`k70`S7E+0k+yf~Ruld-#t$f%x1DKcLVu zx*pHD?y^S&3Rn22S7trQ36pudmhc`v)wBfMVke>M2kWv*dy8kfk94l>er|Sa*OB;c zT(ay9FHn!w<|8Gt<+LD=M0(V^jDQ!Fztg21^Yn77F=$#CZPs$Zh^jAkX3( zC$aIW#&M$Ac5a0o+R=aNNHplk@s^g`_lSP*{ZOMTN*~c3%*hLyYT8fgcbtw1`_7`| zT}a4piHFaJTB!0}2=xel!FgXQWM}3tMStu%^L^&~wHUFUg=wXvv0Pm1T$5PO$SWt6 z<&I}Rh)F!^TrXh!HslsB*R=zAzskxtJFDLMZFtAD$ZvGdEk{q`x93LHKK)KF_WS)I zE@7wlhx&${ZO!G4}{+OP}3Ae@yAc(x4C$oJJ`DM6wbD|du-Zb zc>4E5u|Jn&*WKz&xz8!V4n6K)e_X@)+vedb@`n#@l{gLOM0}yZmBd}&?AU&N^BS&{ zYvTF>WhOU6Lma;iSr0+Yr7hmhTf?WJr^Zxr zTNCW_a_kh$v@{;!@(O<^an9%cdd~OT=UiZWDn+GZ z5LD=&GK${D5fMbM`KK_nkNDdbb?7p}=kXK?iI@WELlFQA?d({%pQIkPKTTmG;m@Gb zb$*E*9gjOJbNx)1n)pRt%dt1KJBJ=Y#A*~Ae`?Pn8N=iyw0@A%hAt3E> z=%p85zk|36A)${@xc2{qBB9Hr)fIR^git_a!B>U``k?~>h#s;j`~elc};@ z>aW9~7>%nL<0xg15?1>&>Z5GL&@>12!3qALC2;U{_%4@ zSC(=EgQc3f|9HFWw{@A|e=sD2_&T5UeOaz7CS7?IGp<(_$7q;VT=zlT=;EEV1Z*v& zpFegFB6e=~^r44ZRQ+CL0%-%LJHhgpvhu&^?5HXBx29OJyc54HaDRN-|LF3W%LAgc zz)5X}j5Y->Pwr_c`kkYzW?9{rnJ>gN?mOW$?v-gCKmR342ryw&GJx_)q?iNa;m-rL z9^y5TTkRfFne%vxiYaQm-?UpebOFOcWS_-%I8z|y(Rda_n4 zp=u$m%x9wq%gu;>;W_v`06Wi2TO$#9o~Wj{0LL}HODdmipO;^eppr*OFd_YntCBkS z$!BF?#8`8o5)$b6z~r!g;Pu$xQqpjwA6T6rzf0rVmHa~G@>BnS``pQI$_@SnJ%iSf2HpkhkJuM+Db$c$p$Os( zDr#ISTsVxiiChOAd{w4N>OS~c>~N7g*izS-SaB&NfPk~Ly`eNmP9J;g;SD#F)E7o4+%v@Ns}$dw6UUDy~O<*H6=1i(Ed{J8y1P{Bw_W z=jd7nZb!f3yh`Ws98GbT;pxYTO9EKg5Ttw|Rl57nGD0T3_rnJ6QLGJ2tKz!QOCV16 zTaA#l01rd+m2E0+pf-WQ5{p1H35f~y83t_~cAm@vZvot%+Y0ztk1lXC@1F8c`QS4m znbBb32ykU$`iQXn{UEPLf|Nwy1_r7~paM^=Z;Qq=`mP9wyZSg%P%;t%Q4Xy_j=2}L39_NTzVI&$_?0Lu^Mq$hIy>Vzagfo?#oASVfh zCP@I}3p(%x^d}R;{6fRD_~Y?!r>Z=?TE0)*>wj%Yi;C?2JqvK^Wp%Y&``P^pF{i>t^9|SjW-0gio2nbDZs~vYN(zJ&WOBaVEys1~TaJ^T zK4^~|ralXjXGNu`U7Whl1lRo^mgw?K-oj;5UjZO^w;$pGeE>lUd@(vGQ0qE3{36A3 z)cg1$k1kSTa5v2=JbDARzO2}hNft=8{r4W%W{^Pg<;ck<_!nzNQP?~c}bj=H$n zJ1;n%9)Xdo4L< zT$BnfuNEk)aaP-#yuqdfZbEEj(x6SbETN;T!aks3ros*8I*8BEF>W@mVcjG4B|J|0C6ZZ zNlE#Zn~3oo@2m1`$1W5(mXTqGs=a27QuTfoYXuy5x&_E%IwLGfln7V>UOYyj|o3Wljb0Hr`mpfSUr$*uAvpuYwNP90LU zF$tYqo5kH9U5}@FMrUbyseX#Fy4h0o6iQRE2Uh~1Tu1&tXq1!^;_H@fiAS|iaaW;X zYW#9sKS?m0*J;LP%MeM$E zY22XjG*Jj_UPMm1Z_3FgWc6fpkz_0#)E-7ljom@+?}y+f6sF7~*X>tB1O_9d&WRxViLgYN;1{HnbUWCg zhCl)ZB2`DX43}ia1vNz9`e$RDiu2C)20ixezk+Q^%%GfP?m{(m*#DB|okTPnAQiM06IP%*{}CR*g|41KYP|K6Lc`03$-G8|-Edlt zVQYbc!r%c7l927Wx|33`Lfz#-rA}l9!rV$Et4q;)sfDvF!DuXba6n+4Te}RTq;5VicA+X%6`EBpoK}hnLVp<=%+bh~S zTz_(Xm^1`P5i%xFi+}(Zzyl;ILTn@g_1+_}{u}WU67>Ly`08KNpxeFTl;?ZQnsBh` zqP?#pwpHA7kMUsaAnVk+{03tJi8=QJs{4wIEGpMJX9B!DobQk8^&Y@B@*$m3`P(%F zmI46vs}qp;gCgq;8@MvM97qHl^bu!xQIoGyfhsjcB^E+fsAVu@S;==iI%pbqYY%bY z9CMK!ihn~y68%S6=%fGidq7sI@N}T0X8#}Owl05zBO?R7p#q?K5kN9WUMWSgwOpTF z)1gbrFjcQkfP0WL#C&N`n{|b^PRzi!PH5Y3)kTJdkR* zw)pd}8h{)d#DcJyCrLbqI-3;v1r!^oB&HGxrtp~vKmq5hc~#W_`B(-i4JhpfxuqEh4zM-A+^A%=}wvj!nJRMx$KpZkf5b5(lEVaKWorcq&10 zk<;JREbbi#7b{n09ZFP}5Tq2;)j-?~(!6M^ruPlHkqfZ`r=#LND?i#MYXqh&_uChG zHox?ys`{2+>}s2X@yVB}q?jJY;)q=r=_>BF^ipw2|9?^&uCLd2JRB|Z9T)7j!WW3# z(qnH{%#PjAjAjZ2s#JN_JpBJQb;ruTZCf(%g8DfPE=q3C{1<`aSmVa8!kv2zl}YJfLdfa$z7dc1BzuI=&PgxY9KEB}FhmEXr$2 z>6SCZVZchaLU#Jptlik0_;(gUHEI~sl`7vSDLO}m^q1f;&q}=3%~1-&<%2f{T5+`I z-74U?GN_?A7+yEm;bHB*;y`*O0dr^!Qx00v^Y!MVufH0fY&%)jaiCf-u!v(*>1}O# zQlV;FkYi%?_>EH-QPX#dS6}h0GYI;L2(VS60=vKcAFUsc^vTc60rsrw$w-EKm~tw{!HvxlPl#S zi3!%e`W~51OAN}24!1Z5-?SsmG>z{kKj6<45lI{zd}xg^?$;AbxuV1laMb?Mo>51e1aP5U{oxG#YIuW(=^L2Aw3j(~*z-#^U9NtE<#II?jFuJ{ zXKW@In4he8Wz=>RGmu6e4u%hD?L3zD0D+~55IN%p3ieiCB%jnlocq)Zm zrzoT~^~)uwb{$PNu2r=VQNXPbcMgcmna+ToJ5v6)M^pg~!`M?UHaGws6E`^l$UVRj zx}g~2`qbpl`M+9K3cme|Wq5we;?AxX_wcj9hq?=e3QG;{uJt>uz+s#FPKaMhIQ2TP%6T+W`> z9dzM+C+C$&j0yqL53<@Iy=Eb~t>l^g0v%sIRWx|T^XjY$e^<>jO7H9=-DB52bWTWk_E@5) zK(PTEFX;6J$%{`x0z%_O(>@P_+asRBo1P+aK=DAFfh2a8LcMPycfVO3*GH(~WZ?gr z&iPGzemNb5L}CKWGOLE8%jT|8GH>Lsg%fpTY$I+WQLL0~OGKa*f2;|qg;u7ML1b%| zFW>r}<*&nQ8Rzhc)F^Nj!7XT%m8y3ZJm=!XCpmZDzc_WihvV`J-`W@y;`6)j9hZ9d zKpD>L^Yf(SZ+&EG^yH~!!x8GN$*Uwt{W5!zo)ZeS>n@l&vQ5LeuJzrjYhcqeP^)Pa zh}Fo%zs2eV5-V}9Ae$K|3co^0A_ybb8b!wSoSi|ZqQfUwV@Uxs+JQW zE+7mHKHK=2Lp&h!%6q%7Uc4TD!gBg{nAc(VNLgZF5&X*CK~;p$&iZ7{aiy>G0f+b} zIo~6%-IB?AyQ~J?aUsTpQ4M~%c(S6+72sG^cT;?^yrhs=XB$kJ@Qc`zI=`LX8OB(F zKP!r5?$?0!2myW^GW*|xI{e!hdbGEL!IIc5HQ{&TVE9G|!4TvJZ5I)PuVa+~6?zJ= ze$X}+wV5R7P^zfF#uLC-FjRnJXYjaZp|vFu%1VYw$=Z@q*9-Es+gvemOfyM?tL2-^ z$qvfeyu%uMoE}gSEFr%bND0uD!jgnV*$zv@-k#f?u=P;uJ!|uLPe+x4(vOO*|1sZy z@qG6!!I{(9ivfe&x>&z)Hik)>T!3h=t|Mu3qm49D0&gPk*eOuazt;pY%V=pTdlF=RD~2#zL!4D~F&qMRgWCr!tHo@ zKFNhq=Vt7)xseBTH`3>iA2qdIIe*;qsH;IsJH`)@+847TBZ9=jlUfYrYA1}=A^>*r z6J3{5C`AEu)uiYId@RLHq+!E*hTxXu zn+gQUD-dr9SCfDk7!N5<%nBYfvd55onX_EHnLNCvHjw zgXDv99pCrj>rsKYTxPj2oD6l)IRs}23Vzbb3-0)UK16V4ezHE0V51_&rcexhg@r|E zZdJ{(`~Fv)c*8QMZ@u=i4*SX6+u$yL-CPh}^y3cRe+BzaGu@1l`_-75e>@1Nie}0F zBC7(y6>&uxpEDV(VaS;eDR!RU^GL}C;fP`)ObT0D&ZP%-c%01Z6((2zSGF+6WJYIM zxgtoSUBLOu)TJurQca?VEDBu{Sdz%<; z?gWgbdl2U9O107MbieMkw5ZOHQKu)#3BEgR3B_B2UaOV8yuPbOlNDc#3VXWS=9D=| zudZa~`#g9QcciqIny0}I>IZ0>>5qc_AIdx(HLds`zqXzC7`Buaz;10^_dT5@X*i+0 z9P;Wv`$Z1SP+bV{12q_bT5{KEm_QM+gCbA3L}J_r0pa3jEN|W_$!=?qSh9Rd8uW=? z`f9!PeX0Y1NecoKhX-~>21t!li4q`6ruMxH{@QU%fuxIKUV`LnBP@}-a4#El5T^kS z_4*C_z70*iCo}KlVHb^~MHerC>3*4hrfS29{&g*x`vCDX2ClR;*!y{foC@uDiXaXl_~$(~#&j*mZ?=qJLsn`gc}SG^Vc zX=(+)Y5%+LckyqSrAMtL=bTNpaX7~|mp*RUrGLNfJ0D=PaL76MKCN2d;$d=T{%P-S z3g)u2u;P6*JPVn6jtL9-KOcr5z?RG)$nulA_i-!6*8V~d>1+jwh&Z|%3jk`w9{?(@ zUK0!K>t0Yp$Ac}}m_7oMISZ^el)BDb{)nj?SSOf~fB}Ta`lp`t$;s`VYXhR90jB3P zN|ilZ1}bL9pFL?|Z;LLc_*}rH-Xy6}KC)W>jpC!X<&x9xNdv~BNfW3r+?O87}7%h2&CE%%08eI=B? z)u{1K*AENt#e8=1c~_TwR`kczTh^6FPW_#kT*T_6xClX%EQ27mGUt8nMglxe63Owg zWxTJ%S9;YO3N`vOzEzk#@PCTnL-1elYvdp#mePNAssBd+@8!LZGzn7TBuJjy_}FX& z&W`7>;eO|4o!C3JukPpYa+U1Y1%?^HB4q;@)X`rqVG(4`#7-gjlS` z$=m4$hxwY?3QDZFtm%B&i>T`hd$!E{#uDEmz9F1q=mZgb7VSb(1^lM?%?khZl|(`zohPH4@_f4>^{5sztU5q*dMuG1W@P>E zGL~ed%sG z&qnpikHc#&^5PjaQ}d zahmYWwgtP;`~)qeJ!D*fs@m9y8?gfaoAP0*d)J zhzoGm`^JHI&y~oL#A5r?F=X{mZq4R}DM_)NKTP?0-dP1+{faSxX>Z^M4lh?w%Q}nl zv9rz9i&vBH^u&9neMt38t4V!Rsa$>wheBGxAxXEohXgE>WzmN)!7SHwu8qnPNfQ|p zFc6eV3az%~?cAeoV3}&&E9-X(c^wxrD@*(B?%CYk)!BgMTI8?83p+~}4?i#(tvAB3 z0Za-6q${u<1b|8I^1E-Ej5G~wOliW$5?W)MX3cumpqn(GQS~E>O-nQJneK`G!@5nH z@WS{nd2z?*{4zVTr}CQ4}65XtRwP;U2r&U9Od8Nb=@FJ6o# zQp{A$wUUsde(xlwQGyl{=<}h>iFx?*!%2H~uoPj3p1kuzccf92TmTmQ#;vw+9Q;7= z-2giLJrd(%T=*-wcY4{nvBRrfFTg?{XXB*$c)|H%p6^XF%Xzji7he7OXse1bB9nk0 zfp3fELxdp#Xd?(C$gu-Zvbgxf*l40daBHeQ=?)k*!9YU0zZlbnoEdbvi#TRM{EpFa z$44S-cK?Bi=pi1<_#8=jM;HEW0?nSIFGoi@$Guy( z4i}J%&dJwI+VRZQ@Cgq3qKmwL{&!XL;5tf7{h_EHVIuv1!dzPo&-AY$x)!H?* zI`X!>-eUf(MYc)yGe0C59EOie0JmtrLVShr9YsL}k}NwNkK0_5JK0B^%^}OF^hIk} zq5f;eS9bX%J0vH@-#=P2%a-aFa>;Nv`rSCm9q*Yzg->%eJMrC*z*WGNe5;%4-+3cE$}Tqzn)1b#h_tV4vI;Jgf9mD;m0)c~^kZ$#mfN{Na4 zqgOAIyzIVW*(s_z(Icw5hEzz%i?@VJGb!k4b_#mUKl5#a(2>+%qe^`}=uA!c&A<%M zqwXsT%v*!SsYr%&W&E(evc54jYFyWFsbb!eW7l1V=&hNRmuoz=7#;Td`FUIPxv~z} z2!0?miLlUX6?CPMsG-EWNq4_lwExm`D$vfheDcsExFJT~cDhOPecyU;%P^zP&fe)3 zfx`koMPl6#)9sb;!76mQw5Sddqr7f?Y5IX;0ogYM7Jyn~B65_Vq!ox1YZ6!sAp|TG z3xdP~yp2zf?1N=y^+rm{ws5jX%L^hz1UdMQeL9Qg>o_&;KOWk1{|caXe$@5&V!`|I zVSg2IFU$5 zu(tEbwpa18pFfns!g)IVD8)SWmgf}b`K^R{4|G_VFTr1_=mpBXKH#ZyCKbkL01o%io+Zmo#<`Ql8KD(ger#Jqpj}R+ufDUt@UEuZ9^XK@D z>FMSA_2$@pvor>^iEcla!P`=-b42wvl?-3jU9JDsHznpU11@z+&1`J)N|3YW+U50P zo8rK|$68)aL)(MyM!tTM*ru{w7fpi$0ns6B;nEiE^OlK7J=_4Cz;o(mZwH>(w|-Z< zMsICVVQpb!;UfCZsa2Wg4i`Z%S8nY2m|N&str)$vTj)2E3OB=J-Y}Nedb(UAzE434@wEe?D z4}KN3+3Pj*>v0Tnl$CRZ_pDfMp5NAI{CFrz-;9Rb5gswNO|iv6XYl+TA6Mr`usf$aKJcvziPvDdmt-S*L_NQ>)EW}?i>NK$!zpJV z>PYB0Q!@uW3O@qX8VIaUJb?t!P6K7||!Q%}plRZESs+lsf$c6@MsMXxO81GhA2h#%6zsea4`$o#ZGQ zmy<9%^?do!mCw%Qu|6-}Z|A1wq36_7a3q_ao}-1eQ%E*B@jJIaC0ny`pPHLPTYe0B z4V#tVqvg{e#VxsV{;=SglKN#RBUE1BIuW9zYzVy=iQjDzTQ--LW4|zwvxnY^hZx(0 z4Px#UAg84ERnu2`Ud$J9BU+o$So~LP(X`R~B7e=yW0%ffQg5YgA%0jUp7}fJ^=O5ct40l(Y&-S41~obm4FzI-6#QYViqe8KX?Ri**+` z2W#rp3uFrm7+w!Eig@iuHe7dp(eMe6arDX0Anopk9L>m49g1u{xuym?1`>==@0|+) zc7gXzfda&Kae(oK+wfYY9Mft_2W&3)+e$ri?& zZ+vf8+WX2CpP+01h}?LOckik>M7u=+k`$bE)a8n)E4-5*|KQ)ov^9{H54a^nGodU+ zB6%mQJVvZ+kg3vkv>Wn)Dr-ld-Jbk9ii%G=I?bft!_3wud*~=4-xl=t?+fZsd*k1= zh@=z1rbnt+CR0XyEFf?C4-JQt`*(-hp}18BpBQ_{lD)`&H0J(eKdOc}#7KdZw>~Ct zMtphW@dRdyB$?-wCb=iCt9jfIx_ID5-j7hn`Do7oCfWb+E|^IiBCMxK+)(Dbsll#- z76Z!01Ih$0f?UHkmyEQxOY$h>fR)P(Uu*ipl|!Y>SsCw=ABR`&;a&dx@ytw{XAl)p zM>q?T-H+Q0K@dUE5+l{V8syE*=;1Cht3#m9-@)RHIEN3LedVJc19E6#Lc*snjPH{Y z_wNloy!ucmIfZ*qtUSaseL}ilFIzVaT(s|XAd#l76o313Jr8Y6fmeVLz<_q9Kaenv zK*h&IR9lCojy`tktQh}r7n;JL+RL?eHMMfvhrj>LheN<4 z&?5PA{zIx~lqxuFd?8FD=!kQmrc!fLs^pE26NeAyN?^0SQKA*rO zI&j>}yk!@XG!%XOedx=(YwJhe>TlTTQH?1be+)aWki6fWY6AMAj;3^%HGM zcoQ9k>DeL<75)1m4wpFVHUqSlSWhGxXw{A7oAATP!!ls)jdZyVu?ovLNitd~&-x1} z55WhC6nbF-LJ|`bpDg@^>qO2k$4NmT{gCUWMI(R(h+EP3Js>S=fZ#ghTiS@Gqo807 zBTvEt+7SvT&{b$%eFUzR+wW_I*(^Ry=UE97X>h>7 zj*!G=ZYHb{N#&=7tglpxaq?Z-2s(=CM?(3w%;YeN7A7O!d;4XIag1@jrbid9MS4Odg@0xx(_OunV~s?%xc5&hWE2T&{MLBB;uUgHYz& zQ-pk#riP<bIi%@)*lJW8^552oj72wN$J6lHgqnhw(J=>RX`wZEt3->0^L`Q#E5d zF?k6I1&%BQ4m(@qePhUX9o~SBxF)T>j~4 z!3&X?GXspGU`H3^s-gz3^TS3yl$MGsMce`?6~36pi2D}uq1UisQe5SCmj@{Tm$h(@ zKHgRtKUgdjbQ62LqJPtlDt~Uo)rz-ZH$ zMRhxH4V!o<7#fLPEV5=GWb8evm+1F^jFJE?Grny*4s)EwD_y6=r1_k3E1#}>B!`8CHTeE}e%8VKScO|#;iH1T*O|Yaqbq7K`Fokf?#V=i{br|N zAWahQUcBiVQd}LY3D>Jwn@_C)J=uzzsL$x(o8f0RafkZF`IAbun{rY=0$y!Hk@|nP zdB3YN+20Aon4S=A#C2g;v%DxLe|IrRvlN7&aTlnTu=$z7DY;~I*Uk{7_lJm}kr^|H8m~Yq=)H}%uo$pE zjy7bixpJR|SO}!<+>N#MoKs3_=TA`)m}SqH7Zj=5PYt`H9Xrbb5lz~IPYp0z=5 zLVi%DRm`5D=j=xRN)*?X50QJAso=;2FR$(F4&^ab?XB@6x5as0WHJtCAAHf&Isf!EvsI|B=(N4;*C@s4)F8(v7-tXg zxLDd36|PRYBk8WG5ueio@ys`}QNT0DpZ@8F(kDIvbAVjpzRNvK{yPExfm3~eU=~R_ zhcnUd{MURD2OEcii-p}~km6?LPm={m%Al)q$SdwM+WKbLxm>?^@5YyFbt=bU@Fxw; zm-5ew(*ph+ft+{UXnrXu0;ih_nt`3a)Y_%{{Eitd7$csm+~Y4_h|85%v~kHB}4@SL)4!dByAUTYrk#*Sy=Hh+w!&iFuZ^iJU@cP|Usi8te|w~G zCz&TNwe07IC6}4kHCDo3b%8q?xDkV)U${W!Pxp=-PB@)d`RulL@vTC}JNGivHS>?F zG`^?`zOK}0Ytszf%0;wfu@PIz)BNJiAwG&1MBp5T{Nm1O^Z7;yO+)6uUuz>;7jl#~ zq7=gerh!gy4Rq{Vuwerq%3-6kX&9!K1a{_(Fo44eXn)uH- zCt2oXXSOK`ZW6cXPTi3SzSoQ(_q+W1Zpt%V9oB+X-wn0H4ryWwpe>Abf4jQ%CDAd= zD#LG0ff2}ia6>W9pdtX`iKPBRRVrGFKkNXiUcN-I$FLURQQpk0=T_n}YCn+&OkKIp z@JwYli&g%8?(Ux5nwrw#u+wDpwyM8xli&Z~P$I|^@5!cs@~#z^mYI)0ty!>z$@gw* zIFOepLtbqlFBCDumksZO&-7^F$*G>5NuE&>tIyM5yS;K#X(apoOrAZ(#DEUzXwBNj z5*p{J{A^I-r9j-!7gHa(=|e(bkqJ^@1~=h{YU9vXpVcxs>7EDf)_d zhxCjSZbDNqGyKmKlz}Oz4tO*CS~pEHGy`Rjt(7)a)A`tOeD3D;ySkVqY$+IFNvmI@ zUvua_osc{rg|c#iGg25b0Hh%L*k-p=bxbUIM|aM;y1st+%X!c7#b+OS;~LL{O6=dU zYG34cy;~*;qlE)J$QL;A#e|yH>%3j0A8@U^agCCJ>yBJV+2ebTeZrPYY?h;XF=f$`rPp;_%GwLrCSsXsw5T0k5s$TfVV%LTC-B3#|d=vNcpDSz5Kbu z;P?64o(w1UY1>oqsuV2bEd)x%^eiJqw}duY#9{H9;loVYQ_O6KX#GhD{IT%pv06&& z4U_uQ?^z$Jo9h}r)M*YBn zz5I3D%W$mcB$K1x-#KShr(Yz?OV#lu---Ru$%*49m6Egmvwk1mHzB`@cc=mSC1@5I zzE^gYFg^yBeZS>D8~|27{VxYlp0!2`hn)_y&;Vq=-yC288qeS*+R({B`QpUZ3u;C4-S7dhZO6)04@O|ZhK$#_hPXJ*$aixv zdA@Tn@hV|?R?F!+H^XP~nBAr<^*1gYENR7qJKY_&$NE;;8#kCfahPa?sW@@mNOS$P zb$XsGtN7M?<0~K|PyNlvKmw=y!Oi%93>JPBd2kk$|F!V@c@)yr%s{wkyFzKveEFzm zzl_13{5K8e=j{OXxzenE1rf!U@nk^`GweLHZm+MMI(cC$_vNog_Cuz6@yogqsG)E3 z7w|!acs;e7!xYydj+_+~Ec{rl1Td4hkF>Z*VfAJcS0RsALNwxPMY3(F$uka*si}YQ zG8%qM4+MX{{$Skw?)M$N^8RckYx?1n)}7!g+AJ$p%>?SBbSv)pQK_G~YW;hb%U@gs zUA_pm>2XEnIK+X!h9O{?A@mR*Sj4%qxlY3 zK6&(xt|q5zg^>p{MD>v8;o(Pth9(p%3@La89|e3r(77nU^FZeyq_2NHP4`J~!2IC% zJ~|)Ovo5}IoPAZ*K&|?_-HPeB45Ql*uQ1mN9dpwM|C`|3L^R?D(h36LkHBcd62e(T z#mabm-U`vwsTEOcd2qV>P7aeYoJB;FSq~TOAcljWmT(NDmUNlSd zUT><@Fv_S{GjqY(WmC8lfFtW!j7E7MNwufkgnSH6Zzc5bC`^Bh1M%CVfAO0$;I}>% zXqzLYPenVCr`z#A^!6?*kd_ihNIpWI7H)dB7x;IbOV8HX-uqy4bZGo!c8xy9kh3U$ zrQH5C*`v`Kfpr&_4XnRK*?r*f&Nc!;DGN5cq0RKOjSAC1N4^-g3+}nM7`$CQt{k3f za^KK;`8>Y#@xumy>nv?XG2nLqtzY%ryp}wa@YX5i%7N{|lV3)z@mrn!FD^UnWPY!C zg6TSJqF0xh^v}Ic#ceU}Xa<32d&sI2rE0;h{XoK^QEK|CQ2u{wc<4QRQ@F7$mzD?|MS9Qjb+P8cbn%Oh z8x%Bg+7`reTURfQV#=k*PmJ^y%dBVc?)4lrMhF?+PcClADOD($E{N40ZQJg=*?F*j zq2o(bw#76}$G{g8cMfZ+oSEk?Zq$?V@uasVbwxJDwH`7o3V@K)ng2KB_&(;{6R%A` z8D!|bl#F0mf{sY6B}}xtCXBZy^&iJm0D=9_>e4bdYxJ8I=}ygar!UMG);_7MKi;1y#=RxEvCgmyidSk^zT zI}C2BXM62HNtN@urGS!pPy9*l(I20dVEDHKz^ocxe)5y_><9?x0HBgY@$7y6QgWD8 zw(?Q>4{%#yg#PCQ`>pp93$?1;I~S_mS9rjFlDHGSo4Au6{}Q)DLHr6mi&T5kaz*v) zrJO^VNo+c+&~W8D%P{)PYzEeQo{(w|Uvmd`-uJL7DV09?|ZO!<)PmBX^{`o{|` zrO(^&T3YgQ<6Nay;zFo(i$%SvK8=DkJs1fu9{EtuN2Ps~s@5vsLiVGHb3Pi8v zx~)EJh>LFgvw%>%U|cG=qGIF28q?uNYc9>hAZuy)FAP19JJz7R9e5I(;R=H`_o=7Wp+qTxh={ z!Rz?+Tv_&Mn)8xqgI@ZLz4V^=1>W4a>6q7jm9L++51@JL)7}F#T7$(4AM%H?<^TQ; zfto702GS|^Q-KNwl9_+rVkrNgqXj1NVA%LSYV?~|{6Qo485*1I#(SDa+O_c}p|>g1JvWNg3YSKi_2nI*;gooS3a zpZo6AC%W~%n#n|yZHI0+O{P>@&~+YJ_q3qXjD!S{Ult*tBr_8ITasbQJl%g(-+Y+U z30{yNt#wYPa>BB0!%i7^$o9MtvSSaRr35UAhC}EAC~N$^LQ$1oX~~tV)8jQq5VZ3C zp6-Z#Bk)fB9XT?hzbMDkz=wNn0V48 z9NFJA(J8D#V0mX40_)OMGquP`#z0i;GXn(_O3SzZB6#1!HQt!B*0WUx1QqV#|181x z_vL{)EO2Z=CS2??MG&dm3GQ3(<*I7`X1-3qe1O~|!2>!AR0w{nh1>)`SMD{8XF(*$ zr&W2;MRH=8#cFsVOSD0F)~C4JBAFe(nb4#QmV}i|X^S|=B+n&NLfV4oqsy_Pjp z*Hb~}9S=!VQh&CcIiQ^1Pyq457!3}maj_U!&K32V|RHS0yjE`$>VcQw_(J~zS>myCPHmTU8;*GXa zaQp}yKvI2tLRNc_e!75(E}QdT`1!+spQ@wWH^=Qb+wlqvobH;~heqq3`WwEojUDqtzbq6QAujqsc5bbx<}*>=)23^29JzrP zqAV$*OvT;nCf9oxwoiJs?&$HT;T;nLn2%xOQ%m&yZa>8kJ7Vd+YG?*y*Bl@ySkR_3r03!=oj+Z5pwIZ^hlL z+a|mG+XIG+taF%?xP(KR%AX%LdhN&E!Tl8Q#8WppO;N3FgL1reTTP#$e6~J+!p!&{ zH>_jQF#`=x7ONN1JbxOMG#t5sqxXAzr)fs`v}%s@5G3qitL7^(=zt#Ooj9AF3v>Ff5B|4uKVuuo-UnFkJ#)lq+tp0 zswt)C4^4?|%hHdKPGhNEEu~9#l23~Ix9S+ZMg!#5YrSVk zOWpSup$oYG-xqKe9>6876YSiM=u|sioQkS@d_=WA;n`ceH?ggA@=1I@8+_)|EOUa(_+u)9y|xr4 zAZ>n^$AYah_mY70NL+Z5`iN$a1snJa6E%GE1ar8lrcjt9tb?Z$X0DAdVn)|Y8+XS9 zEe$J$nbUP3_+Lo#w93Qc48T)BPbLqWLxwuZ7jm*A;zCRiK>(#1r zH=;GyHYQw>l)oJbJ!KE{oZv0m%9^M>uBM$uo1>-UB}wVb=sJ+9bY|e?#Hk9l3%rm{ zdw`Y(KV!?X^;6W(+TDHzqtm8guLEME?M`r4v}5i`%v7c5{)Ts@!^Ab|xsRFPYP+}T zIeeJOH}Ys(7z7`K+9@PLUrWHu)G2d$8%$!Y=ZZO|B^eXvwqCbTcKDOgYrqY&k$ldtH+h?BSc7dK41N*)^A3|w5l{R00fPW?qrJmuV^%5|RP@T=$ynFm;NBve)8*Fo0 ziTBA=7R@Z-$Q)N~yP%AayYj46T#gwbvt@L+ZE5G{cAZ(21>UDY!1qq1*YR1 z6W%U3%jB-e;~W!fyA{~}03GWbZ`ppksFR1rT}ma!vi+{}or9}j=D_^rIfJ1zDEd6p z^h7zHYh-Sul}D*)made(V+LFiRiDGg&IadEqZ46_sqgOOVbpQy@dTr9=bq07HyNY% zk6+z4NulY|6aM*Uy6R@OmwPe033K3ro3jNptC3dke+l>xJpZ{S>!)AV_kXbN)g5jb z1T?9U^2?g!u9~3DKiu~Ta#ug;aAY3md(qv_YAUA4NXMwf_@EigX?zBZLRtj;DWapO z3(UbwNsCB7~NiU-R*wp}+^@4n~zO&On47 z8iY=Spr+6qyWk6RP12-{)sDLsvgUW)`@#5tQ2`%MHUfS?g!%}ICV=*ZK@s#7h!C!T zOV0eVr3x_T_7>>BxTHnQ&PL0<ZefL`frZ%PQ`xZ6tB&HCe@;)5-k z`uT*pz0-ZYddw#W&yB@;ZHEchUq3$Wjn}&m|7WO*eRj=jWrgUpOT*e9Fn!uM{ghti z`7d;O?~yeu)%xz=goY|#c)jMKZA_b)LQiXv?KIv8M1vIX_ek1^LtMt0oY{ibnNoJ8 zqKG>ppW|M{sy`OgEIRvtoiQnqT>SDyGLOOZIvWU$m|4NVlYHh8fH5V*O2ckFu#GX8 z$&Fm1+WMrdA6y z*8|!3Qo7+6T&8qktF3U6q*W#ndL=XXOkaB(xro9Hd<0e+?)0#=C}r+VN~Gk)5dB$H z=eLN})*`MEGr<>SbRFp)Wa2gJLrQLEFr4G{>gGM0xXGiBm7-pcCJX7^QboOXAWnO- z64|i!8;QlQg)huCZnY**Oue=U+}X9ly(Y)@^d6NBOoRV zF69ik1ZFOx;|drvFk*ng(6k*$8dFqo2M>4(@MLr-z_Y){6e7Xja|$>Md_VLrz)%(t zA$|rE1yL1vN=xWD!LSN3gO{fdhD`wQ8X^SH;~Y8%R@#-j+L3ccX1#pvNboZb1lOnJ z_QpMyp7Z)n*_=(AIi#XhiN;c+HOJ#QV#Q9r z7se5T7-_NR6|b|k4RYJtewdGgI1!k3OiHY@SVL5{ttSIQZE-}nrA7G>Ely`Q7F$9> z)qSs-i0voCjZ)dl(QN9&cmdBr1lp(1-0cGe(i?Ig47hG~%>TLP{!mGF_oFg8JdECI zo_%{ndBbs0=zsRz(VDiBzrIp*ezN9TYu;?X(MtQvitN2v#Xt%B{W}jgN*7d;z z#33ZsuIv8u|LhOBLz36Mzp6LgacN0>d@GyG#{se^0oqkYPLMQgD!Y>I&@m>3w2Ag8 z@U@S}&2IDbXdL{&6E~rq1P6|b=)`!)hw5l4%UpEK&#qMAEz~D)B`8k*T3~KdnQyaM z6peV5#(}3Mt;t@&C-SRgT3`m8FB{35l~yQ9N2{FkH6%|2i{Y<_$1Z`xW4JqwbjY4J z{ZU@s#Qcv)izW^+TJ-g+-U(e35BocTjsx!JHQ#SwF*@{d!;yB|thfLBcfQ+@*YjvK zN(=iQq`BHk#~bI)BtHCQ)cfYN-dOTH-zF-lYao8MhJ-qg=+e}UpDKBBZC-k@Q(?9j zP|x87SY2cIHCP^I-I&=$w!f-SCH%+iBZr1|xAoka`gL>3lpHsq+$U$rYJHFS z|5o`U==%K63%54Y;=X*5^R7l^Um1-;UxG~j%534}ZlfO5Mr#}wCn5$u8(ekvN z?m)qqHm4=CqO}kuHYP7w>YJjPnV{}TRCQ`!U6Mo2}=$et|qEPX0j2*&0qBp_CSl6k^@mD)F=(-$%sS4XcUF2aaA=o|(CZxzHZ zc`&j`PpVrXXH_P#UIh7#^%2O|(S;1E5&rhF;I0Et)FY8#_;Rg$EraeWMJ1C55WZj! z3#WJUNSREVw8JbjT)g*s)#NRq|IEE+Re!p!@pI{i2g4-h=gQ(u@wl~*(9EW01%NdYqIcXvymVA>lKm_t5 zF9%3)O{hMSFS;5S8e|+d+e0w!Hh>~Z9Bn3ojSa*{L@b z_x;J1{Mw61GS5wZu{p1_4}LlVM{S_=cAYuW{wkf@5d)YNHsK_9X**D{wJhv_gDz3y zPT45n#4df}@r1tkjd~~?jZw6vEzj#KjzBs`!G^0#$_E;0jigQRF?T)UiEj37Fr$q+ zfg7L`8LTW`Hh_f!(voK{77X;s0Lg_y5)R7ZD0BcsJS?k9)1d}sg4QC4%PNhPzk&w~YFIQ^Es@G9o1z@VM(ZFMEZG^2bN8N#-(a@?Cb%Lg8RNm)h={vZm+@sNS7#jFZM zOGQ8@v0Y|pj~tM{z-Jj{hW(oUT5Tb&i7d!e{5#Gy$ujF2Zv2KgXD0B$tdn)-s^ex` z$hzuOsy@oL*DB&Uw(PhZ0+o&|4sdf{gmKq6bo&GC*6iYJ&6)N{qZIh^Guz$8Yh8nU z99kJXwYFVUtiHHy$IBIkn1-{D1%(7G$Il!-05wJc-wVw`!m~ufN}uyinE7u=?p@AH z9p{}hLkVrJ3nN|RMyr=T)xw;7TC3;>dkI1@%SH>w@}x}{cTb&pIz(|%hN2;mUP-UZ zU_!dU4Fe5{2iv1bd%)$8$t@QTx?2I&AKwU3j*BK0E67eJYXQAQO?$~*Q$nY~r_A?7 zPtd3P6rhFBw?J!IR64r4Jc3l%r;N+g7znX4b5-xhUaKIOJdq@ykTPajprress73TF z{A06?3#`dcG{rNvd2{wBu(;zzvt2XQJ&NN2q1k5po>K?6tto>YJ!``13Ri53Hut_N zh8SK@8y_OR$}J|(6+)M(3=socJfP=DF1Ju$>a+)~Ez)3PQnkPoDPA-BM~t&?6FU%3>jl~qj_i}I~ft^)-x>TNJsipQwG27?mk{kP-jgbv*V+ znDV7qzLs4=_pg2c&^jMoLRsoak76i$O<7gW4va(oA*!Gp{}%R6fcffxr07l}>iIJF zPD%~qX4yK{3TPzIY6IJjeRDS2A-Cz2!6!9^D%X^s(n1F~XkBx4LMD+K9 zk1{PgPcJUao=&^>Qu70KMZm7zVGuQck%vFY?B7UrkD0kNj#mA}fC0Tcy1UG@gHb^3 zbw~K~J#)*RDzC1+_5Y`)fn|#e?%lio?As$}M~Byy4xNwbhP)QzIz7Z&Y*NwfOUA&C z(zGz3xhr6BC{I4-L)(5F`V*39)bL!gJUaH%ksk6=ey5mtXJU40CF|T&SfQ>7BYpE*FiyEI9@Ll2ijdRR&3>5*Te3ozHk4(;E2^omzO@1tOnGgb zJ~Fm9oy~_ic`0mYn8bzKMI|$%iL>O-dGinQN*{diAZFoV8e3sE=+emgYP@JG`-Fjz zhY7o{1gQu?VEGkqTp!u^(S$}=6CYS`RLa7(4v*oxotmyqBOH~VlPgi!gM`!`Hi^Hd z7LIXL@fay5)XK_uu8PVrS%;*yppoDK>uwW9*rocB?ffw@d@1M`cSpiab~#V$3KH~@ zl7vkXs?5)}iK>f~5C=?zx=6F1*dXCkauZ(kX;;74M5Pk-O6wE%+FIkRjfdgJ)E!bw zV9`4qmDL_x1Xp*F)`~05N=c3Kgh5Igm8TS<+;W&gJg%PzR9&g?lUp;`f~gz~D(~5z zl9`K*^VC$EYo@Jd=KPGkXMRgfMy@%7O@7PMw^DB&ZSvEioxu$Odm|+IN1Ng?h7Zao zt};W>`}l^kFxVyZKJjfbza>9xX(Q8XL|*bRm7jZq_hc$`0et|H)>kj|;wSTy9jBzs zE%&&wddWkT^7|HS7uxkb&0hLQ&*u7)zVdDEyD_t<#&;zg$h1WG!>;=YcoC94X!5=| z%|W^adkV~_hj@(4{#trOT|q<)0#(l3eQ`+yXRl8)^{8~NKdp4`OkRoWMqa7w#>9<0 z*L>(emo6x8tP`&wUVbZqJQ^URIKsH#M625SgwPo+*XmCeK~q&VR(T|_5F zxS351qi;Yw%hz~Mg-M3t8`K-!fU64A!&)MWoEnNpwr4ms-kTC}1S8g43$UXD&?&WG zuL*!+=QVg7iSv`!Tr&u%skV|f7v*1y9ncA#)dwOZKypeqoV8o}QMyu)+uxm++uLD-Q7FVUS_r!ZLxO!D7Z! z7c_gl1PiH!l1=vyrVhH64pUyHZXm6>z$Y3e7ycFfX=9`7-oBo>xz--nT4W_sVFSU>gcwR8}EQ#xDM5SLp^US{-6KzS#O6z~m79?b=W4D;RkT8&^ zm7-x<#*`;oyGV7AbE66)JFP)e8X?<4aj9+HTi7&~PpgmL6bFQefiiFGi@7)O*hX3v zBb`Y##LEi1K9b@|DC=eZg8@qGN6y~H0FI8F8fVGX0OVOWwS76z^*PH3u`hN;nt`n+r?5qMfd0o z^`a^~2#(2j!HL+9e2lfNJhl1>uX48;UGgHI)N`B4Rs}0Aiw(suR|T zK&zCSalN;Ks3)EU9fC35=(cadfyKVs$RdS(S53gku5K?2UaD>N6PT#F2^>BPwjyz6 zSYTVhIawsWdl(~MCOZjrfmPqpMMm`1PDJ!<()**YgluQ+6L(Vk2ZN=6F?XypV5DFYVaBOCEB`U Li|O^>{r-OeuVc1V literal 0 HcmV?d00001 From c24b504a07bd9e0bcec7dc40a47420350c902918 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Mon, 27 Jun 2016 08:53:14 +0200 Subject: [PATCH 424/498] Speedup flushing tests --- .../reactive/FlushingIntegrationTests.java | 4 ++- .../annotation/SseIntegrationTests.java | 29 ++++++++++++------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/FlushingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/FlushingIntegrationTests.java index 2427abdca8..d3a45693ec 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/FlushingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/FlushingIntegrationTests.java @@ -47,7 +47,9 @@ public class FlushingIntegrationTests extends AbstractHttpHandlerIntegrationTest Mono result = this.webClient .perform(get("http://localhost:" + port)) .extract(bodyStream(String.class)) - .take(2) + .takeUntil(s -> { + return s.endsWith("data1"); + }) .reduce((s1, s2) -> s1 + s2); TestSubscriber diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java index 30cbbecc3d..f25d567c7a 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java @@ -86,17 +86,18 @@ public class SseIntegrationTests extends AbstractHttpHandlerIntegrationTests { @Test public void sseAsString() throws Exception { - Mono result = this.webClient + Flux result = this.webClient .perform(get("http://localhost:" + port + "/sse/string") .accept(new MediaType("text", "event-stream"))) .extract(bodyStream(String.class)) - .take(Duration.ofMillis(1000)) - .reduce((s1, s2) -> s1 + s2); + .filter(s -> !s.equals("\n")) + .map(s -> (s.replace("\n", ""))) + .take(2); TestSubscriber .subscribe(result) .await() - .assertValues("data:foo 0\n\ndata:foo 1\n\n"); + .assertValues("data:foo 0", "data:foo 1"); } @Test @@ -105,28 +106,36 @@ public class SseIntegrationTests extends AbstractHttpHandlerIntegrationTests { .perform(get("http://localhost:" + port + "/sse/person") .accept(new MediaType("text", "event-stream"))) .extract(bodyStream(String.class)) - .take(Duration.ofMillis(1000)) + .filter(s -> !s.equals("\n")) + .map(s -> (s.replace("\n", ""))) + .takeUntil(s -> { + return s.endsWith("foo 1\"}"); + }) .reduce((s1, s2) -> s1 + s2); TestSubscriber .subscribe(result) .await() - .assertValues("data:{\"name\":\"foo 0\"}\n\ndata:{\"name\":\"foo 1\"}\n\n"); + .assertValues("data:{\"name\":\"foo 0\"}data:{\"name\":\"foo 1\"}"); } @Test public void sseAsEvent() throws Exception { - Mono result = this.webClient + Flux result = this.webClient .perform(get("http://localhost:" + port + "/sse/event") .accept(new MediaType("text", "event-stream"))) .extract(bodyStream(String.class)) - .take(Duration.ofMillis(1000)) - .reduce((s1, s2) -> s1 + s2); + .filter(s -> !s.equals("\n")) + .map(s -> (s.replace("\n", ""))) + .take(2); TestSubscriber .subscribe(result) .await() - .assertValues("id:0\n:bar\ndata:foo\n\nid:1\n:bar\ndata:foo\n\n"); + .assertValues( + "id:0:bardata:foo", + "id:1:bardata:foo" + ); } @RestController From 699b0571267e5a27897f6d6d6304d18006ceed85 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 27 Jun 2016 08:38:01 -0400 Subject: [PATCH 425/498] ResponseBodyResultHandler ignores ResponseEntity Currently ResponseEntityResultHandler is ordered lower than ResponseBodyResultHandler by default whch means a ResponseEntity should not be picked by the ResponseBodyResultHandler. However as it is easy to have both ResponseEntity and @ResponseBody e.g. in @RestControler (or even by mistake) and in general it makes sense for ResponseBodyResultHandler to explicitly recognize and ignore the ResponseEntity return type. --- .../annotation/ResponseBodyResultHandler.java | 42 ++++++++++++++----- .../ResponseBodyResultHandlerTests.java | 29 ++++++------- 2 files changed, 46 insertions(+), 25 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java index 1fbafc0b41..e0ce99ee1c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java @@ -24,9 +24,9 @@ import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.convert.ConversionService; +import org.springframework.http.HttpEntity; import org.springframework.http.converter.reactive.HttpMessageConverter; import org.springframework.web.bind.annotation.ResponseBody; -import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.HandlerResultHandler; import org.springframework.web.reactive.accept.HeaderContentTypeResolver; @@ -39,10 +39,10 @@ import org.springframework.web.server.ServerWebExchange; * with {@code @ResponseBody} writing to the body of the request or response with * an {@link HttpMessageConverter}. * - *

    By default the order for the result handler is set to 100. It detects the - * presence of an {@code @ResponseBody} annotation and should be ordered after - * result handlers that look for a specific return type such as - * {@code ResponseEntity}. + *

    By default the order for this result handler is set to 100. As it detects + * the presence of {@code @ResponseBody} it should be ordered after result + * handlers that look for a specific return type. Note however that this handler + * does recognize and explicitly ignores the {@code ResponseEntity} return type. * * @author Rossen Stoyanchev * @author Stephane Maldini @@ -85,16 +85,36 @@ public class ResponseBodyResultHandler extends AbstractMessageConverterResultHan @Override public boolean supports(HandlerResult result) { - Object handler = result.getHandler(); - if (handler instanceof HandlerMethod) { - MethodParameter returnType = ((HandlerMethod) handler).getReturnType(); - Class containingClass = returnType.getContainingClass(); - return (AnnotationUtils.findAnnotation(containingClass, ResponseBody.class) != null || - returnType.getMethodAnnotation(ResponseBody.class) != null); + ResolvableType returnType = result.getReturnValueType(); + if (returnType.getSource() instanceof MethodParameter) { + MethodParameter parameter = (MethodParameter) returnType.getSource(); + if (hasResponseBodyAnnotation(parameter) && !isHttpEntityType(returnType)) { + return true; + } } return false; } + private boolean hasResponseBodyAnnotation(MethodParameter parameter) { + Class containingClass = parameter.getContainingClass(); + return (AnnotationUtils.findAnnotation(containingClass, ResponseBody.class) != null || + parameter.getMethodAnnotation(ResponseBody.class) != null); + } + + private boolean isHttpEntityType(ResolvableType returnType) { + if (HttpEntity.class.isAssignableFrom(returnType.getRawClass())) { + return true; + } + else if (getConversionService().canConvert(returnType.getRawClass(), Mono.class)) { + ResolvableType genericType = returnType.getGeneric(0); + if (HttpEntity.class.isAssignableFrom(genericType.getRawClass())) { + return true; + } + } + return false; + } + + @Override public Mono handleResult(ServerWebExchange exchange, HandlerResult result) { Object body = result.getReturnValue().orElse(null); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java index f2cd44b397..64a576c487 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java @@ -23,6 +23,7 @@ import java.util.List; import org.junit.Before; import org.junit.Test; +import reactor.core.publisher.Mono; import org.springframework.core.ResolvableType; import org.springframework.core.codec.support.ByteBufferEncoder; @@ -34,6 +35,7 @@ import org.springframework.core.convert.support.GenericConversionService; import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter; import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter; import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; import org.springframework.http.converter.reactive.CodecHttpMessageConverter; import org.springframework.http.converter.reactive.HttpMessageConverter; import org.springframework.http.converter.reactive.ResourceHttpMessageConverter; @@ -108,14 +110,14 @@ public class ResponseBodyResultHandlerTests { @Test public void supports() throws NoSuchMethodException { - TestController controller = new TestController(); - testSupports(controller, "handleReturningString", true); - testSupports(controller, "handleReturningVoid", true); + Object controller = new TestController(); + testSupports(controller, "handleToString", true); testSupports(controller, "doWork", false); - TestRestController restController = new TestRestController(); - testSupports(restController, "handleReturningString", true); - testSupports(restController, "handleReturningVoid", true); + controller = new TestRestController(); + testSupports(controller, "handleToString", true); + testSupports(controller, "handleToResponseEntity", false); + testSupports(controller, "handleToMonoResponseEntity", false); } private void testSupports(Object controller, String method, boolean result) throws NoSuchMethodException { @@ -139,11 +141,15 @@ public class ResponseBodyResultHandlerTests { @RestController @SuppressWarnings("unused") private static class TestRestController { - public String handleReturningString() { + public String handleToString() { return null; } - public Void handleReturningVoid() { + public ResponseEntity handleToResponseEntity() { + return null; + } + + public Mono> handleToMonoResponseEntity() { return null; } } @@ -152,12 +158,7 @@ public class ResponseBodyResultHandlerTests { private static class TestController { @ResponseBody - public String handleReturningString() { - return null; - } - - @ResponseBody - public Void handleReturningVoid() { + public String handleToString() { return null; } From 67175005e33d7b10dcea86ac5afed173be37fa90 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 27 Jun 2016 08:43:15 -0400 Subject: [PATCH 426/498] Add failing Jackson-related response body tests --- .../MessageConverterResultHandlerTests.java | 134 ++++++++++++++++-- 1 file changed, 126 insertions(+), 8 deletions(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterResultHandlerTests.java index e022bbeee1..295ecc4cf7 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterResultHandlerTests.java @@ -17,13 +17,17 @@ package org.springframework.web.reactive.result.method.annotation; import java.io.ByteArrayOutputStream; import java.io.OutputStream; +import java.io.Serializable; import java.net.URI; import java.nio.charset.Charset; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; @@ -33,21 +37,13 @@ import reactor.core.test.TestSubscriber; import rx.Observable; import org.springframework.core.ResolvableType; -import org.springframework.core.codec.Decoder; -import org.springframework.core.codec.Encoder; -import org.springframework.core.codec.support.ByteBufferDecoder; import org.springframework.core.codec.support.ByteBufferEncoder; -import org.springframework.core.codec.support.JacksonJsonDecoder; import org.springframework.core.codec.support.JacksonJsonEncoder; -import org.springframework.core.codec.support.Jaxb2Decoder; import org.springframework.core.codec.support.Jaxb2Encoder; -import org.springframework.core.codec.support.JsonObjectDecoder; -import org.springframework.core.codec.support.StringDecoder; import org.springframework.core.codec.support.StringEncoder; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter; import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter; -import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.core.io.buffer.support.DataBufferTestUtils; @@ -68,6 +64,9 @@ import org.springframework.web.server.session.WebSessionManager; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.mockito.Mockito.mock; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.http.MediaType.APPLICATION_JSON_UTF8; +import static org.springframework.web.reactive.HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE; /** * Unit tests for {@link AbstractMessageConverterResultHandler}. @@ -99,6 +98,18 @@ public class MessageConverterResultHandlerTests { assertEquals("image/x-png", this.response.getHeaders().getFirst("Content-Type")); } + @Test // SPR-13631 + public void useDefaultCharset() throws Exception { + this.exchange.getAttributes().put(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, + Collections.singleton(APPLICATION_JSON)); + + String body = "foo"; + ResolvableType bodyType = ResolvableType.forType(String.class); + this.resultHandler.writeBody(this.exchange, body, bodyType).block(Duration.ofSeconds(5)); + + assertEquals(APPLICATION_JSON_UTF8, this.response.getHeaders().getContentType()); + } + @Test public void voidReturnType() throws Exception { testVoidReturnType(null, ResolvableType.forType(Void.class)); @@ -125,6 +136,40 @@ public class MessageConverterResultHandlerTests { TestSubscriber.subscribe(mono).assertError(IllegalStateException.class); } + @Test // SPR-12811 + @Ignore + public void jacksonTypeOfListElement() throws Exception { + List body = Arrays.asList(new Foo("foo"), new Bar("bar")); + ResolvableType bodyType = ResolvableType.forClassWithGenerics(List.class, ParentClass.class); + this.resultHandler.writeBody(this.exchange, body, bodyType).block(Duration.ofSeconds(5)); + + assertEquals(APPLICATION_JSON_UTF8, this.response.getHeaders().getContentType()); + assertResponseBody("[{\"type\":\"foo\",\"parentProperty\":\"foo\"}," + + "{\"type\":\"bar\",\"parentProperty\":\"bar\"}]"); + } + + @Test // SPR-13318 + @Ignore + public void jacksonTypeWithSubType() throws Exception { + SimpleBean body = new SimpleBean(123L, "foo"); + ResolvableType bodyType = ResolvableType.forClass(Identifiable.class); + this.resultHandler.writeBody(this.exchange, body, bodyType).block(Duration.ofSeconds(5)); + + assertEquals(APPLICATION_JSON_UTF8, this.response.getHeaders().getContentType()); + assertResponseBody("{\"id\":123,\"name\":\"foo\"}"); + } + + @Test // SPR-13318 + @Ignore + public void jacksonTypeWithSubTypeOfListElement() throws Exception { + List body = Arrays.asList(new SimpleBean(123L, "foo"), new SimpleBean(456L, "bar")); + ResolvableType bodyType = ResolvableType.forClassWithGenerics(List.class, Identifiable.class); + this.resultHandler.writeBody(this.exchange, body, bodyType).block(Duration.ofSeconds(5)); + + assertEquals(APPLICATION_JSON_UTF8, this.response.getHeaders().getContentType()); + assertResponseBody("[{\"id\":123,\"name\":\"foo\"},{\"id\":456,\"name\":\"bar\"}]"); + } + private AbstractMessageConverterResultHandler createResultHandler(HttpMessageConverter... converters) { List> converterList; @@ -149,4 +194,77 @@ public class MessageConverterResultHandlerTests { return new AbstractMessageConverterResultHandler(converterList, service, resolver) {}; } + private void assertResponseBody(String responseBody) { + TestSubscriber.subscribe(this.response.getBody()) + .assertValuesWith(buf -> assertEquals(responseBody, + DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); + } + + + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type") + @SuppressWarnings("unused") + private static class ParentClass { + + private String parentProperty; + + public ParentClass() { + } + + public ParentClass(String parentProperty) { + this.parentProperty = parentProperty; + } + + public String getParentProperty() { + return parentProperty; + } + + public void setParentProperty(String parentProperty) { + this.parentProperty = parentProperty; + } + } + + @JsonTypeName("foo") + private static class Foo extends ParentClass { + + public Foo(String parentProperty) { + super(parentProperty); + } + } + + @JsonTypeName("bar") + private static class Bar extends ParentClass { + + public Bar(String parentProperty) { + super(parentProperty); + } + } + + private interface Identifiable extends Serializable { + + @SuppressWarnings("unused") + Long getId(); + } + + @SuppressWarnings({ "serial" }) + private static class SimpleBean implements Identifiable { + + private Long id; + + private String name; + + public SimpleBean(Long id, String name) { + this.id = id; + this.name = name; + } + + @Override + public Long getId() { + return id; + } + + public String getName() { + return name; + } + } + } From cbe2cbcc8829f810113e605554d78245a6de9c72 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 27 Jun 2016 09:34:19 -0400 Subject: [PATCH 427/498] CodecHttpMessageConverter supports a default charset --- .../http/converter/reactive/CodecHttpMessageConverter.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/CodecHttpMessageConverter.java b/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/CodecHttpMessageConverter.java index 02a9cf7c71..f61648784e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/CodecHttpMessageConverter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/CodecHttpMessageConverter.java @@ -144,6 +144,12 @@ public class CodecHttpMessageConverter implements HttpMessageConverter { contentTypeToUse = (mediaType != null ? mediaType : contentTypeToUse); } if (contentTypeToUse != null) { + if (contentTypeToUse.getCharset() == null) { + MediaType mediaType = getDefaultContentType(type); + if (mediaType != null && mediaType.getCharset() != null) { + contentTypeToUse = new MediaType(contentTypeToUse, mediaType.getCharset()); + } + } headers.setContentType(contentTypeToUse); } } From 13b6f4fee47c89f9602710ab1298722fe87504b3 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Tue, 28 Jun 2016 11:11:45 +0200 Subject: [PATCH 428/498] Add SseEventEncoder to WebReactiveConfiguration --- .../config/WebReactiveConfiguration.java | 11 +++- .../config/WebReactiveConfigurationTests.java | 50 +++++++++++-------- 2 files changed, 37 insertions(+), 24 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java index 740dbcc18a..777157c02f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java @@ -46,6 +46,7 @@ import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutu import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter; import org.springframework.format.Formatter; import org.springframework.http.MediaType; +import org.springframework.http.codec.SseEventEncoder; import org.springframework.http.converter.reactive.CodecHttpMessageConverter; import org.springframework.http.converter.reactive.HttpMessageConverter; import org.springframework.http.converter.reactive.ResourceHttpMessageConverter; @@ -241,6 +242,7 @@ public class WebReactiveConfiguration implements ApplicationContextAware { * {@link #configureMessageConverters(List)}. */ protected final void addDefaultHttpMessageConverters(List> converters) { + List> sseDataEncoders = new ArrayList<>(); converters.add(converter(new ByteBufferEncoder(), new ByteBufferDecoder())); converters.add(converter(new StringEncoder(), new StringDecoder())); converters.add(new ResourceHttpMessageConverter()); @@ -248,9 +250,14 @@ public class WebReactiveConfiguration implements ApplicationContextAware { converters.add(converter(new Jaxb2Encoder(), new Jaxb2Decoder())); } if (jackson2Present) { - JsonObjectDecoder objectDecoder = new JsonObjectDecoder(); - converters.add(converter(new JacksonJsonEncoder(), new JacksonJsonDecoder(objectDecoder))); + JacksonJsonEncoder jacksonEncoder = new JacksonJsonEncoder(); + JacksonJsonDecoder jacksonDecoder = new JacksonJsonDecoder(new JsonObjectDecoder()); + converters.add(converter(jacksonEncoder, jacksonDecoder)); + sseDataEncoders.add(jacksonEncoder); + } else { + } + converters.add(converter(new SseEventEncoder(sseDataEncoders), null)); } private static HttpMessageConverter converter(Encoder encoder, Decoder decoder) { diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/WebReactiveConfigurationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/WebReactiveConfigurationTests.java index 0f4e8e32c7..962bd32c85 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/WebReactiveConfigurationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/WebReactiveConfigurationTests.java @@ -24,6 +24,7 @@ import javax.xml.bind.annotation.XmlRootElement; import org.junit.Before; import org.junit.Test; +import static org.springframework.http.MediaType.*; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import rx.Observable; @@ -139,13 +140,14 @@ public class WebReactiveConfigurationTests { assertNotNull(adapter); List> converters = adapter.getMessageConverters(); - assertEquals(5, converters.size()); + assertEquals(6, converters.size()); - assertHasConverter(converters, ByteBuffer.class, MediaType.APPLICATION_OCTET_STREAM); - assertHasConverter(converters, String.class, MediaType.TEXT_PLAIN); - assertHasConverter(converters, Resource.class, MediaType.IMAGE_PNG); - assertHasConverter(converters, TestBean.class, MediaType.APPLICATION_XML); - assertHasConverter(converters, TestBean.class, MediaType.APPLICATION_JSON); + assertHasConverter(converters, ByteBuffer.class, APPLICATION_OCTET_STREAM, APPLICATION_OCTET_STREAM); + assertHasConverter(converters, String.class, TEXT_PLAIN, TEXT_PLAIN); + assertHasConverter(converters, Resource.class, IMAGE_PNG, IMAGE_PNG); + assertHasConverter(converters, TestBean.class, APPLICATION_XML, APPLICATION_XML); + assertHasConverter(converters, TestBean.class, APPLICATION_JSON, APPLICATION_JSON); + assertHasConverter(converters, TestBean.class, null, MediaType.parseMediaType("text/event-stream")); name = "mvcConversionService"; ConversionService service = context.getBean(name, ConversionService.class); @@ -168,8 +170,8 @@ public class WebReactiveConfigurationTests { List> converters = adapter.getMessageConverters(); assertEquals(2, converters.size()); - assertHasConverter(converters, String.class, MediaType.TEXT_PLAIN); - assertHasConverter(converters, TestBean.class, MediaType.APPLICATION_XML); + assertHasConverter(converters, String.class, TEXT_PLAIN, TEXT_PLAIN); + assertHasConverter(converters, TestBean.class, APPLICATION_XML, APPLICATION_XML); } @Test @@ -195,13 +197,14 @@ public class WebReactiveConfigurationTests { assertEquals(0, handler.getOrder()); List> converters = handler.getMessageConverters(); - assertEquals(5, converters.size()); + assertEquals(6, converters.size()); - assertHasConverter(converters, ByteBuffer.class, MediaType.APPLICATION_OCTET_STREAM); - assertHasConverter(converters, String.class, MediaType.TEXT_PLAIN); - assertHasConverter(converters, Resource.class, MediaType.IMAGE_PNG); - assertHasConverter(converters, TestBean.class, MediaType.APPLICATION_XML); - assertHasConverter(converters, TestBean.class, MediaType.APPLICATION_JSON); + assertHasConverter(converters, ByteBuffer.class, APPLICATION_OCTET_STREAM, APPLICATION_OCTET_STREAM); + assertHasConverter(converters, String.class, TEXT_PLAIN, TEXT_PLAIN); + assertHasConverter(converters, Resource.class, IMAGE_PNG, IMAGE_PNG); + assertHasConverter(converters, TestBean.class, APPLICATION_XML, APPLICATION_XML); + assertHasConverter(converters, TestBean.class, APPLICATION_JSON, APPLICATION_JSON); + assertHasConverter(converters, TestBean.class, null, MediaType.parseMediaType("text/event-stream")); name = "mvcContentTypeResolver"; RequestedContentTypeResolver resolver = context.getBean(name, RequestedContentTypeResolver.class); @@ -219,13 +222,14 @@ public class WebReactiveConfigurationTests { assertEquals(100, handler.getOrder()); List> converters = handler.getMessageConverters(); - assertEquals(5, converters.size()); + assertEquals(6, converters.size()); - assertHasConverter(converters, ByteBuffer.class, MediaType.APPLICATION_OCTET_STREAM); - assertHasConverter(converters, String.class, MediaType.TEXT_PLAIN); - assertHasConverter(converters, Resource.class, MediaType.IMAGE_PNG); - assertHasConverter(converters, TestBean.class, MediaType.APPLICATION_XML); - assertHasConverter(converters, TestBean.class, MediaType.APPLICATION_JSON); + assertHasConverter(converters, ByteBuffer.class, APPLICATION_OCTET_STREAM, APPLICATION_OCTET_STREAM); + assertHasConverter(converters, String.class, TEXT_PLAIN, TEXT_PLAIN); + assertHasConverter(converters, Resource.class, IMAGE_PNG, IMAGE_PNG); + assertHasConverter(converters, TestBean.class, APPLICATION_XML, APPLICATION_XML); + assertHasConverter(converters, TestBean.class, APPLICATION_JSON, APPLICATION_JSON); + assertHasConverter(converters, TestBean.class, null, MediaType.parseMediaType("text/event-stream")); name = "mvcContentTypeResolver"; RequestedContentTypeResolver resolver = context.getBean(name, RequestedContentTypeResolver.class); @@ -254,10 +258,12 @@ public class WebReactiveConfigurationTests { } - private void assertHasConverter(List> converters, Class clazz, MediaType mediaType) { + private void assertHasConverter(List> converters, Class clazz, + MediaType readMediaType, MediaType writeMediaType) { ResolvableType type = ResolvableType.forClass(clazz); assertTrue(converters.stream() - .filter(c -> c.canRead(type, mediaType) && c.canWrite(type, mediaType)) + .filter(c -> (readMediaType == null || c.canRead(type, readMediaType)) + && (writeMediaType == null || c.canWrite(type, writeMediaType))) .findAny() .isPresent()); } From 3a681fba890ab6044b54145c2f33bdea0922caa6 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Tue, 28 Jun 2016 11:17:57 +0200 Subject: [PATCH 429/498] AbstractResponseBodySubscriber improvements - AbstractResponseBodySubscriber now checks if the current state is expected before changing to a new state. - Included comments by @violetagg --- .../AbstractResponseBodySubscriber.java | 40 +++++++++++-------- .../reactive/ServletHttpHandlerAdapter.java | 8 ++-- 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractResponseBodySubscriber.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractResponseBodySubscriber.java index 7c174edd12..98d13c52e2 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractResponseBodySubscriber.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractResponseBodySubscriber.java @@ -139,8 +139,8 @@ abstract class AbstractResponseBodySubscriber implements Subscriber */ protected abstract void close(); - private void changeState(State oldState, State newState) { - this.state.compareAndSet(oldState, newState); + private boolean changeState(State oldState, State newState) { + return this.state.compareAndSet(oldState, newState); } /** @@ -171,8 +171,9 @@ abstract class AbstractResponseBodySubscriber implements Subscriber Subscription subscription) { if (BackpressureUtils.validate(subscriber.subscription, subscription)) { subscriber.subscription = subscription; - subscriber.changeState(this, REQUESTED); - subscription.request(1); + if (subscriber.changeState(this, REQUESTED)) { + subscription.request(1); + } } } }, @@ -186,15 +187,17 @@ abstract class AbstractResponseBodySubscriber implements Subscriber @Override void onNext(AbstractResponseBodySubscriber subscriber, DataBuffer dataBuffer) { - subscriber.changeState(this, RECEIVED); - subscriber.receiveBuffer(dataBuffer); + if (subscriber.changeState(this, RECEIVED)) { + subscriber.receiveBuffer(dataBuffer); + } } @Override void onComplete(AbstractResponseBodySubscriber subscriber) { - subscriber.subscriptionCompleted = true; - subscriber.changeState(this, COMPLETED); - subscriber.close(); + if (subscriber.changeState(this, COMPLETED)) { + subscriber.subscriptionCompleted = true; + subscriber.close(); + } } }, /** @@ -217,12 +220,14 @@ abstract class AbstractResponseBodySubscriber implements Subscriber subscriber.releaseBuffer(); boolean subscriptionCompleted = subscriber.subscriptionCompleted; if (!subscriptionCompleted) { - subscriber.changeState(this, REQUESTED); - subscriber.subscription.request(1); + if (subscriber.changeState(this, REQUESTED)) { + subscriber.subscription.request(1); + } } else { - subscriber.changeState(this, COMPLETED); - subscriber.close(); + if (subscriber.changeState(this, COMPLETED)) { + subscriber.close(); + } } } } @@ -258,7 +263,7 @@ abstract class AbstractResponseBodySubscriber implements Subscriber }; void onSubscribe(AbstractResponseBodySubscriber subscriber, Subscription s) { - throw new IllegalStateException(toString()); + s.cancel(); } void onNext(AbstractResponseBodySubscriber subscriber, DataBuffer dataBuffer) { @@ -266,9 +271,10 @@ abstract class AbstractResponseBodySubscriber implements Subscriber } void onError(AbstractResponseBodySubscriber subscriber, Throwable t) { - subscriber.changeState(this, COMPLETED); - subscriber.writeError(t); - subscriber.close(); + if (subscriber.changeState(this, COMPLETED)) { + subscriber.writeError(t); + subscriber.close(); + } } void onComplete(AbstractResponseBodySubscriber subscriber) { diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java index aa1edf35cd..3f0f430424 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java @@ -269,7 +269,8 @@ public class ServletHttpHandlerAdapter extends HttpServlet { onWritePossible(); } } - catch (IOException ignored) { + catch (IOException ex) { + onError(ex); } } @@ -277,13 +278,12 @@ public class ServletHttpHandlerAdapter extends HttpServlet { protected boolean write(DataBuffer dataBuffer) throws IOException { ServletOutputStream output = outputStream(); - boolean ready = output.isReady(); - if (this.flushOnNext) { flush(); - ready = output.isReady(); } + boolean ready = output.isReady(); + if (this.logger.isTraceEnabled()) { this.logger.trace("write: " + dataBuffer + " ready: " + ready); } From 11ed847aca046590e162d631d8a6ef67791d01cb Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Tue, 28 Jun 2016 10:49:15 +0200 Subject: [PATCH 430/498] AbstractRequestBodyPublisher improvements Reactored Servlet 3.1 and Undertow request support (AbstractResponseBodySubscriber) to use an internal state machine, making thread-safity a lot easier. --- .../AbstractRequestBodyPublisher.java | 389 ++++++++++++------ .../reactive/ServletHttpHandlerAdapter.java | 82 ++-- .../reactive/UndertowHttpHandlerAdapter.java | 90 ++-- 3 files changed, 321 insertions(+), 240 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractRequestBodyPublisher.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractRequestBodyPublisher.java index 2a29fec904..5be914a6cb 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractRequestBodyPublisher.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractRequestBodyPublisher.java @@ -16,16 +16,21 @@ package org.springframework.http.server.reactive; +import java.io.IOException; +import java.nio.channels.Channel; import java.util.Objects; import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import javax.servlet.ReadListener; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import reactor.core.util.BackpressureUtils; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.util.Assert; /** * Abstract base class for {@code Publisher} implementations that bridge between @@ -38,171 +43,287 @@ import org.springframework.util.Assert; */ abstract class AbstractRequestBodyPublisher implements Publisher { - private ResponseBodySubscription subscription; + protected final Log logger = LogFactory.getLog(getClass()); - private volatile boolean stalled; + private final AtomicReference state = + new AtomicReference<>(State.UNSUBSCRIBED); + + private final AtomicLong demand = new AtomicLong(); + + private Subscriber subscriber; @Override public void subscribe(Subscriber subscriber) { - Objects.requireNonNull(subscriber); - Assert.state(this.subscription == null, "Only a single subscriber allowed"); - - this.subscription = new ResponseBodySubscription(subscriber); - subscriber.onSubscribe(this.subscription); + if (this.logger.isTraceEnabled()) { + this.logger.trace(this.state + " subscribe: " + subscriber); + } + this.state.get().subscribe(this, subscriber); } /** - * Publishes the given signal to the subscriber. - * @param dataBuffer the signal to publish - * @see Subscriber#onNext(Object) + * Called via a listener interface to indicate that reading is possible. + * @see ReadListener#onDataAvailable() + * @see org.xnio.ChannelListener#handleEvent(Channel) */ - protected final void publishOnNext(DataBuffer dataBuffer) { - Assert.state(this.subscription != null); - this.subscription.publishOnNext(dataBuffer); + protected final void onDataAvailable() { + if (this.logger.isTraceEnabled()) { + this.logger.trace(this.state + " onDataAvailable"); + } + this.state.get().onDataAvailable(this); } /** - * Publishes the given error to the subscriber. - * @param t the error to publish - * @see Subscriber#onError(Throwable) + * Called via a listener interface to indicate that all data has been read. + * @see ReadListener#onAllDataRead() + * @see org.xnio.ChannelListener#handleEvent(Channel) */ - protected final void publishOnError(Throwable t) { - if (this.subscription != null) { - this.subscription.publishOnError(t); + protected final void onAllDataRead() { + if (this.logger.isTraceEnabled()) { + this.logger.trace(this.state + " onAllDataRead"); } + this.state.get().onAllDataRead(this); } /** - * Publishes the complete signal to the subscriber. - * @see Subscriber#onComplete() + * Called by a listener interface to indicate that as error has occured. + * @param t the error + * @see ReadListener#onError(Throwable) */ - protected final void publishOnComplete() { - if (this.subscription != null) { - this.subscription.publishOnComplete(); + protected final void onError(Throwable t) { + if (this.logger.isErrorEnabled()) { + this.logger.error(this.state + " onError: " + t, t); } + this.state.get().onError(this, t); } /** - * Returns true if the {@code Subscriber} associated with this {@code Publisher} has - * cancelled its {@code Subscription}. - * @return {@code true} if a subscriber has been registered and its subscription has - * been cancelled; {@code false} otherwise - * @see ResponseBodySubscription#isCancelled() - * @see Subscription#cancel() + * Reads and publishes data buffers from the input. Continues till either there is no + * more demand, or till there is no more data to be read. + * @return {@code true} if there is more data to be read; {@code false} otherwise */ - protected final boolean isSubscriptionCancelled() { - return (this.subscription != null && this.subscription.isCancelled()); - } - - /** - * Checks the subscription for demand, and marks this publisher as "stalled" if there - * is none. The next time the subscriber {@linkplain Subscription#request(long) - * requests} more events, the {@link #noLongerStalled()} method is called. - * @return {@code true} if there is demand; {@code false} otherwise - */ - protected final boolean checkSubscriptionForDemand() { - if (this.subscription == null || !this.subscription.hasDemand()) { - this.stalled = true; - return false; - } - else { - return true; - } - } - - /** - * Abstract template method called when this publisher is no longer "stalled". Used in - * sub-classes to resume reading from the request. - */ - protected abstract void noLongerStalled(); - - private final class ResponseBodySubscription implements Subscription { - - private final Subscriber subscriber; - - private final AtomicLong demand = new AtomicLong(); - - private boolean cancelled; - - public ResponseBodySubscription(Subscriber subscriber) { - Assert.notNull(subscriber, "'subscriber' must not be null"); - - this.subscriber = subscriber; - } - - @Override - public final void cancel() { - this.cancelled = true; - } - - /** - * Indicates whether this subscription has been cancelled. - * @see #cancel() - */ - protected final boolean isCancelled() { - return this.cancelled; - } - - @Override - public final void request(long n) { - if (!isCancelled() && BackpressureUtils.checkRequest(n, this.subscriber)) { - long demand = BackpressureUtils.addAndGet(this.demand, n); - - if (stalled && demand > 0) { - stalled = false; - noLongerStalled(); - } - } - } - - /** - * Indicates whether this subscription has demand. - * @see #request(long) - */ - protected final boolean hasDemand() { - return this.demand.get() > 0; - } - - /** - * Publishes the given signal to the subscriber wrapped by this subscription, if - * it has not been cancelled. If there is {@linkplain #hasDemand() no demand} for - * the signal, an exception will be thrown. - * @param dataBuffer the signal to publish - * @see Subscriber#onNext(Object) - */ - protected final void publishOnNext(DataBuffer dataBuffer) { - if (!isCancelled()) { - if (hasDemand()) { + private boolean readAndPublish() { + try { + while (hasDemand()) { + DataBuffer dataBuffer = read(); + if (dataBuffer != null) { BackpressureUtils.getAndSub(this.demand, 1L); this.subscriber.onNext(dataBuffer); } else { - throw new IllegalStateException("No demand for: " + dataBuffer); + return false; + } + } + return true; + } + catch (IOException ex) { + onError(ex); + return false; + } + } + + /** + * Reads a data buffer from the input, if possible. Returns {@code null} if a buffer + * could not be read. + * @return the data buffer that was read; or {@code null} + */ + protected abstract DataBuffer read() throws IOException; + + /** + * Closes the input. + */ + protected abstract void close(); + + private boolean hasDemand() { + return this.demand.get() > 0; + } + + private boolean changeState(AbstractRequestBodyPublisher.State oldState, + AbstractRequestBodyPublisher.State newState) { + return this.state.compareAndSet(oldState, newState); + } + + private static final class RequestBodySubscription implements Subscription { + + private final AbstractRequestBodyPublisher publisher; + + public RequestBodySubscription(AbstractRequestBodyPublisher publisher) { + this.publisher = publisher; + } + + @Override + public final void request(long n) { + if (this.publisher.logger.isTraceEnabled()) { + this.publisher.logger.trace(state() + " request: " + n); + } + state().request(this.publisher, n); + } + + @Override + public final void cancel() { + if (this.publisher.logger.isTraceEnabled()) { + this.publisher.logger.trace(state() + " cancel"); + } + state().cancel(this.publisher); + } + + private AbstractRequestBodyPublisher.State state() { + return this.publisher.state.get(); + } + + } + + /** + * Represents a state for the {@link Publisher} to be in. The following figure + * indicate the four different states that exist, and the relationships between them. + * + *

    +	 *       UNSUBSCRIBED
    +	 *        |
    +	 *        v
    +	 * DATA_UNAVAILABLE <---> DATA_AVAILABLE
    +	 *                |       |
    +	 *                v       v
    +	 *                COMPLETED
    +	 * 
    + * Refer to the individual states for more information. + */ + + private enum State { + /** + * The initial unsubscribed state. Will respond to {@link + * #subscribe(AbstractRequestBodyPublisher, Subscriber)} by + * changing state to {@link #DATA_UNAVAILABLE}. + */ + UNSUBSCRIBED { + @Override + void subscribe(AbstractRequestBodyPublisher publisher, + Subscriber subscriber) { + Objects.requireNonNull(subscriber); + if (publisher.changeState(this, DATA_UNAVAILABLE)) { + Subscription subscription = new RequestBodySubscription( + publisher); + publisher.subscriber = subscriber; + subscriber.onSubscribe(subscription); + } + else { + throw new IllegalStateException(toString()); + } + } + }, + /** + * State that gets entered when there is no data to be read. Responds to {@link + * #request(AbstractRequestBodyPublisher, long)} by increasing the demand, and + * responds to {@link #onDataAvailable(AbstractRequestBodyPublisher)} by + * reading the available data and changing state to {@link #DATA_AVAILABLE} if + * there continues to be more data available after the demand has been satisfied. + */ + DATA_UNAVAILABLE { + @Override + void request(AbstractRequestBodyPublisher publisher, long n) { + if (BackpressureUtils.checkRequest(n, publisher.subscriber)) { + BackpressureUtils.addAndGet(publisher.demand, n); + } + } + + @Override + void onDataAvailable(AbstractRequestBodyPublisher publisher) { + boolean dataAvailable = publisher.readAndPublish(); + if (dataAvailable) { + publisher.changeState(this, DATA_AVAILABLE); + } + } + + }, + /** + * State that gets entered when there is data to be read. Responds to {@link + * #request(AbstractRequestBodyPublisher, long)} by increasing the demand, and + * by reading the available data and changing state to {@link #DATA_UNAVAILABLE} + * if there is no more data available. + */ + DATA_AVAILABLE { + @Override + void request(AbstractRequestBodyPublisher publisher, long n) { + if (BackpressureUtils.checkRequest(n, publisher.subscriber)) { + BackpressureUtils.addAndGet(publisher.demand, n); + boolean dataAvailable = publisher.readAndPublish(); + if (!dataAvailable) { + publisher.changeState(this, DATA_UNAVAILABLE); + } + } + } + }, + /** + * The terminal completed state. Does not respond to any events. + */ + COMPLETED { + @Override + void subscribe(AbstractRequestBodyPublisher publisher, + Subscriber subscriber) { + // ignore + } + + @Override + void request(AbstractRequestBodyPublisher publisher, long n) { + // ignore + } + + @Override + void cancel(AbstractRequestBodyPublisher publisher) { + // ignore + } + + @Override + void onDataAvailable(AbstractRequestBodyPublisher publisher) { + // ignore + } + + @Override + void onAllDataRead(AbstractRequestBodyPublisher publisher) { + // ignore + } + + @Override + void onError(AbstractRequestBodyPublisher publisher, Throwable t) { + // ignore + } + }; + + void subscribe(AbstractRequestBodyPublisher publisher, + Subscriber subscriber) { + throw new IllegalStateException(toString()); + } + + void request(AbstractRequestBodyPublisher publisher, long n) { + throw new IllegalStateException(toString()); + } + + void cancel(AbstractRequestBodyPublisher publisher) { + if (publisher.changeState(this, COMPLETED)) { + publisher.close(); + } + } + + void onDataAvailable(AbstractRequestBodyPublisher publisher) { + throw new IllegalStateException(toString()); + } + + void onAllDataRead(AbstractRequestBodyPublisher publisher) { + if (publisher.changeState(this, COMPLETED)) { + publisher.close(); + if (publisher.subscriber != null) { + publisher.subscriber.onComplete(); } } } - /** - * Publishes the given error to the subscriber wrapped by this subscription, if it - * has not been cancelled. - * @param t the error to publish - * @see Subscriber#onError(Throwable) - */ - protected final void publishOnError(Throwable t) { - if (!isCancelled()) { - this.subscriber.onError(t); + void onError(AbstractRequestBodyPublisher publisher, Throwable t) { + if (publisher.changeState(this, COMPLETED)) { + publisher.close(); + if (publisher.subscriber != null) { + publisher.subscriber.onError(t); + } } } - /** - * Publishes the complete signal to the subscriber wrapped by this subscription, - * if it has not been cancelled. - * @see Subscriber#onComplete() - */ - protected final void publishOnComplete() { - if (!isCancelled()) { - this.subscriber.onComplete(); - } - } } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java index 3f0f430424..4314ec221e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java @@ -141,10 +141,8 @@ public class ServletHttpHandlerAdapter extends HttpServlet { private static class RequestBodyPublisher extends AbstractRequestBodyPublisher { - private static final Log logger = LogFactory.getLog(RequestBodyPublisher.class); - - private final RequestBodyReadListener readListener = - new RequestBodyReadListener(); + private final RequestBodyPublisher.RequestBodyReadListener readListener = + new RequestBodyPublisher.RequestBodyReadListener(); private final ServletAsyncContextSynchronizer synchronizer; @@ -165,76 +163,50 @@ public class ServletHttpHandlerAdapter extends HttpServlet { } @Override - protected void noLongerStalled() { - try { - this.readListener.onDataAvailable(); - } - catch (IOException ex) { - this.readListener.onError(ex); + protected DataBuffer read() throws IOException { + ServletInputStream input = this.synchronizer.getRequest().getInputStream(); + if (input.isReady()) { + int read = input.read(this.buffer); + if (logger.isTraceEnabled()) { + logger.trace("read:" + read); + } + + if (read > 0) { + DataBuffer dataBuffer = this.dataBufferFactory.allocateBuffer(read); + dataBuffer.write(this.buffer, 0, read); + return dataBuffer; + } } + return null; + } + + @Override + protected void close() { + this.synchronizer.readComplete(); + } private class RequestBodyReadListener implements ReadListener { @Override public void onDataAvailable() throws IOException { - if (isSubscriptionCancelled()) { - return; - } - logger.trace("onDataAvailable"); - ServletInputStream input = - RequestBodyPublisher.this.synchronizer.getRequest() - .getInputStream(); - - while (true) { - if (!checkSubscriptionForDemand()) { - break; - } - - boolean ready = input.isReady(); - logger.trace( - "Input ready: " + ready + " finished: " + input.isFinished()); - - if (!ready) { - break; - } - - int read = input.read(RequestBodyPublisher.this.buffer); - logger.trace("Input read:" + read); - - if (read == -1) { - break; - } - else if (read > 0) { - DataBuffer dataBuffer = - RequestBodyPublisher.this.dataBufferFactory - .allocateBuffer(read); - dataBuffer.write(RequestBodyPublisher.this.buffer, 0, read); - - publishOnNext(dataBuffer); - } - } + RequestBodyPublisher.this.onDataAvailable(); } @Override public void onAllDataRead() throws IOException { - logger.trace("All data read"); - RequestBodyPublisher.this.synchronizer.readComplete(); - - publishOnComplete(); + RequestBodyPublisher.this.onAllDataRead(); } @Override - public void onError(Throwable t) { - logger.trace("RequestBodyReadListener Error", t); - RequestBodyPublisher.this.synchronizer.readComplete(); + public void onError(Throwable throwable) { + RequestBodyPublisher.this.onError(throwable); - publishOnError(t); } } - } + private static class ResponseBodySubscriber extends AbstractResponseBodySubscriber { private final ResponseBodyWriteListener writeListener = diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java index 4d3f4ac7ae..dfc9d5837b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java @@ -57,7 +57,6 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle this.dataBufferFactory = dataBufferFactory; } - @Override public void handleRequest(HttpServerExchange exchange) throws Exception { @@ -72,7 +71,8 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle responseBody.registerListener(); ServerHttpResponse response = new UndertowServerHttpResponse(exchange, responseChannel, - publisher -> Mono.from(subscriber -> publisher.subscribe(responseBody)), + publisher -> Mono + .from(subscriber -> publisher.subscribe(responseBody)), this.dataBufferFactory); this.delegate.handle(request, response).subscribe(new Subscriber() { @@ -90,7 +90,8 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle @Override public void onError(Throwable ex) { if (exchange.isResponseStarted() || exchange.getStatusCode() > 500) { - logger.error("Error from request handling. Completing the request.", ex); + logger.error("Error from request handling. Completing the request.", + ex); } else { exchange.setStatusCode(500); @@ -107,10 +108,11 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle private static class RequestBodyPublisher extends AbstractRequestBodyPublisher { - private static final Log logger = LogFactory.getLog(RequestBodyPublisher.class); + private final ChannelListener readListener = + new ReadListener(); - private final ChannelListener listener = - new RequestBodyListener(); + private final ChannelListener closeListener = + new CloseListener(); private final StreamSourceChannel requestChannel; @@ -127,11 +129,31 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle } public void registerListener() { - this.requestChannel.getReadSetter().set(this.listener); + this.requestChannel.getReadSetter().set(this.readListener); + this.requestChannel.getCloseSetter().set(this.closeListener); this.requestChannel.resumeReads(); } - private void close() { + @Override + protected DataBuffer read() throws IOException { + ByteBuffer byteBuffer = this.pooledByteBuffer.getBuffer(); + int read = this.requestChannel.read(byteBuffer); + if (logger.isTraceEnabled()) { + logger.trace("read:" + read); + } + + if (read > 0) { + byteBuffer.flip(); + return this.dataBufferFactory.wrap(byteBuffer); + } + else if (read == -1) { + onAllDataRead(); + } + return null; + } + + @Override + protected void close() { if (this.pooledByteBuffer != null) { IoUtils.safeClose(this.pooledByteBuffer); } @@ -140,54 +162,21 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle } } - @Override - protected void noLongerStalled() { - this.listener.handleEvent(this.requestChannel); - } - - private class RequestBodyListener - implements ChannelListener { + private class ReadListener implements ChannelListener { @Override public void handleEvent(StreamSourceChannel channel) { - if (isSubscriptionCancelled()) { - return; - } - logger.trace("handleEvent"); - ByteBuffer byteBuffer = - RequestBodyPublisher.this.pooledByteBuffer.getBuffer(); - try { - while (true) { - if (!checkSubscriptionForDemand()) { - break; - } - int read = channel.read(byteBuffer); - logger.trace("Input read:" + read); - - if (read == -1) { - publishOnComplete(); - close(); - break; - } - else if (read == 0) { - // input not ready, wait until we are invoked again - break; - } - else { - byteBuffer.flip(); - DataBuffer dataBuffer = - RequestBodyPublisher.this.dataBufferFactory - .wrap(byteBuffer); - publishOnNext(dataBuffer); - } - } - } - catch (IOException ex) { - publishOnError(ex); - } + onDataAvailable(); } } + private class CloseListener implements ChannelListener { + + @Override + public void handleEvent(StreamSourceChannel channel) { + onAllDataRead(); + } + } } private static class ResponseBodySubscriber extends AbstractResponseBodySubscriber { @@ -296,5 +285,4 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle } - } \ No newline at end of file From 0605c0f3be7740be7147faadfb7df080632c1981 Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Wed, 29 Jun 2016 10:23:38 +0300 Subject: [PATCH 431/498] Make AbstractResponseBodySubscriber.onSubscribe thread-safe When there are simultaneous invocations of onSubscribe, only the first one should succeed, the rest should cancel the provided subscriptions --- .../reactive/AbstractResponseBodySubscriber.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractResponseBodySubscriber.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractResponseBodySubscriber.java index 98d13c52e2..8158d525f7 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractResponseBodySubscriber.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractResponseBodySubscriber.java @@ -18,6 +18,7 @@ package org.springframework.http.server.reactive; import java.io.IOException; import java.nio.channels.Channel; +import java.util.Objects; import java.util.concurrent.atomic.AtomicReference; import javax.servlet.WriteListener; @@ -169,11 +170,13 @@ abstract class AbstractResponseBodySubscriber implements Subscriber @Override void onSubscribe(AbstractResponseBodySubscriber subscriber, Subscription subscription) { - if (BackpressureUtils.validate(subscriber.subscription, subscription)) { + Objects.requireNonNull(subscription, "Subscription cannot be null"); + if (subscriber.changeState(this, REQUESTED)) { subscriber.subscription = subscription; - if (subscriber.changeState(this, REQUESTED)) { - subscription.request(1); - } + subscription.request(1); + } + else { + super.onSubscribe(subscriber, subscription); } } }, From 1f2fbba89bc46783f3216f0f3e7d89f4730779b6 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Wed, 29 Jun 2016 10:34:46 +0200 Subject: [PATCH 432/498] Make JsonObjectDecoder mandatory in JacksonJsonDecoder --- .../codec/support/JacksonJsonDecoder.java | 11 ++----- .../web/client/reactive/WebClient.java | 3 +- .../config/WebReactiveConfiguration.java | 3 +- .../support/JacksonJsonDecoderTests.java | 33 ++----------------- .../RequestBodyArgumentResolverTests.java | 3 +- .../annotation/SseIntegrationTests.java | 3 +- 6 files changed, 10 insertions(+), 46 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonDecoder.java index 93815729db..a72f187ed3 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonDecoder.java @@ -45,22 +45,17 @@ public class JacksonJsonDecoder extends AbstractDecoder { private final ObjectMapper mapper; - private Decoder preProcessor; + private final Decoder preProcessor = new JsonObjectDecoder(); public JacksonJsonDecoder() { - this(new ObjectMapper(), null); + this(new ObjectMapper()); } - public JacksonJsonDecoder(Decoder preProcessor) { - this(new ObjectMapper(), preProcessor); - } - - public JacksonJsonDecoder(ObjectMapper mapper, Decoder preProcessor) { + public JacksonJsonDecoder(ObjectMapper mapper) { super(new MimeType("application", "json", StandardCharsets.UTF_8), new MimeType("application", "*+json", StandardCharsets.UTF_8)); this.mapper = mapper; - this.preProcessor = preProcessor; } @Override diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebClient.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebClient.java index 50f9205c2f..f38f7b02f8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebClient.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebClient.java @@ -28,7 +28,6 @@ import org.springframework.core.codec.support.ByteBufferDecoder; import org.springframework.core.codec.support.ByteBufferEncoder; import org.springframework.core.codec.support.JacksonJsonDecoder; import org.springframework.core.codec.support.JacksonJsonEncoder; -import org.springframework.core.codec.support.JsonObjectDecoder; import org.springframework.core.codec.support.StringDecoder; import org.springframework.core.codec.support.StringEncoder; import org.springframework.http.HttpStatus; @@ -86,7 +85,7 @@ public final class WebClient { this.messageEncoders = Arrays.asList(new ByteBufferEncoder(), new StringEncoder(), new JacksonJsonEncoder()); this.messageDecoders = Arrays.asList(new ByteBufferDecoder(), new StringDecoder(), - new JacksonJsonDecoder(new JsonObjectDecoder())); + new JacksonJsonDecoder()); } /** diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java index 777157c02f..f49056c4eb 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java @@ -36,7 +36,6 @@ import org.springframework.core.codec.support.JacksonJsonDecoder; import org.springframework.core.codec.support.JacksonJsonEncoder; import org.springframework.core.codec.support.Jaxb2Decoder; import org.springframework.core.codec.support.Jaxb2Encoder; -import org.springframework.core.codec.support.JsonObjectDecoder; import org.springframework.core.codec.support.StringDecoder; import org.springframework.core.codec.support.StringEncoder; import org.springframework.core.convert.converter.Converter; @@ -251,7 +250,7 @@ public class WebReactiveConfiguration implements ApplicationContextAware { } if (jackson2Present) { JacksonJsonEncoder jacksonEncoder = new JacksonJsonEncoder(); - JacksonJsonDecoder jacksonDecoder = new JacksonJsonDecoder(new JsonObjectDecoder()); + JacksonJsonDecoder jacksonDecoder = new JacksonJsonDecoder(); converters.add(converter(jacksonEncoder, jacksonDecoder)); sseDataEncoders.add(jacksonEncoder); } else { diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonDecoderTests.java index c9c9110589..6554bc32fc 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonDecoderTests.java @@ -59,8 +59,8 @@ public class JacksonJsonDecoderTests extends AbstractDataBufferAllocatingTestCas } @Test - @Ignore // Issues 112 (no generic type), otherwise works - public void decodeToListWithoutObjectDecoder() throws Exception { + @Ignore // Issue 109 + public void decodeToList() throws Exception { Flux source = Flux.just(stringBuffer( "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]")); @@ -73,8 +73,7 @@ public class JacksonJsonDecoderTests extends AbstractDataBufferAllocatingTestCas } @Test - @Ignore // Issue 109 - public void decodeToFluxWithoutObjectDecoder() throws Exception { + public void decodeToFlux() throws Exception { Flux source = Flux.just(stringBuffer( "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]")); @@ -85,32 +84,6 @@ public class JacksonJsonDecoderTests extends AbstractDataBufferAllocatingTestCas assertValues(new Pojo("f1", "b1"), new Pojo("f2", "b2")); } - @Test - @Ignore // Issue 109 - public void decodeToListWithObjectDecoder() throws Exception { - Flux source = Flux.just(stringBuffer( - "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]")); - - Method method = getClass().getDeclaredMethod("handle", List.class); - ResolvableType elementType = ResolvableType.forMethodParameter(method, 0); - Flux flux = new JacksonJsonDecoder(new JsonObjectDecoder()).decode(source, elementType, null); - - TestSubscriber.subscribe(flux).assertNoError().assertComplete(). - assertValues(Arrays.asList(new Pojo("f1", "b1"), new Pojo("f2", "b2"))); - } - - @Test - public void decodeToFluxWithObjectDecoder() throws Exception { - Flux source = Flux.just(stringBuffer( - "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]")); - - ResolvableType elementType = ResolvableType.forClass(Pojo.class); - Flux flux = new JacksonJsonDecoder(new JsonObjectDecoder()).decode(source, elementType, null); - - TestSubscriber.subscribe(flux).assertNoError().assertComplete(). - assertValues(new Pojo("f1", "b1"), new Pojo("f2", "b2")); - } - @SuppressWarnings("unused") void handle(List list) { } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java index 50bb84234d..6f46067fab 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java @@ -44,7 +44,6 @@ import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.annotation.SynthesizingMethodParameter; import org.springframework.core.codec.Decoder; import org.springframework.core.codec.support.JacksonJsonDecoder; -import org.springframework.core.codec.support.JsonObjectDecoder; import org.springframework.core.codec.support.StringDecoder; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter; @@ -93,7 +92,7 @@ public class RequestBodyArgumentResolverTests { @Before public void setUp() throws Exception { - this.resolver = resolver(new JacksonJsonDecoder(new JsonObjectDecoder())); + this.resolver = resolver(new JacksonJsonDecoder()); this.request = new MockServerHttpRequest(HttpMethod.GET, new URI("/path")); MockServerHttpResponse response = new MockServerHttpResponse(); DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java index f25d567c7a..673728f3f7 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java @@ -33,7 +33,6 @@ import org.springframework.core.codec.Encoder; import org.springframework.core.codec.support.ByteBufferDecoder; import org.springframework.core.codec.support.JacksonJsonDecoder; import org.springframework.core.codec.support.JacksonJsonEncoder; -import org.springframework.core.codec.support.JsonObjectDecoder; import org.springframework.core.codec.support.StringDecoder; import org.springframework.http.MediaType; import org.springframework.http.client.reactive.ReactorHttpClientRequestFactory; @@ -69,7 +68,7 @@ public class SseIntegrationTests extends AbstractHttpHandlerIntegrationTests { this.webClient.setMessageDecoders(Arrays.asList( new ByteBufferDecoder(), new StringDecoder(false), - new JacksonJsonDecoder(new JsonObjectDecoder()))); + new JacksonJsonDecoder())); } @Override From 76baf85cfb7f2589ef4a80bf0318e05caf3bd2f6 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Wed, 29 Jun 2016 14:19:12 +0200 Subject: [PATCH 433/498] Polish JacksonJsonDecoder --- .../codec/support/JacksonJsonDecoder.java | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonDecoder.java index a72f187ed3..13d2d7aea2 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonDecoder.java @@ -68,20 +68,16 @@ public class JacksonJsonDecoder extends AbstractDecoder { JavaType javaType = typeFactory.constructType(elementType.getType()); ObjectReader reader = this.mapper.readerFor(javaType); - Flux stream = Flux.from(inputStream); - if (this.preProcessor != null) { - stream = this.preProcessor.decode(inputStream, elementType, mimeType, hints); - } - - return stream.map(dataBuffer -> { - try { - Object value = reader.readValue(dataBuffer.asInputStream()); - DataBufferUtils.release(dataBuffer); - return value; - } - catch (IOException e) { - throw new CodecException("Error while reading the data", e); - } + return this.preProcessor.decode(inputStream, elementType, mimeType, hints) + .map(dataBuffer -> { + try { + Object value = reader.readValue(dataBuffer.asInputStream()); + DataBufferUtils.release(dataBuffer); + return value; + } + catch (IOException e) { + return Flux.error(new CodecException("Error while reading the data", e)); + } }); } From 478b4149f78a47b00fed64b70e14a834f5f7a009 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 29 Jun 2016 12:04:51 -0400 Subject: [PATCH 434/498] Add netty-buffer as a compile dependency This is a temporary measure for M1 as the dependency will go away once issue #116 is addressed. --- spring-web-reactive/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index 15b2b0ede0..874db4012b 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -95,6 +95,7 @@ dependencies { compile "org.reactivestreams:reactive-streams:1.0.0" compile "io.projectreactor:reactor-core:${reactorVersion}" compile "commons-logging:commons-logging:1.2" + compile "io.netty:netty-buffer:${nettyVersion}" // Temporarily for JsonObjectDecoder (GH #116) optional "org.springframework:spring-context-support:${springVersion}" // for FreeMarker optional "io.reactivex:rxjava:${rxJavaVersion}" From e9d8152ab2b25ba6ec51fadf5206e7c82d7a23e3 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 29 Jun 2016 17:34:45 -0400 Subject: [PATCH 435/498] Port fix for SPR-14397 This is a port of the following commit, adapted for Java 8+: https://github.com/spring-projects/spring-framework/commit/89396ff01ff159aa7df18e332f3888cf9ce3dc20 --- .../RequestMappingInfoHandlerMapping.java | 273 ++++++++++++------ ...RequestMappingInfoHandlerMappingTests.java | 74 ++--- 2 files changed, 214 insertions(+), 133 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java index 8712b1c69b..3a9dbd36d8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java @@ -20,7 +20,6 @@ import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; -import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; @@ -35,18 +34,16 @@ import org.springframework.http.HttpMethod; import org.springframework.http.InvalidMediaTypeException; import org.springframework.http.MediaType; import org.springframework.http.server.reactive.ServerHttpRequest; -import org.springframework.util.CollectionUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; -import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerMapping; -import org.springframework.web.reactive.result.condition.ParamsRequestCondition; -import org.springframework.web.server.ServerWebInputException; +import org.springframework.web.reactive.result.condition.NameValueExpression; import org.springframework.web.server.MethodNotAllowedException; import org.springframework.web.server.NotAcceptableStatusException; import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.ServerWebInputException; import org.springframework.web.server.UnsupportedMediaTypeStatusException; /** @@ -207,59 +204,29 @@ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMe * method but not by query parameter conditions */ @Override - protected HandlerMethod handleNoMatch(Set requestMappingInfos, - String lookupPath, ServerWebExchange exchange) throws Exception { + protected HandlerMethod handleNoMatch(Set infos, String lookupPath, + ServerWebExchange exchange) throws Exception { - Set allowedMethods = new LinkedHashSet<>(4); + PartialMatchHelper helper = new PartialMatchHelper(infos, exchange); - Set patternMatches = new HashSet<>(); - Set patternAndMethodMatches = new HashSet<>(); - - for (RequestMappingInfo info : requestMappingInfos) { - if (info.getPatternsCondition().getMatchingCondition(exchange) != null) { - patternMatches.add(info); - if (info.getMethodsCondition().getMatchingCondition(exchange) != null) { - patternAndMethodMatches.add(info); - } - else { - for (RequestMethod method : info.getMethodsCondition().getMethods()) { - allowedMethods.add(method.name()); - } - } - } + if (helper.isEmpty()) { + return null; } ServerHttpRequest request = exchange.getRequest(); - if (patternMatches.isEmpty()) { - return null; - } - else if (patternAndMethodMatches.isEmpty()) { + + if (helper.hasMethodsMismatch()) { HttpMethod httpMethod = request.getMethod(); + Set methods = helper.getAllowedMethods(); if (HttpMethod.OPTIONS.matches(httpMethod.name())) { - HttpOptionsHandler handler = new HttpOptionsHandler(allowedMethods); + HttpOptionsHandler handler = new HttpOptionsHandler(methods); return new HandlerMethod(handler, HTTP_OPTIONS_HANDLE_METHOD); } - else if (!allowedMethods.isEmpty()) { - throw new MethodNotAllowedException(httpMethod.name(), allowedMethods); - } + throw new MethodNotAllowedException(httpMethod.name(), methods); } - Set consumableMediaTypes; - Set producibleMediaTypes; - List> paramConditions; - - if (patternAndMethodMatches.isEmpty()) { - consumableMediaTypes = getConsumableMediaTypes(exchange, patternMatches); - producibleMediaTypes = getProducibleMediaTypes(exchange, patternMatches); - paramConditions = getRequestParams(exchange, patternMatches); - } - else { - consumableMediaTypes = getConsumableMediaTypes(exchange, patternAndMethodMatches); - producibleMediaTypes = getProducibleMediaTypes(exchange, patternAndMethodMatches); - paramConditions = getRequestParams(exchange, patternAndMethodMatches); - } - - if (!consumableMediaTypes.isEmpty()) { + if (helper.hasConsumesMismatch()) { + Set mediaTypes = helper.getConsumableMediaTypes(); MediaType contentType; try { contentType = request.getHeaders().getContentType(); @@ -267,62 +234,176 @@ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMe catch (InvalidMediaTypeException ex) { throw new UnsupportedMediaTypeStatusException(ex.getMessage()); } - throw new UnsupportedMediaTypeStatusException(contentType, new ArrayList<>(consumableMediaTypes)); + throw new UnsupportedMediaTypeStatusException(contentType, new ArrayList<>(mediaTypes)); } - else if (!producibleMediaTypes.isEmpty()) { - throw new NotAcceptableStatusException(new ArrayList<>(producibleMediaTypes)); + + if (helper.hasProducesMismatch()) { + Set mediaTypes = helper.getProducibleMediaTypes(); + throw new NotAcceptableStatusException(new ArrayList<>(mediaTypes)); } - else { - if (!CollectionUtils.isEmpty(paramConditions)) { - Map params = request.getQueryParams().entrySet().stream() - .collect(Collectors.toMap(Entry::getKey, - entry -> entry.getValue().toArray(new String[entry.getValue().size()])) - ); - throw new ServerWebInputException("Unsatisfied query parameter conditions: " + - paramConditions + ", actual parameters: " + params); + + if (helper.hasParamsMismatch()) { + throw new ServerWebInputException( + "Unsatisfied query parameter conditions: " + helper.getParamConditions() + + ", actual parameters: " + request.getQueryParams()); + } + + return null; + } + + + /** + * Aggregate all partial matches and expose methods checking across them. + */ + private static class PartialMatchHelper { + + private final List partialMatches = new ArrayList<>(); + + + public PartialMatchHelper(Set infos, ServerWebExchange exchange) { + this.partialMatches.addAll(infos.stream(). + filter(info -> info.getPatternsCondition().getMatchingCondition(exchange) != null). + map(info -> new PartialMatch(info, exchange)). + collect(Collectors.toList())); + } + + + /** + * Whether there any partial matches. + */ + public boolean isEmpty() { + return this.partialMatches.isEmpty(); + } + + /** + * Any partial matches for "methods"? + */ + public boolean hasMethodsMismatch() { + return !this.partialMatches.stream(). + filter(PartialMatch::hasMethodsMatch).findAny().isPresent(); + } + + /** + * Any partial matches for "methods" and "consumes"? + */ + public boolean hasConsumesMismatch() { + return !this.partialMatches.stream(). + filter(PartialMatch::hasConsumesMatch).findAny().isPresent(); + } + + /** + * Any partial matches for "methods", "consumes", and "produces"? + */ + public boolean hasProducesMismatch() { + return !this.partialMatches.stream(). + filter(PartialMatch::hasProducesMatch).findAny().isPresent(); + } + + /** + * Any partial matches for "methods", "consumes", "produces", and "params"? + */ + public boolean hasParamsMismatch() { + return !this.partialMatches.stream(). + filter(PartialMatch::hasParamsMatch).findAny().isPresent(); + } + + /** + * Return declared HTTP methods. + */ + public Set getAllowedMethods() { + return this.partialMatches.stream(). + flatMap(m -> m.getInfo().getMethodsCondition().getMethods().stream()). + map(Enum::name). + collect(Collectors.toCollection(LinkedHashSet::new)); + } + + /** + * Return declared "consumable" types but only among those that also + * match the "methods" condition. + */ + public Set getConsumableMediaTypes() { + return this.partialMatches.stream().filter(PartialMatch::hasMethodsMatch). + flatMap(m -> m.getInfo().getConsumesCondition().getConsumableMediaTypes().stream()). + collect(Collectors.toCollection(LinkedHashSet::new)); + } + + /** + * Return declared "producible" types but only among those that also + * match the "methods" and "consumes" conditions. + */ + public Set getProducibleMediaTypes() { + return this.partialMatches.stream().filter(PartialMatch::hasConsumesMatch). + flatMap(m -> m.getInfo().getProducesCondition().getProducibleMediaTypes().stream()). + collect(Collectors.toCollection(LinkedHashSet::new)); + } + + /** + * Return declared "params" conditions but only among those that also + * match the "methods", "consumes", and "params" conditions. + */ + public List>> getParamConditions() { + return this.partialMatches.stream().filter(PartialMatch::hasProducesMatch). + map(match -> match.getInfo().getParamsCondition().getExpressions()). + collect(Collectors.toList()); + } + + + /** + * Container for a RequestMappingInfo that matches the URL path at least. + */ + private static class PartialMatch { + + private final RequestMappingInfo info; + + private final boolean methodsMatch; + + private final boolean consumesMatch; + + private final boolean producesMatch; + + private final boolean paramsMatch; + + + /** + * @param info RequestMappingInfo that matches the URL path + * @param exchange the current exchange + */ + public PartialMatch(RequestMappingInfo info, ServerWebExchange exchange) { + this.info = info; + this.methodsMatch = info.getMethodsCondition().getMatchingCondition(exchange) != null; + this.consumesMatch = info.getConsumesCondition().getMatchingCondition(exchange) != null; + this.producesMatch = info.getProducesCondition().getMatchingCondition(exchange) != null; + this.paramsMatch = info.getParamsCondition().getMatchingCondition(exchange) != null; } - else { - return null; + + + public RequestMappingInfo getInfo() { + return this.info; + } + + public boolean hasMethodsMatch() { + return this.methodsMatch; + } + + public boolean hasConsumesMatch() { + return hasMethodsMatch() && this.consumesMatch; + } + + public boolean hasProducesMatch() { + return hasConsumesMatch() && this.producesMatch; + } + + public boolean hasParamsMatch() { + return hasProducesMatch() && this.paramsMatch; + } + + @Override + public String toString() { + return this.info.toString(); } } } - private Set getConsumableMediaTypes(ServerWebExchange exchange, - Set partialMatches) { - - Set result = new HashSet<>(); - for (RequestMappingInfo partialMatch : partialMatches) { - if (partialMatch.getConsumesCondition().getMatchingCondition(exchange) == null) { - result.addAll(partialMatch.getConsumesCondition().getConsumableMediaTypes()); - } - } - return result; - } - - private Set getProducibleMediaTypes(ServerWebExchange exchange, - Set partialMatches) { - - Set result = new HashSet<>(); - for (RequestMappingInfo partialMatch : partialMatches) { - if (partialMatch.getProducesCondition().getMatchingCondition(exchange) == null) { - result.addAll(partialMatch.getProducesCondition().getProducibleMediaTypes()); - } - } - return result; - } - - private List> getRequestParams(ServerWebExchange exchange, - Set partialMatches) { - - return partialMatches.stream() - .map(RequestMappingInfo::getParamsCondition) - .filter(condition -> condition.getMatchingCondition(exchange) == null) - .map(ParamsRequestCondition::getExpressions) - .map(expressions -> expressions.stream().map(Object::toString).collect(Collectors.toList())) - .collect(Collectors.toList()); - } - - /** * Default handler for HTTP OPTIONS. */ diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java index 1fe69c50fb..00042ec69f 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java @@ -32,6 +32,7 @@ import org.junit.Test; import reactor.core.publisher.Mono; import reactor.core.test.TestSubscriber; +import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -43,6 +44,8 @@ import org.springframework.stereotype.Controller; import org.springframework.ui.ExtendedModelMap; import org.springframework.ui.ModelMap; import org.springframework.util.MultiValueMap; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @@ -50,18 +53,20 @@ import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerMapping; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.result.method.RequestMappingInfo.BuilderConfiguration; -import org.springframework.web.server.ServerWebInputException; import org.springframework.web.server.MethodNotAllowedException; import org.springframework.web.server.NotAcceptableStatusException; import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.ServerWebInputException; import org.springframework.web.server.UnsupportedMediaTypeStatusException; import org.springframework.web.server.adapter.DefaultServerWebExchange; import org.springframework.web.server.session.WebSessionManager; import org.springframework.web.util.HttpRequestPathHelper; +import static org.hamcrest.CoreMatchers.containsString; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; @@ -147,41 +152,28 @@ public class RequestMappingInfoHandlerMappingTests { public void getHandlerRequestMethodNotAllowed() throws Exception { ServerWebExchange exchange = createExchange(HttpMethod.POST, "/bar"); Mono mono = this.handlerMapping.getHandler(exchange); + assertError(mono, MethodNotAllowedException.class, ex -> assertEquals(new HashSet<>(Arrays.asList("GET", "HEAD")), ex.getSupportedMethods())); } - // SPR-9603 - - @Test + @Test // SPR-9603 public void getHandlerRequestMethodMatchFalsePositive() throws Exception { ServerWebExchange exchange = createExchange(HttpMethod.GET, "/users"); exchange.getRequest().getHeaders().setAccept(Collections.singletonList(MediaType.APPLICATION_XML)); this.handlerMapping.registerHandler(new UserController()); Mono mono = this.handlerMapping.getHandler(exchange); - TestSubscriber - .subscribe(mono) - .assertError(NotAcceptableStatusException.class); + TestSubscriber.subscribe(mono).assertError(NotAcceptableStatusException.class); } - // SPR-8462 - - @Test + @Test // SPR-8462 public void getHandlerMediaTypeNotSupported() throws Exception { testHttpMediaTypeNotSupportedException("/person/1"); testHttpMediaTypeNotSupportedException("/person/1/"); testHttpMediaTypeNotSupportedException("/person/1.json"); } - @Test - public void getHandlerHttpOptions() throws Exception { - testHttpOptions("/foo", "GET,HEAD"); - testHttpOptions("/person/1", "PUT"); - testHttpOptions("/persons", "GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS"); - testHttpOptions("/something", "PUT,POST"); - } - @Test public void getHandlerTestInvalidContentType() throws Exception { ServerWebExchange exchange = createExchange(HttpMethod.PUT, "/person/1"); @@ -196,24 +188,32 @@ public class RequestMappingInfoHandlerMappingTests { // SPR-8462 @Test - public void getHandlerMediaTypeNotAccepted() throws Exception { - testHttpMediaTypeNotAcceptableException("/persons"); - testHttpMediaTypeNotAcceptableException("/persons/"); - testHttpMediaTypeNotAcceptableException("/persons.json"); + public void getHandlerTestMediaTypeNotAcceptable() throws Exception { + testMediaTypeNotAcceptable("/persons"); + testMediaTypeNotAcceptable("/persons/"); + testMediaTypeNotAcceptable("/persons.json"); } // SPR-12854 @Test - public void getHandlerUnsatisfiedServletRequestParameterException() throws Exception { + public void getHandlerTestRequestParamMismatch() throws Exception { ServerWebExchange exchange = createExchange(HttpMethod.GET, "/params"); Mono mono = this.handlerMapping.getHandler(exchange); assertError(mono, ServerWebInputException.class, ex -> { - assertEquals(ex.getReason(), "Unsatisfied query parameter conditions: " + - "[[bar=baz], [foo=bar]], actual parameters: {}"); + assertThat(ex.getReason(), containsString("[foo=bar]")); + assertThat(ex.getReason(), containsString("[bar=baz]")); }); } + @Test + public void getHandlerHttpOptions() throws Exception { + testHttpOptions("/foo", "GET,HEAD"); + testHttpOptions("/person/1", "PUT"); + testHttpOptions("/persons", "GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS"); + testHttpOptions("/something", "PUT,POST"); + } + @Test public void getHandlerProducibleMediaTypesAttribute() throws Exception { ServerWebExchange exchange = createExchange(HttpMethod.GET, "/content"); @@ -398,7 +398,7 @@ public class RequestMappingInfoHandlerMappingTests { assertEquals(allowHeader, ((HttpHeaders) value.get()).getFirst("Allow")); } - private void testHttpMediaTypeNotAcceptableException(String url) throws Exception { + private void testMediaTypeNotAcceptable(String url) throws Exception { ServerWebExchange exchange = createExchange(HttpMethod.GET, url); exchange.getRequest().getHeaders().setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); Mono mono = this.handlerMapping.getHandler(exchange); @@ -431,15 +431,15 @@ public class RequestMappingInfoHandlerMappingTests { @Controller private static class TestController { - @RequestMapping(value = "/foo", method = RequestMethod.GET) + @GetMapping("/foo") public void foo() { } - @RequestMapping(value = "/foo", method = RequestMethod.GET, params="p") + @GetMapping(path = "/foo", params="p") public void fooParam() { } - @RequestMapping(value = "/ba*", method = { RequestMethod.GET, RequestMethod.HEAD }) + @RequestMapping(path = "/ba*", method = { RequestMethod.GET, RequestMethod.HEAD }) public void bar() { } @@ -447,7 +447,7 @@ public class RequestMappingInfoHandlerMappingTests { public void empty() { } - @RequestMapping(value = "/person/{id}", method = RequestMethod.PUT, consumes="application/xml") + @PutMapping(path = "/person/{id}", consumes="application/xml") public void consumes(@RequestBody String text) { } @@ -488,11 +488,11 @@ public class RequestMappingInfoHandlerMappingTests { @Controller private static class UserController { - @RequestMapping(value = "/users", method = RequestMethod.GET, produces = "application/json") + @GetMapping(path = "/users", produces = "application/json") public void getUser() { } - @RequestMapping(value = "/users", method = RequestMethod.PUT) + @PutMapping(path = "/users") public void saveUser() { } } @@ -510,16 +510,16 @@ public class RequestMappingInfoHandlerMappingTests { @Override protected RequestMappingInfo getMappingForMethod(Method method, Class handlerType) { - RequestMapping annotation = AnnotationUtils.findAnnotation(method, RequestMapping.class); - if (annotation != null) { + RequestMapping annot = AnnotatedElementUtils.findMergedAnnotation(method, RequestMapping.class); + if (annot != null) { BuilderConfiguration options = new BuilderConfiguration(); options.setPathHelper(getPathHelper()); options.setPathMatcher(getPathMatcher()); options.setSuffixPatternMatch(true); options.setTrailingSlashMatch(true); - return RequestMappingInfo.paths(annotation.value()).methods(annotation.method()) - .params(annotation.params()).headers(annotation.headers()) - .consumes(annotation.consumes()).produces(annotation.produces()) + return RequestMappingInfo.paths(annot.value()).methods(annot.method()) + .params(annot.params()).headers(annot.headers()) + .consumes(annot.consumes()).produces(annot.produces()) .options(options).build(); } else { From e545b20289df650499aeb8f3a30317b5d678cfa5 Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Thu, 30 Jun 2016 11:18:13 +0300 Subject: [PATCH 436/498] Make AbstractResponseBodySubscriber.onWritePossible thread-safe When there are simultaneous invocations of onWritePossible, only the first one should succeed. This can happens when AbstractResponseBodySubscriber.onNext and WriteListener.onWritePossible() are called respectively by the application and the web container. --- .../AbstractResponseBodySubscriber.java | 66 ++++++++++++------- 1 file changed, 42 insertions(+), 24 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractResponseBodySubscriber.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractResponseBodySubscriber.java index 8158d525f7..e1b6457453 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractResponseBodySubscriber.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractResponseBodySubscriber.java @@ -152,10 +152,13 @@ abstract class AbstractResponseBodySubscriber implements Subscriber * UNSUBSCRIBED * | * v - * REQUESTED <---> RECEIVED - * | | - * v v - * COMPLETED + * REQUESTED -------------------> RECEIVED + * ^ ^ + * | | + * --------- WRITING <----- + * | + * v + * COMPLETED * * Refer to the individual states for more information. */ @@ -206,36 +209,41 @@ abstract class AbstractResponseBodySubscriber implements Subscriber /** * State that gets entered after a buffer has been * {@linkplain Subscriber#onNext(Object) received}. Responds to - * {@code onWritePossible} by writing the current buffer, and if it can be - * written completely, changes state to either {@link #REQUESTED} if the - * subscription has not been completed; or {@link #COMPLETED} if it has. + * {@code onWritePossible} by writing the current buffer and changes + * the state to {@link #WRITING}. If it can be written completely, + * changes the state to either {@link #REQUESTED} if the subscription + * has not been completed; or {@link #COMPLETED} if it has. If it cannot + * be written completely the state will be changed to {@link #RECEIVED}. */ RECEIVED { @Override void onWritePossible(AbstractResponseBodySubscriber subscriber) { - DataBuffer dataBuffer = subscriber.currentBuffer; - try { - boolean writeCompleted = subscriber.write(dataBuffer); - if (writeCompleted) { - if (dataBuffer instanceof FlushingDataBuffer) { - subscriber.flush(); - } - subscriber.releaseBuffer(); - boolean subscriptionCompleted = subscriber.subscriptionCompleted; - if (!subscriptionCompleted) { - if (subscriber.changeState(this, REQUESTED)) { + if (subscriber.changeState(this, WRITING)) { + DataBuffer dataBuffer = subscriber.currentBuffer; + try { + boolean writeCompleted = subscriber.write(dataBuffer); + if (writeCompleted) { + if (dataBuffer instanceof FlushingDataBuffer) { + subscriber.flush(); + } + subscriber.releaseBuffer(); + boolean subscriptionCompleted = subscriber.subscriptionCompleted; + if (!subscriptionCompleted) { + subscriber.changeState(WRITING, REQUESTED); subscriber.subscription.request(1); } - } - else { - if (subscriber.changeState(this, COMPLETED)) { + else { + subscriber.changeState(WRITING, COMPLETED); subscriber.close(); } } + else { + subscriber.changeState(WRITING, RECEIVED); + } + } + catch (IOException ex) { + subscriber.onError(ex); } - } - catch (IOException ex) { - subscriber.onError(ex); } } @@ -244,6 +252,16 @@ abstract class AbstractResponseBodySubscriber implements Subscriber subscriber.subscriptionCompleted = true; } }, + /** + * State that gets entered after a writing of the current buffer has been + * {@code onWritePossible started}. + */ + WRITING { + @Override + void onComplete(AbstractResponseBodySubscriber subscriber) { + subscriber.subscriptionCompleted = true; + } + }, /** * The terminal completed state. Does not respond to any events. */ From 1d48e7c5b9a45b3c8a37437b259c4a894d07bd07 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Thu, 30 Jun 2016 15:26:17 +0200 Subject: [PATCH 437/498] Allow to set response status on Undertow Refactored Undertow support to register a response listener only when the body is written to, as opposed to registering it at startup. The reason for this is that getting the response channel from the HttpServerExchange commits the status and response, making it impossible to change them after the fact. Fixed issue #119. --- .../reactive/UndertowHttpHandlerAdapter.java | 206 +----------------- .../reactive/UndertowServerHttpRequest.java | 90 +++++++- .../reactive/UndertowServerHttpResponse.java | 133 +++++++++-- .../RequestMappingIntegrationTests.java | 5 +- 4 files changed, 209 insertions(+), 225 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java index dfc9d5837b..a856a7a101 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java @@ -16,29 +16,19 @@ package org.springframework.http.server.reactive; -import java.io.IOException; -import java.nio.ByteBuffer; - -import io.undertow.connector.PooledByteBuffer; import io.undertow.server.HttpServerExchange; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; -import org.xnio.ChannelListener; -import org.xnio.ChannelListeners; -import org.xnio.IoUtils; -import org.xnio.channels.StreamSinkChannel; -import org.xnio.channels.StreamSourceChannel; -import reactor.core.publisher.Mono; -import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.util.Assert; /** * @author Marek Hawrylczak * @author Rossen Stoyanchev + * @author Arjen Poutsma */ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandler { @@ -60,20 +50,11 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle @Override public void handleRequest(HttpServerExchange exchange) throws Exception { - RequestBodyPublisher requestBody = - new RequestBodyPublisher(exchange, this.dataBufferFactory); - requestBody.registerListener(); - ServerHttpRequest request = new UndertowServerHttpRequest(exchange, requestBody); + ServerHttpRequest request = + new UndertowServerHttpRequest(exchange, this.dataBufferFactory); - StreamSinkChannel responseChannel = exchange.getResponseChannel(); - ResponseBodySubscriber responseBody = - new ResponseBodySubscriber(exchange, responseChannel); - responseBody.registerListener(); ServerHttpResponse response = - new UndertowServerHttpResponse(exchange, responseChannel, - publisher -> Mono - .from(subscriber -> publisher.subscribe(responseBody)), - this.dataBufferFactory); + new UndertowServerHttpResponse(exchange, this.dataBufferFactory); this.delegate.handle(request, response).subscribe(new Subscriber() { @@ -106,183 +87,4 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle }); } - private static class RequestBodyPublisher extends AbstractRequestBodyPublisher { - - private final ChannelListener readListener = - new ReadListener(); - - private final ChannelListener closeListener = - new CloseListener(); - - private final StreamSourceChannel requestChannel; - - private final DataBufferFactory dataBufferFactory; - - private final PooledByteBuffer pooledByteBuffer; - - public RequestBodyPublisher(HttpServerExchange exchange, - DataBufferFactory dataBufferFactory) { - this.requestChannel = exchange.getRequestChannel(); - this.pooledByteBuffer = - exchange.getConnection().getByteBufferPool().allocate(); - this.dataBufferFactory = dataBufferFactory; - } - - public void registerListener() { - this.requestChannel.getReadSetter().set(this.readListener); - this.requestChannel.getCloseSetter().set(this.closeListener); - this.requestChannel.resumeReads(); - } - - @Override - protected DataBuffer read() throws IOException { - ByteBuffer byteBuffer = this.pooledByteBuffer.getBuffer(); - int read = this.requestChannel.read(byteBuffer); - if (logger.isTraceEnabled()) { - logger.trace("read:" + read); - } - - if (read > 0) { - byteBuffer.flip(); - return this.dataBufferFactory.wrap(byteBuffer); - } - else if (read == -1) { - onAllDataRead(); - } - return null; - } - - @Override - protected void close() { - if (this.pooledByteBuffer != null) { - IoUtils.safeClose(this.pooledByteBuffer); - } - if (this.requestChannel != null) { - IoUtils.safeClose(this.requestChannel); - } - } - - private class ReadListener implements ChannelListener { - - @Override - public void handleEvent(StreamSourceChannel channel) { - onDataAvailable(); - } - } - - private class CloseListener implements ChannelListener { - - @Override - public void handleEvent(StreamSourceChannel channel) { - onAllDataRead(); - } - } - } - - private static class ResponseBodySubscriber extends AbstractResponseBodySubscriber { - - private final ChannelListener listener = - new ResponseBodyListener(); - - private final HttpServerExchange exchange; - - private final StreamSinkChannel responseChannel; - - private volatile ByteBuffer byteBuffer; - - public ResponseBodySubscriber(HttpServerExchange exchange, - StreamSinkChannel responseChannel) { - this.exchange = exchange; - this.responseChannel = responseChannel; - } - - public void registerListener() { - this.responseChannel.getWriteSetter().set(this.listener); - this.responseChannel.resumeWrites(); - } - - @Override - protected void writeError(Throwable t) { - if (!this.exchange.isResponseStarted() && - this.exchange.getStatusCode() < 500) { - this.exchange.setStatusCode(500); - } - } - - @Override - protected void flush() throws IOException { - if (logger.isTraceEnabled()) { - logger.trace("flush"); - } - this.responseChannel.flush(); - } - - @Override - protected boolean write(DataBuffer dataBuffer) throws IOException { - if (this.byteBuffer == null) { - return false; - } - if (logger.isTraceEnabled()) { - logger.trace("write: " + dataBuffer); - } - int total = this.byteBuffer.remaining(); - int written = writeByteBuffer(this.byteBuffer); - - if (logger.isTraceEnabled()) { - logger.trace("written: " + written + " total: " + total); - } - return written == total; - } - - private int writeByteBuffer(ByteBuffer byteBuffer) throws IOException { - int written; - int totalWritten = 0; - do { - written = this.responseChannel.write(byteBuffer); - totalWritten += written; - } - while (byteBuffer.hasRemaining() && written > 0); - return totalWritten; - } - - @Override - protected void receiveBuffer(DataBuffer dataBuffer) { - super.receiveBuffer(dataBuffer); - this.byteBuffer = dataBuffer.asByteBuffer(); - } - - @Override - protected void releaseBuffer() { - super.releaseBuffer(); - this.byteBuffer = null; - } - - @Override - protected void close() { - try { - this.responseChannel.shutdownWrites(); - - if (!this.responseChannel.flush()) { - this.responseChannel.getWriteSetter().set(ChannelListeners - .flushingChannelListener( - o -> IoUtils.safeClose(this.responseChannel), - ChannelListeners.closingChannelExceptionHandler())); - this.responseChannel.resumeWrites(); - } - } - catch (IOException ignored) { - } - } - - private class ResponseBodyListener implements ChannelListener { - - @Override - public void handleEvent(StreamSinkChannel channel) { - onWritePossible(); - } - - } - - } - } \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java index 51ef11d445..19f56d596d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java @@ -16,16 +16,22 @@ package org.springframework.http.server.reactive; +import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; +import java.nio.ByteBuffer; +import io.undertow.connector.PooledByteBuffer; import io.undertow.server.HttpServerExchange; import io.undertow.server.handlers.Cookie; import io.undertow.util.HeaderValues; -import org.reactivestreams.Publisher; +import org.xnio.ChannelListener; +import org.xnio.IoUtils; +import org.xnio.channels.StreamSourceChannel; import reactor.core.publisher.Flux; import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -43,14 +49,14 @@ public class UndertowServerHttpRequest extends AbstractServerHttpRequest { private final HttpServerExchange exchange; - private final Flux body; + private final RequestBodyPublisher body; public UndertowServerHttpRequest(HttpServerExchange exchange, - Publisher body) { + DataBufferFactory dataBufferFactory) { Assert.notNull(exchange, "'exchange' is required."); - Assert.notNull(exchange, "'body' is required."); this.exchange = exchange; - this.body = Flux.from(body); + this.body = new RequestBodyPublisher(exchange, dataBufferFactory); + this.body.registerListener(); } @@ -92,7 +98,79 @@ public class UndertowServerHttpRequest extends AbstractServerHttpRequest { @Override public Flux getBody() { - return this.body; + return Flux.from(this.body); } + private static class RequestBodyPublisher extends AbstractRequestBodyPublisher { + + private final ChannelListener readListener = + new ReadListener(); + + private final ChannelListener closeListener = + new CloseListener(); + + private final StreamSourceChannel requestChannel; + + private final DataBufferFactory dataBufferFactory; + + private final PooledByteBuffer pooledByteBuffer; + + public RequestBodyPublisher(HttpServerExchange exchange, + DataBufferFactory dataBufferFactory) { + this.requestChannel = exchange.getRequestChannel(); + this.pooledByteBuffer = + exchange.getConnection().getByteBufferPool().allocate(); + this.dataBufferFactory = dataBufferFactory; + } + + private void registerListener() { + this.requestChannel.getReadSetter().set(this.readListener); + this.requestChannel.getCloseSetter().set(this.closeListener); + this.requestChannel.resumeReads(); + } + + @Override + protected DataBuffer read() throws IOException { + ByteBuffer byteBuffer = this.pooledByteBuffer.getBuffer(); + int read = this.requestChannel.read(byteBuffer); + if (logger.isTraceEnabled()) { + logger.trace("read:" + read); + } + + if (read > 0) { + byteBuffer.flip(); + return this.dataBufferFactory.wrap(byteBuffer); + } + else if (read == -1) { + onAllDataRead(); + } + return null; + } + + @Override + protected void close() { + if (this.pooledByteBuffer != null) { + IoUtils.safeClose(this.pooledByteBuffer); + } + if (this.requestChannel != null) { + IoUtils.safeClose(this.requestChannel); + } + } + + private class ReadListener implements ChannelListener { + + @Override + public void handleEvent(StreamSourceChannel channel) { + onDataAvailable(); + } + } + + private class CloseListener implements ChannelListener { + + @Override + public void handleEvent(StreamSourceChannel channel) { + onAllDataRead(); + } + } + } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java index 7ce3a5d89a..0dfb54f26b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java @@ -19,16 +19,19 @@ package org.springframework.http.server.reactive; import java.io.File; import java.io.FileInputStream; import java.io.IOException; +import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.util.List; import java.util.Map; -import java.util.function.Function; import io.undertow.server.HttpServerExchange; import io.undertow.server.handlers.Cookie; import io.undertow.server.handlers.CookieImpl; import io.undertow.util.HttpString; import org.reactivestreams.Publisher; +import org.xnio.ChannelListener; +import org.xnio.ChannelListeners; +import org.xnio.IoUtils; import org.xnio.channels.StreamSinkChannel; import reactor.core.publisher.Mono; @@ -44,27 +47,18 @@ import org.springframework.util.Assert; * * @author Marek Hawrylczak * @author Rossen Stoyanchev + * @author Arjen Poutsma */ public class UndertowServerHttpResponse extends AbstractServerHttpResponse implements ZeroCopyHttpOutputMessage { private final HttpServerExchange exchange; - private final StreamSinkChannel responseChannel; - - private final Function, Mono> responseBodyWriter; - public UndertowServerHttpResponse(HttpServerExchange exchange, - StreamSinkChannel responseChannel, - Function, Mono> responseBodyWriter, DataBufferFactory dataBufferFactory) { super(dataBufferFactory); Assert.notNull(exchange, "'exchange' is required."); - Assert.notNull(responseChannel, "'responseChannel' must not be null"); - Assert.notNull(responseBodyWriter, "'responseBodyWriter' must not be null"); this.exchange = exchange; - this.responseChannel = responseChannel; - this.responseBodyWriter = responseBodyWriter; } @@ -80,16 +74,26 @@ public class UndertowServerHttpResponse extends AbstractServerHttpResponse @Override protected Mono writeWithInternal(Publisher publisher) { - return this.responseBodyWriter.apply(publisher); + return Mono.from(s -> { + // lazily create Subscriber, since calling + // {@link HttpServerExchange#getResponseChannel} as done in the + // ResponseBodySubscriber constructor commits the response status and headers + ResponseBodySubscriber subscriber = new ResponseBodySubscriber(this.exchange); + subscriber.registerListener(); + publisher.subscribe(subscriber); + }); } @Override public Mono writeWith(File file, long position, long count) { writeHeaders(); writeCookies(); + try { + StreamSinkChannel responseChannel = + getUndertowExchange().getResponseChannel(); FileChannel in = new FileInputStream(file).getChannel(); - long result = this.responseChannel.transferFrom(in, position, count); + long result = responseChannel.transferFrom(in, position, count); if (result < count) { return Mono.error(new IOException("Could only write " + result + " out of " + count + " bytes")); @@ -128,4 +132,107 @@ public class UndertowServerHttpResponse extends AbstractServerHttpResponse } } + private static class ResponseBodySubscriber extends AbstractResponseBodySubscriber { + + private final ChannelListener listener = new WriteListener(); + + private final HttpServerExchange exchange; + + private final StreamSinkChannel responseChannel; + + private volatile ByteBuffer byteBuffer; + + public ResponseBodySubscriber(HttpServerExchange exchange) { + this.exchange = exchange; + this.responseChannel = exchange.getResponseChannel(); + } + + public void registerListener() { + this.responseChannel.getWriteSetter().set(this.listener); + this.responseChannel.resumeWrites(); + } + + @Override + protected void writeError(Throwable t) { + if (!this.exchange.isResponseStarted() && + this.exchange.getStatusCode() < 500) { + this.exchange.setStatusCode(500); + } + } + + @Override + protected void flush() throws IOException { + if (logger.isTraceEnabled()) { + logger.trace("flush"); + } + this.responseChannel.flush(); + } + + @Override + protected boolean write(DataBuffer dataBuffer) throws IOException { + if (this.byteBuffer == null) { + return false; + } + if (logger.isTraceEnabled()) { + logger.trace("write: " + dataBuffer); + } + int total = this.byteBuffer.remaining(); + int written = writeByteBuffer(this.byteBuffer); + + if (logger.isTraceEnabled()) { + logger.trace("written: " + written + " total: " + total); + } + return written == total; + } + + private int writeByteBuffer(ByteBuffer byteBuffer) throws IOException { + int written; + int totalWritten = 0; + do { + written = this.responseChannel.write(byteBuffer); + totalWritten += written; + } + while (byteBuffer.hasRemaining() && written > 0); + return totalWritten; + } + + @Override + protected void receiveBuffer(DataBuffer dataBuffer) { + super.receiveBuffer(dataBuffer); + this.byteBuffer = dataBuffer.asByteBuffer(); + } + + @Override + protected void releaseBuffer() { + super.releaseBuffer(); + this.byteBuffer = null; + } + + @Override + protected void close() { + try { + this.responseChannel.shutdownWrites(); + + if (!this.responseChannel.flush()) { + this.responseChannel.getWriteSetter().set(ChannelListeners + .flushingChannelListener( + o -> IoUtils.safeClose(this.responseChannel), + ChannelListeners.closingChannelExceptionHandler())); + this.responseChannel.resumeWrites(); + } + } + catch (IOException ignored) { + } + } + + private class WriteListener implements ChannelListener { + + @Override + public void handleEvent(StreamSinkChannel channel) { + onWritePossible(); + } + + } + + } } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java index 5e5338a33f..b04b0204ef 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java @@ -67,9 +67,7 @@ import org.springframework.web.reactive.config.WebReactiveConfiguration; import org.springframework.web.reactive.result.view.freemarker.FreeMarkerConfigurer; import org.springframework.web.server.adapter.WebHttpHandlerBuilder; -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; /** @@ -173,7 +171,6 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati } @Test - @Ignore // Issue #119 public void serializeAsMonoResponseEntity() throws Exception { serializeAsPojo("http://localhost:" + port + "/monoResponseEntity"); } From ea18e7380353d8458757e034de5301de0f7678b3 Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Thu, 30 Jun 2016 14:23:18 +0300 Subject: [PATCH 438/498] AbstractRequestBodyPublisher.onDataAvailable improvement When in state DATA_AVAILABLE if there are simultaneous invocations of AbstractRequestBodyPublisher.RequestBodySubscription.request and ReadListener.onDataAvailable, the first one will process the available data, the second one should not throw an exception because thus it will signal to web container that there are problems while there are not. --- .../http/server/reactive/AbstractRequestBodyPublisher.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractRequestBodyPublisher.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractRequestBodyPublisher.java index 5be914a6cb..412ecdf8db 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractRequestBodyPublisher.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractRequestBodyPublisher.java @@ -251,6 +251,11 @@ abstract class AbstractRequestBodyPublisher implements Publisher { } } } + + @Override + void onDataAvailable(AbstractRequestBodyPublisher publisher) { + // ignore + } }, /** * The terminal completed state. Does not respond to any events. From 8d654584a86dbca6a1f348769c8287f5f84c981c Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Thu, 30 Jun 2016 16:30:16 +0200 Subject: [PATCH 439/498] Make JsonObjectDecoder package private --- .../springframework/core/codec/support/JsonObjectDecoder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectDecoder.java index e45dc66c34..15773d7aaf 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectDecoder.java @@ -45,7 +45,7 @@ import org.springframework.util.MimeType; * * @author Sebastien Deleuze */ -public class JsonObjectDecoder extends AbstractDecoder { +class JsonObjectDecoder extends AbstractDecoder { private static final int ST_CORRUPTED = -1; From b115f1885d431c5b470b1f469055b70ae08f3af2 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Thu, 30 Jun 2016 16:59:08 +0200 Subject: [PATCH 440/498] Move JSON and XML codecs to org.springframework.http.codec --- .../support => http/codec/json}/JacksonJsonDecoder.java | 3 ++- .../support => http/codec/json}/JacksonJsonEncoder.java | 3 ++- .../support => http/codec/json}/JsonObjectDecoder.java | 3 ++- .../codec/support => http/codec/xml}/Jaxb2Decoder.java | 3 ++- .../codec/support => http/codec/xml}/Jaxb2Encoder.java | 3 ++- .../support => http/codec/xml}/JaxbContextContainer.java | 2 +- .../codec/support => http/codec/xml}/XmlEventDecoder.java | 3 ++- .../springframework/web/client/reactive/WebClient.java | 4 ++-- .../web/reactive/config/WebReactiveConfiguration.java | 8 ++++---- .../core/codec/support/StringDecoderTests.java | 2 +- .../{core/codec/support => http/codec}/Pojo.java | 2 +- .../support => http/codec}/SseEventEncoderTests.java | 4 ++-- .../codec/json}/JacksonJsonDecoderTests.java | 4 +++- .../codec/json}/JacksonJsonEncoderTests.java | 4 +++- .../codec/json}/JsonObjectDecoderTests.java | 2 +- .../support => http/codec/xml}/Jaxb2DecoderTests.java | 5 ++++- .../support => http/codec/xml}/Jaxb2EncoderTests.java | 3 ++- .../support => http/codec/xml}/XmlEventDecoderTests.java | 3 ++- .../web/client/reactive/WebClientIntegrationTests.java | 2 +- .../web/reactive/config/ViewResolverRegistryTests.java | 2 +- .../reactive/config/WebReactiveConfigurationTests.java | 6 +++--- .../annotation/MessageConverterResultHandlerTests.java | 4 ++-- .../annotation/RequestBodyArgumentResolverTests.java | 2 +- .../method/annotation/RequestMappingIntegrationTests.java | 2 +- .../method/annotation/ResponseBodyResultHandlerTests.java | 4 ++-- .../annotation/ResponseEntityResultHandlerTests.java | 4 ++-- .../result/method/annotation/SseIntegrationTests.java | 4 ++-- .../result/view/HttpMessageConverterViewTests.java | 4 ++-- 28 files changed, 55 insertions(+), 40 deletions(-) rename spring-web-reactive/src/main/java/org/springframework/{core/codec/support => http/codec/json}/JacksonJsonDecoder.java (96%) rename spring-web-reactive/src/main/java/org/springframework/{core/codec/support => http/codec/json}/JacksonJsonEncoder.java (97%) rename spring-web-reactive/src/main/java/org/springframework/{core/codec/support => http/codec/json}/JsonObjectDecoder.java (98%) rename spring-web-reactive/src/main/java/org/springframework/{core/codec/support => http/codec/xml}/Jaxb2Decoder.java (98%) rename spring-web-reactive/src/main/java/org/springframework/{core/codec/support => http/codec/xml}/Jaxb2Encoder.java (95%) rename spring-web-reactive/src/main/java/org/springframework/{core/codec/support => http/codec/xml}/JaxbContextContainer.java (97%) rename spring-web-reactive/src/main/java/org/springframework/{core/codec/support => http/codec/xml}/XmlEventDecoder.java (97%) rename spring-web-reactive/src/test/java/org/springframework/{core/codec/support => http/codec}/Pojo.java (96%) rename spring-web-reactive/src/test/java/org/springframework/{core/codec/support => http/codec}/SseEventEncoderTests.java (98%) rename spring-web-reactive/src/test/java/org/springframework/{core/codec/support => http/codec/json}/JacksonJsonDecoderTests.java (95%) rename spring-web-reactive/src/test/java/org/springframework/{core/codec/support => http/codec/json}/JacksonJsonEncoderTests.java (95%) rename spring-web-reactive/src/test/java/org/springframework/{core/codec/support => http/codec/json}/JsonObjectDecoderTests.java (98%) rename spring-web-reactive/src/test/java/org/springframework/{core/codec/support => http/codec/xml}/Jaxb2DecoderTests.java (97%) rename spring-web-reactive/src/test/java/org/springframework/{core/codec/support => http/codec/xml}/Jaxb2EncoderTests.java (97%) rename spring-web-reactive/src/test/java/org/springframework/{core/codec/support => http/codec/xml}/XmlEventDecoderTests.java (96%) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonDecoder.java b/spring-web-reactive/src/main/java/org/springframework/http/codec/json/JacksonJsonDecoder.java similarity index 96% rename from spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonDecoder.java rename to spring-web-reactive/src/main/java/org/springframework/http/codec/json/JacksonJsonDecoder.java index 13d2d7aea2..e5d51005ef 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/codec/json/JacksonJsonDecoder.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.core.codec.support; +package org.springframework.http.codec.json; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -29,6 +29,7 @@ import reactor.core.publisher.Flux; import org.springframework.core.ResolvableType; import org.springframework.core.codec.CodecException; import org.springframework.core.codec.Decoder; +import org.springframework.core.codec.support.AbstractDecoder; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.support.DataBufferUtils; import org.springframework.util.Assert; diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonEncoder.java b/spring-web-reactive/src/main/java/org/springframework/http/codec/json/JacksonJsonEncoder.java similarity index 97% rename from spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonEncoder.java rename to spring-web-reactive/src/main/java/org/springframework/http/codec/json/JacksonJsonEncoder.java index 89bdaa9cb7..69cabc1c0e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JacksonJsonEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/codec/json/JacksonJsonEncoder.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.core.codec.support; +package org.springframework.http.codec.json; import java.io.IOException; import java.io.OutputStream; @@ -31,6 +31,7 @@ import reactor.core.publisher.Mono; import org.springframework.core.ResolvableType; import org.springframework.core.codec.CodecException; +import org.springframework.core.codec.support.AbstractEncoder; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.util.Assert; diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectDecoder.java b/spring-web-reactive/src/main/java/org/springframework/http/codec/json/JsonObjectDecoder.java similarity index 98% rename from spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectDecoder.java rename to spring-web-reactive/src/main/java/org/springframework/http/codec/json/JsonObjectDecoder.java index 15773d7aaf..82b298e847 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JsonObjectDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/codec/json/JsonObjectDecoder.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.core.codec.support; +package org.springframework.http.codec.json; import java.nio.charset.StandardCharsets; import java.util.ArrayList; @@ -28,6 +28,7 @@ import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import org.springframework.core.ResolvableType; +import org.springframework.core.codec.support.AbstractDecoder; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.support.DataBufferUtils; diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Decoder.java b/spring-web-reactive/src/main/java/org/springframework/http/codec/xml/Jaxb2Decoder.java similarity index 98% rename from spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Decoder.java rename to spring-web-reactive/src/main/java/org/springframework/http/codec/xml/Jaxb2Decoder.java index 0bf14b8c4b..4054f3b2e1 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Decoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/codec/xml/Jaxb2Decoder.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.core.codec.support; +package org.springframework.http.codec.xml; import java.util.ArrayList; import java.util.List; @@ -36,6 +36,7 @@ import reactor.core.publisher.Mono; import org.springframework.core.ResolvableType; import org.springframework.core.codec.CodecException; +import org.springframework.core.codec.support.AbstractDecoder; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.util.ClassUtils; import org.springframework.util.MimeType; diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Encoder.java b/spring-web-reactive/src/main/java/org/springframework/http/codec/xml/Jaxb2Encoder.java similarity index 95% rename from spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Encoder.java rename to spring-web-reactive/src/main/java/org/springframework/http/codec/xml/Jaxb2Encoder.java index e99c44d1d2..b7eb53f895 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Encoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/codec/xml/Jaxb2Encoder.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.core.codec.support; +package org.springframework.http.codec.xml; import java.io.OutputStream; import java.nio.charset.StandardCharsets; @@ -26,6 +26,7 @@ import javax.xml.bind.annotation.XmlType; import reactor.core.publisher.Flux; import org.springframework.core.ResolvableType; +import org.springframework.core.codec.support.AbstractSingleValueEncoder; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.util.ClassUtils; diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JaxbContextContainer.java b/spring-web-reactive/src/main/java/org/springframework/http/codec/xml/JaxbContextContainer.java similarity index 97% rename from spring-web-reactive/src/main/java/org/springframework/core/codec/support/JaxbContextContainer.java rename to spring-web-reactive/src/main/java/org/springframework/http/codec/xml/JaxbContextContainer.java index 46d38ff5ae..87ec8a4380 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JaxbContextContainer.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/codec/xml/JaxbContextContainer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.core.codec.support; +package org.springframework.http.codec.xml; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/XmlEventDecoder.java b/spring-web-reactive/src/main/java/org/springframework/http/codec/xml/XmlEventDecoder.java similarity index 97% rename from spring-web-reactive/src/main/java/org/springframework/core/codec/support/XmlEventDecoder.java rename to spring-web-reactive/src/main/java/org/springframework/http/codec/xml/XmlEventDecoder.java index a5b0be0bdb..7bac139387 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/XmlEventDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/codec/xml/XmlEventDecoder.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.core.codec.support; +package org.springframework.http.codec.xml; import java.io.InputStream; import java.util.ArrayList; @@ -35,6 +35,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.core.ResolvableType; +import org.springframework.core.codec.support.AbstractDecoder; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.support.DataBufferUtils; import org.springframework.util.ClassUtils; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebClient.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebClient.java index f38f7b02f8..af50d86d77 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebClient.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebClient.java @@ -26,8 +26,8 @@ import org.springframework.core.codec.Decoder; import org.springframework.core.codec.Encoder; import org.springframework.core.codec.support.ByteBufferDecoder; import org.springframework.core.codec.support.ByteBufferEncoder; -import org.springframework.core.codec.support.JacksonJsonDecoder; -import org.springframework.core.codec.support.JacksonJsonEncoder; +import org.springframework.http.codec.json.JacksonJsonDecoder; +import org.springframework.http.codec.json.JacksonJsonEncoder; import org.springframework.core.codec.support.StringDecoder; import org.springframework.core.codec.support.StringEncoder; import org.springframework.http.HttpStatus; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java index f49056c4eb..a7b1011722 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java @@ -32,10 +32,10 @@ import org.springframework.core.codec.Decoder; import org.springframework.core.codec.Encoder; import org.springframework.core.codec.support.ByteBufferDecoder; import org.springframework.core.codec.support.ByteBufferEncoder; -import org.springframework.core.codec.support.JacksonJsonDecoder; -import org.springframework.core.codec.support.JacksonJsonEncoder; -import org.springframework.core.codec.support.Jaxb2Decoder; -import org.springframework.core.codec.support.Jaxb2Encoder; +import org.springframework.http.codec.json.JacksonJsonDecoder; +import org.springframework.http.codec.json.JacksonJsonEncoder; +import org.springframework.http.codec.xml.Jaxb2Decoder; +import org.springframework.http.codec.xml.Jaxb2Encoder; import org.springframework.core.codec.support.StringDecoder; import org.springframework.core.codec.support.StringEncoder; import org.springframework.core.convert.converter.Converter; diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringDecoderTests.java index f89be25a22..62187a69aa 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringDecoderTests.java @@ -54,7 +54,7 @@ public class StringDecoderTests extends AbstractDataBufferAllocatingTestCase { MediaType.APPLICATION_JSON)); assertFalse(this.decoder .canDecode(ResolvableType.forClass(Integer.class), MediaType.TEXT_PLAIN)); - assertFalse(this.decoder.canDecode(ResolvableType.forClass(Pojo.class), + assertFalse(this.decoder.canDecode(ResolvableType.forClass(Object.class), MediaType.APPLICATION_JSON)); } diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Pojo.java b/spring-web-reactive/src/test/java/org/springframework/http/codec/Pojo.java similarity index 96% rename from spring-web-reactive/src/test/java/org/springframework/core/codec/support/Pojo.java rename to spring-web-reactive/src/test/java/org/springframework/http/codec/Pojo.java index 1b7e792953..ff4cdc76c4 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Pojo.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/codec/Pojo.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.core.codec.support; +package org.springframework.http.codec; import javax.xml.bind.annotation.XmlRootElement; diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/SseEventEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/http/codec/SseEventEncoderTests.java similarity index 98% rename from spring-web-reactive/src/test/java/org/springframework/core/codec/support/SseEventEncoderTests.java rename to spring-web-reactive/src/test/java/org/springframework/http/codec/SseEventEncoderTests.java index 6f64ecbbc5..352f8d4799 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/SseEventEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/codec/SseEventEncoderTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.core.codec.support; +package org.springframework.http.codec; import java.util.Arrays; @@ -28,7 +28,7 @@ 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.FlushingDataBuffer; -import org.springframework.http.codec.SseEventEncoder; +import org.springframework.http.codec.json.JacksonJsonEncoder; import org.springframework.util.MimeType; import org.springframework.web.reactive.sse.SseEvent; diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/http/codec/json/JacksonJsonDecoderTests.java similarity index 95% rename from spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonDecoderTests.java rename to spring-web-reactive/src/test/java/org/springframework/http/codec/json/JacksonJsonDecoderTests.java index 6554bc32fc..54f99e66e7 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/codec/json/JacksonJsonDecoderTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.core.codec.support; +package org.springframework.http.codec.json; import java.lang.reflect.Method; import java.util.Arrays; @@ -29,6 +29,8 @@ import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.AbstractDataBufferAllocatingTestCase; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.MediaType; +import org.springframework.http.codec.Pojo; +import org.springframework.http.codec.json.JacksonJsonDecoder; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/http/codec/json/JacksonJsonEncoderTests.java similarity index 95% rename from spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonEncoderTests.java rename to spring-web-reactive/src/test/java/org/springframework/http/codec/json/JacksonJsonEncoderTests.java index 95af3fe7e9..cc7bf4d4bd 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JacksonJsonEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/codec/json/JacksonJsonEncoderTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.core.codec.support; +package org.springframework.http.codec.json; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; @@ -27,6 +27,8 @@ import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.AbstractDataBufferAllocatingTestCase; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.MediaType; +import org.springframework.http.codec.Pojo; +import org.springframework.http.codec.json.JacksonJsonEncoder; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JsonObjectDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/http/codec/json/JsonObjectDecoderTests.java similarity index 98% rename from spring-web-reactive/src/test/java/org/springframework/core/codec/support/JsonObjectDecoderTests.java rename to spring-web-reactive/src/test/java/org/springframework/http/codec/json/JsonObjectDecoderTests.java index 21eddc7afe..6770899ce1 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/JsonObjectDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/codec/json/JsonObjectDecoderTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.core.codec.support; +package org.springframework.http.codec.json; import java.nio.charset.StandardCharsets; diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2DecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/Jaxb2DecoderTests.java similarity index 97% rename from spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2DecoderTests.java rename to spring-web-reactive/src/test/java/org/springframework/http/codec/xml/Jaxb2DecoderTests.java index afe3194fa9..3b36689720 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2DecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/Jaxb2DecoderTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.core.codec.support; +package org.springframework.http.codec.xml; import java.util.List; import javax.xml.namespace.QName; @@ -34,6 +34,9 @@ import org.springframework.core.codec.support.jaxb.XmlTypeWithNameAndNamespace; import org.springframework.core.io.buffer.AbstractDataBufferAllocatingTestCase; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.MediaType; +import org.springframework.http.codec.Pojo; +import org.springframework.http.codec.xml.Jaxb2Decoder; +import org.springframework.http.codec.xml.XmlEventDecoder; import static org.junit.Assert.*; diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2EncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/Jaxb2EncoderTests.java similarity index 97% rename from spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2EncoderTests.java rename to spring-web-reactive/src/test/java/org/springframework/http/codec/xml/Jaxb2EncoderTests.java index dddeee0759..4476cfcaf7 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2EncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/Jaxb2EncoderTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.core.codec.support; +package org.springframework.http.codec.xml; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -31,6 +31,7 @@ import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.support.DataBufferTestUtils; import org.springframework.core.io.buffer.support.DataBufferUtils; import org.springframework.http.MediaType; +import org.springframework.http.codec.Pojo; import static org.custommonkey.xmlunit.XMLAssert.assertXMLEqual; import static org.custommonkey.xmlunit.XMLAssert.fail; diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/XmlEventDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/XmlEventDecoderTests.java similarity index 96% rename from spring-web-reactive/src/test/java/org/springframework/core/codec/support/XmlEventDecoderTests.java rename to spring-web-reactive/src/test/java/org/springframework/http/codec/xml/XmlEventDecoderTests.java index 430355d200..fa942c9b94 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/XmlEventDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/XmlEventDecoderTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.core.codec.support; +package org.springframework.http.codec.xml; import javax.xml.stream.events.XMLEvent; @@ -23,6 +23,7 @@ import reactor.core.publisher.Flux; import reactor.core.test.TestSubscriber; import org.springframework.core.io.buffer.AbstractDataBufferAllocatingTestCase; +import org.springframework.http.codec.xml.XmlEventDecoder; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/client/reactive/WebClientIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/client/reactive/WebClientIntegrationTests.java index 0006c773d2..f038ab1cb5 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/client/reactive/WebClientIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/client/reactive/WebClientIntegrationTests.java @@ -34,7 +34,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.test.TestSubscriber; -import org.springframework.core.codec.support.Pojo; +import org.springframework.http.codec.Pojo; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/ViewResolverRegistryTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/ViewResolverRegistryTests.java index 6e46b829b5..c5035730f8 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/ViewResolverRegistryTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/ViewResolverRegistryTests.java @@ -19,7 +19,7 @@ import org.junit.Before; import org.junit.Test; import org.springframework.core.Ordered; -import org.springframework.core.codec.support.JacksonJsonEncoder; +import org.springframework.http.codec.json.JacksonJsonEncoder; import org.springframework.web.context.support.StaticWebApplicationContext; import org.springframework.web.reactive.result.view.HttpMessageConverterView; import org.springframework.web.reactive.result.view.UrlBasedViewResolver; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/WebReactiveConfigurationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/WebReactiveConfigurationTests.java index 962bd32c85..0bbeb262c7 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/WebReactiveConfigurationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/WebReactiveConfigurationTests.java @@ -35,9 +35,9 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.core.ResolvableType; -import org.springframework.core.codec.support.JacksonJsonEncoder; -import org.springframework.core.codec.support.Jaxb2Decoder; -import org.springframework.core.codec.support.Jaxb2Encoder; +import org.springframework.http.codec.json.JacksonJsonEncoder; +import org.springframework.http.codec.xml.Jaxb2Decoder; +import org.springframework.http.codec.xml.Jaxb2Encoder; import org.springframework.core.codec.support.StringDecoder; import org.springframework.core.codec.support.StringEncoder; import org.springframework.core.convert.ConversionService; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterResultHandlerTests.java index 295ecc4cf7..2a21fe27fc 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterResultHandlerTests.java @@ -38,8 +38,8 @@ import rx.Observable; import org.springframework.core.ResolvableType; import org.springframework.core.codec.support.ByteBufferEncoder; -import org.springframework.core.codec.support.JacksonJsonEncoder; -import org.springframework.core.codec.support.Jaxb2Encoder; +import org.springframework.http.codec.json.JacksonJsonEncoder; +import org.springframework.http.codec.xml.Jaxb2Encoder; import org.springframework.core.codec.support.StringEncoder; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java index 6f46067fab..142d9ed3de 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java @@ -43,7 +43,7 @@ import org.springframework.core.MethodParameter; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.annotation.SynthesizingMethodParameter; import org.springframework.core.codec.Decoder; -import org.springframework.core.codec.support.JacksonJsonDecoder; +import org.springframework.http.codec.json.JacksonJsonDecoder; import org.springframework.core.codec.support.StringDecoder; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java index b04b0204ef..826efa4bbd 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java @@ -39,7 +39,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ResolvableType; -import org.springframework.core.codec.support.JacksonJsonEncoder; +import org.springframework.http.codec.json.JacksonJsonEncoder; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.core.io.buffer.DataBuffer; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java index 64a576c487..793fc1216f 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java @@ -27,8 +27,8 @@ import reactor.core.publisher.Mono; import org.springframework.core.ResolvableType; import org.springframework.core.codec.support.ByteBufferEncoder; -import org.springframework.core.codec.support.JacksonJsonEncoder; -import org.springframework.core.codec.support.Jaxb2Encoder; +import org.springframework.http.codec.json.JacksonJsonEncoder; +import org.springframework.http.codec.xml.Jaxb2Encoder; import org.springframework.core.codec.support.StringEncoder; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.convert.support.GenericConversionService; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java index 1bebf0fb2d..edca26d48a 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java @@ -31,8 +31,8 @@ import rx.Single; import org.springframework.core.ResolvableType; import org.springframework.core.codec.support.ByteBufferEncoder; -import org.springframework.core.codec.support.JacksonJsonEncoder; -import org.springframework.core.codec.support.Jaxb2Encoder; +import org.springframework.http.codec.json.JacksonJsonEncoder; +import org.springframework.http.codec.xml.Jaxb2Encoder; import org.springframework.core.codec.support.StringEncoder; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java index 673728f3f7..74bc385ca8 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java @@ -31,8 +31,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.codec.Encoder; import org.springframework.core.codec.support.ByteBufferDecoder; -import org.springframework.core.codec.support.JacksonJsonDecoder; -import org.springframework.core.codec.support.JacksonJsonEncoder; +import org.springframework.http.codec.json.JacksonJsonDecoder; +import org.springframework.http.codec.json.JacksonJsonEncoder; import org.springframework.core.codec.support.StringDecoder; import org.springframework.http.MediaType; import org.springframework.http.client.reactive.ReactorHttpClientRequestFactory; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/HttpMessageConverterViewTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/HttpMessageConverterViewTests.java index 126a7bcdc2..a6a24ac4f9 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/HttpMessageConverterViewTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/HttpMessageConverterViewTests.java @@ -32,8 +32,8 @@ import org.junit.Test; import reactor.core.test.TestSubscriber; import org.springframework.core.ResolvableType; -import org.springframework.core.codec.support.JacksonJsonEncoder; -import org.springframework.core.codec.support.Jaxb2Encoder; +import org.springframework.http.codec.json.JacksonJsonEncoder; +import org.springframework.http.codec.xml.Jaxb2Encoder; import org.springframework.core.codec.support.StringEncoder; import org.springframework.core.io.buffer.support.DataBufferTestUtils; import org.springframework.http.HttpMethod; From 6d089cdc08e54091224cf2ec61ca4c81a6c40ca2 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 30 Jun 2016 13:48:41 -0400 Subject: [PATCH 441/498] Move remaining JAXB2 test files from core to http --- .../codec/support/CharSequenceEncoder.java | 51 +++++++++++++++++++ .../http/codec/xml/Jaxb2DecoderTests.java | 12 ++--- .../http/codec/xml/XmlEventDecoderTests.java | 1 - .../codec/xml}/XmlRootElement.java | 2 +- .../codec/xml}/XmlRootElementWithName.java | 2 +- .../XmlRootElementWithNameAndNamespace.java | 2 +- .../jaxb => http/codec/xml}/XmlType.java | 2 +- .../codec/xml}/XmlTypeWithName.java | 2 +- .../xml}/XmlTypeWithNameAndNamespace.java | 2 +- .../jaxb => http/codec/xml}/package-info.java | 2 +- 10 files changed, 61 insertions(+), 17 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/core/codec/support/CharSequenceEncoder.java rename spring-web-reactive/src/test/java/org/springframework/{core/codec/support/jaxb => http/codec/xml}/XmlRootElement.java (93%) rename spring-web-reactive/src/test/java/org/springframework/{core/codec/support/jaxb => http/codec/xml}/XmlRootElementWithName.java (93%) rename spring-web-reactive/src/test/java/org/springframework/{core/codec/support/jaxb => http/codec/xml}/XmlRootElementWithNameAndNamespace.java (93%) rename spring-web-reactive/src/test/java/org/springframework/{core/codec/support/jaxb => http/codec/xml}/XmlType.java (93%) rename spring-web-reactive/src/test/java/org/springframework/{core/codec/support/jaxb => http/codec/xml}/XmlTypeWithName.java (93%) rename spring-web-reactive/src/test/java/org/springframework/{core/codec/support/jaxb => http/codec/xml}/XmlTypeWithNameAndNamespace.java (93%) rename spring-web-reactive/src/test/java/org/springframework/{core/codec/support/jaxb => http/codec/xml}/package-info.java (53%) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/CharSequenceEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/CharSequenceEncoder.java new file mode 100644 index 0000000000..372645cca6 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/CharSequenceEncoder.java @@ -0,0 +1,51 @@ +/* + * 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.support; + +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; + +/** + * + * @author Rossen Stoyanchev + */ +public class CharSequenceEncoder extends AbstractEncoder { + + private final StringEncoder stringEncoder; + + + public CharSequenceEncoder(StringEncoder encoder) { + super(encoder.getEncodableMimeTypes().toArray(new MimeType[encoder.getEncodableMimeTypes().size()])); + this.stringEncoder = encoder; + } + + + @Override + public Flux encode(Publisher inputStream, + DataBufferFactory bufferFactory, ResolvableType elementType, + MimeType mimeType, Object... hints) { + + return this.stringEncoder.encode( + Flux.from(inputStream).map(CharSequence::toString), + bufferFactory, elementType, mimeType, hints); + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/Jaxb2DecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/Jaxb2DecoderTests.java index 3b36689720..40c19a6840 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/Jaxb2DecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/Jaxb2DecoderTests.java @@ -25,20 +25,14 @@ import reactor.core.publisher.Flux; import reactor.core.test.TestSubscriber; import org.springframework.core.ResolvableType; -import org.springframework.core.codec.support.jaxb.XmlRootElement; -import org.springframework.core.codec.support.jaxb.XmlRootElementWithName; -import org.springframework.core.codec.support.jaxb.XmlRootElementWithNameAndNamespace; -import org.springframework.core.codec.support.jaxb.XmlType; -import org.springframework.core.codec.support.jaxb.XmlTypeWithName; -import org.springframework.core.codec.support.jaxb.XmlTypeWithNameAndNamespace; import org.springframework.core.io.buffer.AbstractDataBufferAllocatingTestCase; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.MediaType; import org.springframework.http.codec.Pojo; -import org.springframework.http.codec.xml.Jaxb2Decoder; -import org.springframework.http.codec.xml.XmlEventDecoder; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; /** * @author Sebastien Deleuze diff --git a/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/XmlEventDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/XmlEventDecoderTests.java index fa942c9b94..09a82eb9ac 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/XmlEventDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/XmlEventDecoderTests.java @@ -23,7 +23,6 @@ import reactor.core.publisher.Flux; import reactor.core.test.TestSubscriber; import org.springframework.core.io.buffer.AbstractDataBufferAllocatingTestCase; -import org.springframework.http.codec.xml.XmlEventDecoder; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlRootElement.java b/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/XmlRootElement.java similarity index 93% rename from spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlRootElement.java rename to spring-web-reactive/src/test/java/org/springframework/http/codec/xml/XmlRootElement.java index 92470a5876..497ade650f 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlRootElement.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/XmlRootElement.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.core.codec.support.jaxb; +package org.springframework.http.codec.xml; /** * @author Arjen Poutsma diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlRootElementWithName.java b/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/XmlRootElementWithName.java similarity index 93% rename from spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlRootElementWithName.java rename to spring-web-reactive/src/test/java/org/springframework/http/codec/xml/XmlRootElementWithName.java index deb7929916..ac8dca6501 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlRootElementWithName.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/XmlRootElementWithName.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.core.codec.support.jaxb; +package org.springframework.http.codec.xml; import javax.xml.bind.annotation.XmlRootElement; diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlRootElementWithNameAndNamespace.java b/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/XmlRootElementWithNameAndNamespace.java similarity index 93% rename from spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlRootElementWithNameAndNamespace.java rename to spring-web-reactive/src/test/java/org/springframework/http/codec/xml/XmlRootElementWithNameAndNamespace.java index e4330da2bc..2dc4afd0b1 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlRootElementWithNameAndNamespace.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/XmlRootElementWithNameAndNamespace.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.core.codec.support.jaxb; +package org.springframework.http.codec.xml; import javax.xml.bind.annotation.XmlRootElement; diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlType.java b/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/XmlType.java similarity index 93% rename from spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlType.java rename to spring-web-reactive/src/test/java/org/springframework/http/codec/xml/XmlType.java index 49d158674c..f9b73eae39 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlType.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/XmlType.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.core.codec.support.jaxb; +package org.springframework.http.codec.xml; /** * @author Arjen Poutsma diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlTypeWithName.java b/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/XmlTypeWithName.java similarity index 93% rename from spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlTypeWithName.java rename to spring-web-reactive/src/test/java/org/springframework/http/codec/xml/XmlTypeWithName.java index f62be41835..0efa8dff31 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlTypeWithName.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/XmlTypeWithName.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.core.codec.support.jaxb; +package org.springframework.http.codec.xml; import javax.xml.bind.annotation.XmlType; diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlTypeWithNameAndNamespace.java b/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/XmlTypeWithNameAndNamespace.java similarity index 93% rename from spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlTypeWithNameAndNamespace.java rename to spring-web-reactive/src/test/java/org/springframework/http/codec/xml/XmlTypeWithNameAndNamespace.java index 4cf7cb6f6b..c1ad65bde7 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlTypeWithNameAndNamespace.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/XmlTypeWithNameAndNamespace.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.core.codec.support.jaxb; +package org.springframework.http.codec.xml; import javax.xml.bind.annotation.XmlType; diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/package-info.java b/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/package-info.java similarity index 53% rename from spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/package-info.java rename to spring-web-reactive/src/test/java/org/springframework/http/codec/xml/package-info.java index e02f2405c1..2f5da29e85 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/package-info.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/package-info.java @@ -1,2 +1,2 @@ @javax.xml.bind.annotation.XmlSchema(namespace = "namespace") -package org.springframework.core.codec.support.jaxb; +package org.springframework.http.codec.xml; From 39253b314af2c1d896a901987c57ee468ef37a6e Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 30 Jun 2016 14:09:09 -0400 Subject: [PATCH 442/498] Fix tests by moving JAXB2 types back into sub-package --- .../springframework/http/codec/xml/Jaxb2DecoderTests.java | 6 ++++++ .../http/codec/xml/XmlEventDecoderTests.java | 1 + .../http/codec/xml/{ => jaxb}/XmlRootElement.java | 2 +- .../http/codec/xml/{ => jaxb}/XmlRootElementWithName.java | 2 +- .../xml/{ => jaxb}/XmlRootElementWithNameAndNamespace.java | 2 +- .../springframework/http/codec/xml/{ => jaxb}/XmlType.java | 2 +- .../http/codec/xml/{ => jaxb}/XmlTypeWithName.java | 2 +- .../codec/xml/{ => jaxb}/XmlTypeWithNameAndNamespace.java | 2 +- .../http/codec/xml/{ => jaxb}/package-info.java | 2 +- 9 files changed, 14 insertions(+), 7 deletions(-) rename spring-web-reactive/src/test/java/org/springframework/http/codec/xml/{ => jaxb}/XmlRootElement.java (93%) rename spring-web-reactive/src/test/java/org/springframework/http/codec/xml/{ => jaxb}/XmlRootElementWithName.java (94%) rename spring-web-reactive/src/test/java/org/springframework/http/codec/xml/{ => jaxb}/XmlRootElementWithNameAndNamespace.java (94%) rename spring-web-reactive/src/test/java/org/springframework/http/codec/xml/{ => jaxb}/XmlType.java (93%) rename spring-web-reactive/src/test/java/org/springframework/http/codec/xml/{ => jaxb}/XmlTypeWithName.java (93%) rename spring-web-reactive/src/test/java/org/springframework/http/codec/xml/{ => jaxb}/XmlTypeWithNameAndNamespace.java (94%) rename spring-web-reactive/src/test/java/org/springframework/http/codec/xml/{ => jaxb}/package-info.java (55%) diff --git a/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/Jaxb2DecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/Jaxb2DecoderTests.java index 40c19a6840..5cfbe1945d 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/Jaxb2DecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/Jaxb2DecoderTests.java @@ -29,6 +29,12 @@ import org.springframework.core.io.buffer.AbstractDataBufferAllocatingTestCase; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.MediaType; import org.springframework.http.codec.Pojo; +import org.springframework.http.codec.xml.jaxb.XmlRootElement; +import org.springframework.http.codec.xml.jaxb.XmlRootElementWithName; +import org.springframework.http.codec.xml.jaxb.XmlRootElementWithNameAndNamespace; +import org.springframework.http.codec.xml.jaxb.XmlType; +import org.springframework.http.codec.xml.jaxb.XmlTypeWithName; +import org.springframework.http.codec.xml.jaxb.XmlTypeWithNameAndNamespace; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; diff --git a/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/XmlEventDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/XmlEventDecoderTests.java index 09a82eb9ac..fa942c9b94 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/XmlEventDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/XmlEventDecoderTests.java @@ -23,6 +23,7 @@ import reactor.core.publisher.Flux; import reactor.core.test.TestSubscriber; import org.springframework.core.io.buffer.AbstractDataBufferAllocatingTestCase; +import org.springframework.http.codec.xml.XmlEventDecoder; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; diff --git a/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/XmlRootElement.java b/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/jaxb/XmlRootElement.java similarity index 93% rename from spring-web-reactive/src/test/java/org/springframework/http/codec/xml/XmlRootElement.java rename to spring-web-reactive/src/test/java/org/springframework/http/codec/xml/jaxb/XmlRootElement.java index 497ade650f..746d218c77 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/XmlRootElement.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/jaxb/XmlRootElement.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.http.codec.xml; +package org.springframework.http.codec.xml.jaxb; /** * @author Arjen Poutsma diff --git a/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/XmlRootElementWithName.java b/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/jaxb/XmlRootElementWithName.java similarity index 94% rename from spring-web-reactive/src/test/java/org/springframework/http/codec/xml/XmlRootElementWithName.java rename to spring-web-reactive/src/test/java/org/springframework/http/codec/xml/jaxb/XmlRootElementWithName.java index ac8dca6501..6509be8997 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/XmlRootElementWithName.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/jaxb/XmlRootElementWithName.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.http.codec.xml; +package org.springframework.http.codec.xml.jaxb; import javax.xml.bind.annotation.XmlRootElement; diff --git a/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/XmlRootElementWithNameAndNamespace.java b/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/jaxb/XmlRootElementWithNameAndNamespace.java similarity index 94% rename from spring-web-reactive/src/test/java/org/springframework/http/codec/xml/XmlRootElementWithNameAndNamespace.java rename to spring-web-reactive/src/test/java/org/springframework/http/codec/xml/jaxb/XmlRootElementWithNameAndNamespace.java index 2dc4afd0b1..0a702e2f01 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/XmlRootElementWithNameAndNamespace.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/jaxb/XmlRootElementWithNameAndNamespace.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.http.codec.xml; +package org.springframework.http.codec.xml.jaxb; import javax.xml.bind.annotation.XmlRootElement; diff --git a/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/XmlType.java b/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/jaxb/XmlType.java similarity index 93% rename from spring-web-reactive/src/test/java/org/springframework/http/codec/xml/XmlType.java rename to spring-web-reactive/src/test/java/org/springframework/http/codec/xml/jaxb/XmlType.java index f9b73eae39..747d99e5c1 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/XmlType.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/jaxb/XmlType.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.http.codec.xml; +package org.springframework.http.codec.xml.jaxb; /** * @author Arjen Poutsma diff --git a/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/XmlTypeWithName.java b/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/jaxb/XmlTypeWithName.java similarity index 93% rename from spring-web-reactive/src/test/java/org/springframework/http/codec/xml/XmlTypeWithName.java rename to spring-web-reactive/src/test/java/org/springframework/http/codec/xml/jaxb/XmlTypeWithName.java index 0efa8dff31..11b5f251f1 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/XmlTypeWithName.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/jaxb/XmlTypeWithName.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.http.codec.xml; +package org.springframework.http.codec.xml.jaxb; import javax.xml.bind.annotation.XmlType; diff --git a/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/XmlTypeWithNameAndNamespace.java b/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/jaxb/XmlTypeWithNameAndNamespace.java similarity index 94% rename from spring-web-reactive/src/test/java/org/springframework/http/codec/xml/XmlTypeWithNameAndNamespace.java rename to spring-web-reactive/src/test/java/org/springframework/http/codec/xml/jaxb/XmlTypeWithNameAndNamespace.java index c1ad65bde7..3396397d66 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/XmlTypeWithNameAndNamespace.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/jaxb/XmlTypeWithNameAndNamespace.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.http.codec.xml; +package org.springframework.http.codec.xml.jaxb; import javax.xml.bind.annotation.XmlType; diff --git a/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/package-info.java b/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/jaxb/package-info.java similarity index 55% rename from spring-web-reactive/src/test/java/org/springframework/http/codec/xml/package-info.java rename to spring-web-reactive/src/test/java/org/springframework/http/codec/xml/jaxb/package-info.java index 2f5da29e85..f3b3a59496 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/package-info.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/jaxb/package-info.java @@ -1,2 +1,2 @@ @javax.xml.bind.annotation.XmlSchema(namespace = "namespace") -package org.springframework.http.codec.xml; +package org.springframework.http.codec.xml.jaxb; From 43a1ea6bac64daf96f35aaf57896d96af36fd75d Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 30 Jun 2016 14:10:15 -0400 Subject: [PATCH 443/498] Remove class checked in by mistake --- .../codec/support/CharSequenceEncoder.java | 51 ------------------- 1 file changed, 51 deletions(-) delete mode 100644 spring-web-reactive/src/main/java/org/springframework/core/codec/support/CharSequenceEncoder.java diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/CharSequenceEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/CharSequenceEncoder.java deleted file mode 100644 index 372645cca6..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/CharSequenceEncoder.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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.support; - -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; - -/** - * - * @author Rossen Stoyanchev - */ -public class CharSequenceEncoder extends AbstractEncoder { - - private final StringEncoder stringEncoder; - - - public CharSequenceEncoder(StringEncoder encoder) { - super(encoder.getEncodableMimeTypes().toArray(new MimeType[encoder.getEncodableMimeTypes().size()])); - this.stringEncoder = encoder; - } - - - @Override - public Flux encode(Publisher inputStream, - DataBufferFactory bufferFactory, ResolvableType elementType, - MimeType mimeType, Object... hints) { - - return this.stringEncoder.encode( - Flux.from(inputStream).map(CharSequence::toString), - bufferFactory, elementType, mimeType, hints); - } - -} From 4e2802338acb29107e7ba28085b8d16b4cf68c2c Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 30 Jun 2016 15:53:58 -0400 Subject: [PATCH 444/498] Add MockWebSessionManager --- .../reactive/MockServerHttpRequest.java | 1 + .../reactive/MockServerHttpResponse.java | 1 + .../reactive/DispatcherHandlerErrorTests.java | 4 +- .../ResponseStatusExceptionHandlerTests.java | 9 ++-- ...positeContentTypeResolverBuilderTests.java | 4 +- .../HeaderContentTypeResolverTests.java | 4 +- ...PathExtensionContentTypeResolverTests.java | 4 +- .../config/WebReactiveConfigurationTests.java | 17 ++++--- .../handler/SimpleUrlHandlerMappingTests.java | 4 +- ...tNegotiatingResultHandlerSupportTests.java | 5 +- .../CompositeRequestConditionTests.java | 4 +- .../ConsumesRequestConditionTests.java | 4 +- .../HeadersRequestConditionTests.java | 4 +- .../ParamsRequestConditionTests.java | 4 +- .../PatternsRequestConditionTests.java | 4 +- .../ProducesRequestConditionTests.java | 4 +- .../RequestConditionHolderTests.java | 4 +- .../condition/RequestMappingInfoTests.java | 6 +-- .../RequestMethodsRequestConditionTests.java | 4 +- .../method/HandlerMethodMappingTests.java | 4 +- .../method/InvocableHandlerMethodTests.java | 3 +- ...RequestMappingInfoHandlerMappingTests.java | 4 +- ...ookieValueMethodArgumentResolverTests.java | 4 +- ...ssionValueMethodArgumentResolverTests.java | 4 +- .../MessageConverterResultHandlerTests.java | 9 ++-- ...ariableMapMethodArgumentResolverTests.java | 4 +- ...thVariableMethodArgumentResolverTests.java | 4 +- ...tAttributeMethodArgumentResolverTests.java | 4 +- ...tHeaderMapMethodArgumentResolverTests.java | 4 +- ...uestHeaderMethodArgumentResolverTests.java | 4 +- ...stParamMapMethodArgumentResolverTests.java | 4 +- ...questParamMethodArgumentResolverTests.java | 4 +- .../ResponseBodyResultHandlerTests.java | 9 ++-- .../ResponseEntityResultHandlerTests.java | 9 ++-- ...nAttributeMethodArgumentResolverTests.java | 10 ++-- .../ExceptionHandlingHttpHandlerTests.java | 6 +-- .../server/session/MockWebSessionManager.java | 51 +++++++++++++++++++ 37 files changed, 144 insertions(+), 88 deletions(-) create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/server/session/MockWebSessionManager.java diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpRequest.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpRequest.java index 805734aced..9448f2e9e2 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpRequest.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpRequest.java @@ -29,6 +29,7 @@ import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; /** + * Mock implementation of {@link ServerHttpRequest}. * @author Rossen Stoyanchev */ public class MockServerHttpRequest implements ServerHttpRequest { diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java index 10292ad6f7..30dbda1681 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java @@ -32,6 +32,7 @@ import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; /** + * Mock implementation of {@link ServerHttpResponse}. * @author Rossen Stoyanchev */ public class MockServerHttpResponse implements ServerHttpResponse { diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java index 8a28d1ce30..993c992882 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java @@ -58,13 +58,13 @@ import org.springframework.web.server.WebHandler; import org.springframework.web.server.adapter.DefaultServerWebExchange; import org.springframework.web.server.handler.ExceptionHandlingWebHandler; import org.springframework.web.server.handler.FilteringWebHandler; +import org.springframework.web.server.session.MockWebSessionManager; import org.springframework.web.server.session.WebSessionManager; import static org.hamcrest.CoreMatchers.startsWith; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertThat; -import static org.mockito.Mockito.mock; /** * Test the effect of exceptions at different stages of request processing by @@ -96,7 +96,7 @@ public class DispatcherHandlerErrorTests { this.dispatcherHandler = new DispatcherHandler(); this.dispatcherHandler.setApplicationContext(appContext); - WebSessionManager sessionManager = mock(WebSessionManager.class); + WebSessionManager sessionManager = new MockWebSessionManager(); this.request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); this.response = new MockServerHttpResponse(); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java index 5929587ec9..d916a81c96 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java @@ -29,10 +29,13 @@ import org.springframework.http.server.reactive.MockServerHttpResponse; import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.MockWebSessionManager; import org.springframework.web.server.session.WebSessionManager; -import static org.junit.Assert.*; -import static org.mockito.Mockito.mock; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; /** * Unit tests for {@link ResponseStatusExceptionHandler}. @@ -52,7 +55,7 @@ public class ResponseStatusExceptionHandlerTests { public void setUp() throws Exception { this.handler = new ResponseStatusExceptionHandler(); MockServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/path")); - WebSessionManager sessionManager = mock(WebSessionManager.class); + WebSessionManager sessionManager = new MockWebSessionManager(); this.response = new MockServerHttpResponse(); this.exchange = new DefaultServerWebExchange(request, this.response, sessionManager); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/accept/CompositeContentTypeResolverBuilderTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/accept/CompositeContentTypeResolverBuilderTests.java index 9575082149..74fd266380 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/accept/CompositeContentTypeResolverBuilderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/accept/CompositeContentTypeResolverBuilderTests.java @@ -29,10 +29,10 @@ import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.web.server.NotAcceptableStatusException; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.MockWebSessionManager; import org.springframework.web.server.session.WebSessionManager; import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.mock; /** * Unit tests for {@link RequestedContentTypeResolverBuilder}. @@ -189,7 +189,7 @@ public class CompositeContentTypeResolverBuilderTests { private ServerWebExchange createExchange(String path) throws URISyntaxException { ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI(path)); - WebSessionManager sessionManager = mock(WebSessionManager.class); + WebSessionManager sessionManager = new MockWebSessionManager(); return new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/accept/HeaderContentTypeResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/accept/HeaderContentTypeResolverTests.java index 5280ed0393..71072f535c 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/accept/HeaderContentTypeResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/accept/HeaderContentTypeResolverTests.java @@ -30,10 +30,10 @@ import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.web.server.NotAcceptableStatusException; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.MockWebSessionManager; import org.springframework.web.server.session.WebSessionManager; import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.mock; /** * Unit tests for {@link HeaderContentTypeResolver}. @@ -75,7 +75,7 @@ public class HeaderContentTypeResolverTests { if (accept != null) { request.getHeaders().add("Accept", accept); } - WebSessionManager sessionManager = mock(WebSessionManager.class); + WebSessionManager sessionManager = new MockWebSessionManager(); return new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/accept/PathExtensionContentTypeResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/accept/PathExtensionContentTypeResolverTests.java index db71fcf1c4..c789fc99c4 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/accept/PathExtensionContentTypeResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/accept/PathExtensionContentTypeResolverTests.java @@ -31,10 +31,10 @@ import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.web.server.NotAcceptableStatusException; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.MockWebSessionManager; import org.springframework.web.server.session.WebSessionManager; import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.mock; /** * Unit tests for {@link PathExtensionContentTypeResolver}. @@ -112,7 +112,7 @@ public class PathExtensionContentTypeResolverTests { private ServerWebExchange createExchange(String path) throws URISyntaxException { ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI(path)); - WebSessionManager sessionManager = mock(WebSessionManager.class); + WebSessionManager sessionManager = new MockWebSessionManager(); return new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/WebReactiveConfigurationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/WebReactiveConfigurationTests.java index 0bbeb262c7..870ae3ac79 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/WebReactiveConfigurationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/WebReactiveConfigurationTests.java @@ -24,7 +24,6 @@ import javax.xml.bind.annotation.XmlRootElement; import org.junit.Before; import org.junit.Test; -import static org.springframework.http.MediaType.*; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import rx.Observable; @@ -35,15 +34,15 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.core.ResolvableType; -import org.springframework.http.codec.json.JacksonJsonEncoder; -import org.springframework.http.codec.xml.Jaxb2Decoder; -import org.springframework.http.codec.xml.Jaxb2Encoder; import org.springframework.core.codec.support.StringDecoder; import org.springframework.core.codec.support.StringEncoder; import org.springframework.core.convert.ConversionService; import org.springframework.core.io.Resource; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; +import org.springframework.http.codec.json.JacksonJsonEncoder; +import org.springframework.http.codec.xml.Jaxb2Decoder; +import org.springframework.http.codec.xml.Jaxb2Encoder; import org.springframework.http.converter.reactive.CodecHttpMessageConverter; import org.springframework.http.converter.reactive.HttpMessageConverter; import org.springframework.http.server.reactive.MockServerHttpRequest; @@ -65,14 +64,18 @@ import org.springframework.web.reactive.result.view.freemarker.FreeMarkerConfigu import org.springframework.web.reactive.result.view.freemarker.FreeMarkerViewResolver; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange; -import org.springframework.web.server.session.WebSessionManager; +import org.springframework.web.server.session.MockWebSessionManager; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.http.MediaType.APPLICATION_OCTET_STREAM; +import static org.springframework.http.MediaType.APPLICATION_XML; +import static org.springframework.http.MediaType.IMAGE_PNG; +import static org.springframework.http.MediaType.TEXT_PLAIN; /** * Unit tests for {@link WebReactiveConfiguration}. @@ -89,7 +92,7 @@ public class WebReactiveConfigurationTests { public void setUp() throws Exception { this.request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); MockServerHttpResponse response = new MockServerHttpResponse(); - this.exchange = new DefaultServerWebExchange(this.request, response, mock(WebSessionManager.class)); + this.exchange = new DefaultServerWebExchange(this.request, response, new MockWebSessionManager()); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingTests.java index 599fcc3b03..bb1b405f16 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/handler/SimpleUrlHandlerMappingTests.java @@ -31,13 +31,13 @@ import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.web.reactive.HandlerMapping; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.MockWebSessionManager; import org.springframework.web.server.session.WebSessionManager; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; -import static org.mockito.Mockito.mock; import static org.springframework.web.reactive.HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE; /** @@ -122,7 +122,7 @@ public class SimpleUrlHandlerMappingTests { private ServerWebExchange createExchange(String path) throws URISyntaxException { ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI(path)); - WebSessionManager sessionManager = mock(WebSessionManager.class); + WebSessionManager sessionManager = new MockWebSessionManager(); return new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/ContentNegotiatingResultHandlerSupportTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/ContentNegotiatingResultHandlerSupportTests.java index 8059a9bdf6..211706577b 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/ContentNegotiatingResultHandlerSupportTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/ContentNegotiatingResultHandlerSupportTests.java @@ -34,10 +34,9 @@ import org.springframework.web.reactive.accept.HeaderContentTypeResolver; import org.springframework.web.reactive.accept.RequestedContentTypeResolver; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange; -import org.springframework.web.server.session.WebSessionManager; +import org.springframework.web.server.session.MockWebSessionManager; import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.mock; import static org.springframework.http.MediaType.ALL; import static org.springframework.http.MediaType.APPLICATION_JSON_UTF8; import static org.springframework.http.MediaType.APPLICATION_OCTET_STREAM; @@ -65,7 +64,7 @@ public class ContentNegotiatingResultHandlerSupportTests { this.handlerSupport = new TestHandlerSupport(); this.request = new MockServerHttpRequest(HttpMethod.GET, new URI("/path")); this.exchange = new DefaultServerWebExchange( - this.request, new MockServerHttpResponse(), mock(WebSessionManager.class)); + this.request, new MockServerHttpResponse(), new MockWebSessionManager()); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/CompositeRequestConditionTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/CompositeRequestConditionTests.java index 88b92ed960..f45e4fbfb3 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/CompositeRequestConditionTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/CompositeRequestConditionTests.java @@ -28,12 +28,12 @@ import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.MockWebSessionManager; import org.springframework.web.server.session.WebSessionManager; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; -import static org.mockito.Mockito.mock; /** * Unit tests for {@link CompositeRequestCondition}. @@ -57,7 +57,7 @@ public class CompositeRequestConditionTests { @Before public void setup() throws Exception { - WebSessionManager sessionManager = mock(WebSessionManager.class); + WebSessionManager sessionManager = new MockWebSessionManager(); this.request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); this.exchange = new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/ConsumesRequestConditionTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/ConsumesRequestConditionTests.java index 03c4acf57c..ff16024929 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/ConsumesRequestConditionTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/ConsumesRequestConditionTests.java @@ -30,6 +30,7 @@ import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.web.reactive.result.condition.ConsumesRequestCondition.ConsumeMediaTypeExpression; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.MockWebSessionManager; import org.springframework.web.server.session.WebSessionManager; import static org.junit.Assert.assertEquals; @@ -37,7 +38,6 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import static org.mockito.Mockito.mock; /** * @author Arjen Poutsma @@ -203,7 +203,7 @@ public class ConsumesRequestConditionTests { if (contentType != null) { request.getHeaders().add("Content-Type", contentType); } - WebSessionManager sessionManager = mock(WebSessionManager.class); + WebSessionManager sessionManager = new MockWebSessionManager(); return new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/HeadersRequestConditionTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/HeadersRequestConditionTests.java index b5dab36223..860e6d7001 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/HeadersRequestConditionTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/HeadersRequestConditionTests.java @@ -28,6 +28,7 @@ import org.springframework.http.server.reactive.MockServerHttpResponse; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.MockWebSessionManager; import org.springframework.web.server.session.WebSessionManager; import static org.junit.Assert.assertEquals; @@ -35,7 +36,6 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; /** * Unit tests for {@link HeadersRequestCondition}. @@ -166,7 +166,7 @@ public class HeadersRequestConditionTests { if (headerName != null) { request.getHeaders().add(headerName, headerValue); } - WebSessionManager sessionManager = mock(WebSessionManager.class); + WebSessionManager sessionManager = new MockWebSessionManager(); return new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/ParamsRequestConditionTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/ParamsRequestConditionTests.java index f28ce748fb..3a19bda99c 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/ParamsRequestConditionTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/ParamsRequestConditionTests.java @@ -28,6 +28,7 @@ import org.springframework.http.server.reactive.MockServerHttpResponse; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.MockWebSessionManager; import org.springframework.web.server.session.WebSessionManager; import static org.junit.Assert.assertEquals; @@ -35,7 +36,6 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; /** * @author Arjen Poutsma @@ -138,7 +138,7 @@ public class ParamsRequestConditionTests { if (paramName != null) { request.getQueryParams().add(paramName, paramValue); } - WebSessionManager sessionManager = mock(WebSessionManager.class); + WebSessionManager sessionManager = new MockWebSessionManager(); return new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/PatternsRequestConditionTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/PatternsRequestConditionTests.java index 4720f94b6a..e153c725df 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/PatternsRequestConditionTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/PatternsRequestConditionTests.java @@ -29,12 +29,12 @@ import org.springframework.http.server.reactive.MockServerHttpResponse; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.MockWebSessionManager; import org.springframework.web.server.session.WebSessionManager; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; -import static org.mockito.Mockito.mock; /** * Unit tests for {@link PatternsRequestCondition}. @@ -227,7 +227,7 @@ public class PatternsRequestConditionTests { private ServerWebExchange createExchange(String path) throws URISyntaxException { ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI(path)); - WebSessionManager sessionManager = mock(WebSessionManager.class); + WebSessionManager sessionManager = new MockWebSessionManager(); return new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/ProducesRequestConditionTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/ProducesRequestConditionTests.java index 4ae8d19363..fad97ddad7 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/ProducesRequestConditionTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/ProducesRequestConditionTests.java @@ -29,6 +29,7 @@ import org.springframework.http.server.reactive.MockServerHttpResponse; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.MockWebSessionManager; import org.springframework.web.server.session.WebSessionManager; import static org.junit.Assert.assertEquals; @@ -36,7 +37,6 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import static org.mockito.Mockito.mock; /** * Unit tests for {@link ProducesRequestCondition}. @@ -320,7 +320,7 @@ public class ProducesRequestConditionTests { request.getHeaders().add("Accept", value); } } - WebSessionManager sessionManager = mock(WebSessionManager.class); + WebSessionManager sessionManager = new MockWebSessionManager(); return new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/RequestConditionHolderTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/RequestConditionHolderTests.java index d5686381b7..266f80aa54 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/RequestConditionHolderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/RequestConditionHolderTests.java @@ -29,13 +29,13 @@ import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.MockWebSessionManager; import org.springframework.web.server.session.WebSessionManager; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; -import static org.mockito.Mockito.mock; /** * Unit tests for {@link RequestConditionHolder}. @@ -54,7 +54,7 @@ public class RequestConditionHolderTests { private ServerWebExchange createExchange() throws URISyntaxException { ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); - WebSessionManager sessionManager = mock(WebSessionManager.class); + WebSessionManager sessionManager = new MockWebSessionManager(); return new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/RequestMappingInfoTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/RequestMappingInfoTests.java index 7ebc75be9e..cfb335ca80 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/RequestMappingInfoTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/RequestMappingInfoTests.java @@ -35,6 +35,7 @@ import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.reactive.result.method.RequestMappingInfo; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.MockWebSessionManager; import org.springframework.web.server.session.WebSessionManager; import static java.util.Arrays.asList; @@ -43,7 +44,6 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; -import static org.mockito.Mockito.mock; /** * Unit tests for {@link RequestMappingInfo}. @@ -61,7 +61,7 @@ public class RequestMappingInfoTests { @Before public void setUp() throws Exception { - WebSessionManager sessionManager = mock(WebSessionManager.class); + WebSessionManager sessionManager = new MockWebSessionManager(); this.request = new MockServerHttpRequest(HttpMethod.GET, new URI("/foo")); this.exchange = new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); } @@ -335,7 +335,7 @@ public class RequestMappingInfoTests { request.getHeaders().add(HttpHeaders.ORIGIN, "http://domain.com"); request.getHeaders().add(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "POST"); - WebSessionManager manager = mock(WebSessionManager.class); + WebSessionManager manager = new MockWebSessionManager(); MockServerHttpResponse response = new MockServerHttpResponse(); ServerWebExchange exchange = new DefaultServerWebExchange(request, response, manager); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/RequestMethodsRequestConditionTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/RequestMethodsRequestConditionTests.java index cce2b92ee0..73aaa01668 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/RequestMethodsRequestConditionTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/RequestMethodsRequestConditionTests.java @@ -31,13 +31,13 @@ import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.MockWebSessionManager; import org.springframework.web.server.session.WebSessionManager; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; import static org.springframework.web.bind.annotation.RequestMethod.DELETE; import static org.springframework.web.bind.annotation.RequestMethod.GET; import static org.springframework.web.bind.annotation.RequestMethod.HEAD; @@ -150,7 +150,7 @@ public class RequestMethodsRequestConditionTests { private ServerWebExchange createExchange(String method) throws URISyntaxException { ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.resolve(method), new URI("/")); - WebSessionManager sessionManager = mock(WebSessionManager.class); + WebSessionManager sessionManager = new MockWebSessionManager(); return new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/HandlerMethodMappingTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/HandlerMethodMappingTests.java index 15df98eb97..23608b0124 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/HandlerMethodMappingTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/HandlerMethodMappingTests.java @@ -40,12 +40,12 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.method.HandlerMethod; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.MockWebSessionManager; import org.springframework.web.server.session.WebSessionManager; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; -import static org.mockito.Mockito.mock; /** * Unit tests for {@link AbstractHandlerMethodMapping}. @@ -150,7 +150,7 @@ public class HandlerMethodMappingTests { private ServerWebExchange createExchange(HttpMethod httpMethod, String path) throws URISyntaxException { ServerHttpRequest request = new MockServerHttpRequest(httpMethod, new URI(path)); - WebSessionManager sessionManager = mock(WebSessionManager.class); + WebSessionManager sessionManager = new MockWebSessionManager(); return new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/InvocableHandlerMethodTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/InvocableHandlerMethodTests.java index 1e8df0b71e..9a5b5c26ed 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/InvocableHandlerMethodTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/InvocableHandlerMethodTests.java @@ -40,6 +40,7 @@ import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.result.method.annotation.RequestParamMethodArgumentResolver; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.MockWebSessionManager; import org.springframework.web.server.session.WebSessionManager; import static org.junit.Assert.assertEquals; @@ -63,7 +64,7 @@ public class InvocableHandlerMethodTests { @Before public void setUp() throws Exception { - WebSessionManager sessionManager = mock(WebSessionManager.class); + WebSessionManager sessionManager = new MockWebSessionManager(); this.request = mock(ServerHttpRequest.class); this.exchange = new DefaultServerWebExchange(request, mock(ServerHttpResponse.class), sessionManager); this.model = new ExtendedModelMap(); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java index 00042ec69f..3d1955007f 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java @@ -59,6 +59,7 @@ import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebInputException; import org.springframework.web.server.UnsupportedMediaTypeStatusException; import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.MockWebSessionManager; import org.springframework.web.server.session.WebSessionManager; import org.springframework.web.util.HttpRequestPathHelper; @@ -68,7 +69,6 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; /** @@ -350,7 +350,7 @@ public class RequestMappingInfoHandlerMappingTests { private ServerWebExchange createExchange(HttpMethod method, String url) throws URISyntaxException { ServerHttpRequest request = new MockServerHttpRequest(method, new URI(url)); - WebSessionManager sessionManager = mock(WebSessionManager.class); + WebSessionManager sessionManager = new MockWebSessionManager(); return new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/CookieValueMethodArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/CookieValueMethodArgumentResolverTests.java index de710dada9..c234b48941 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/CookieValueMethodArgumentResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/CookieValueMethodArgumentResolverTests.java @@ -38,12 +38,12 @@ import org.springframework.web.bind.annotation.CookieValue; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebInputException; import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.MockWebSessionManager; import org.springframework.web.server.session.WebSessionManager; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; /** * Test fixture with {@link CookieValueMethodArgumentResolver}. @@ -69,7 +69,7 @@ public class CookieValueMethodArgumentResolverTests { this.resolver = new CookieValueMethodArgumentResolver(cs, context.getBeanFactory()); ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); - WebSessionManager sessionManager = mock(WebSessionManager.class); + WebSessionManager sessionManager = new MockWebSessionManager(); this.exchange = new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); Method method = getClass().getMethod("params", HttpCookie.class, String.class, String.class); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ExpressionValueMethodArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ExpressionValueMethodArgumentResolverTests.java index f31d996962..5ad0d7f5e8 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ExpressionValueMethodArgumentResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ExpressionValueMethodArgumentResolverTests.java @@ -34,12 +34,12 @@ import org.springframework.http.server.reactive.MockServerHttpResponse; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.MockWebSessionManager; import org.springframework.web.server.session.WebSessionManager; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; /** * Unit tests for {@link ExpressionValueMethodArgumentResolver}. @@ -64,7 +64,7 @@ public class ExpressionValueMethodArgumentResolverTests { this.resolver = new ExpressionValueMethodArgumentResolver(conversionService, context.getBeanFactory()); ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); - WebSessionManager sessionManager = mock(WebSessionManager.class); + WebSessionManager sessionManager = new MockWebSessionManager(); this.exchange = new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); Method method = getClass().getMethod("params", int.class, String.class); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterResultHandlerTests.java index 2a21fe27fc..c00e02fab3 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterResultHandlerTests.java @@ -38,8 +38,6 @@ import rx.Observable; import org.springframework.core.ResolvableType; import org.springframework.core.codec.support.ByteBufferEncoder; -import org.springframework.http.codec.json.JacksonJsonEncoder; -import org.springframework.http.codec.xml.Jaxb2Encoder; import org.springframework.core.codec.support.StringEncoder; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter; @@ -48,6 +46,8 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.core.io.buffer.support.DataBufferTestUtils; import org.springframework.http.HttpMethod; +import org.springframework.http.codec.json.JacksonJsonEncoder; +import org.springframework.http.codec.xml.Jaxb2Encoder; import org.springframework.http.converter.reactive.CodecHttpMessageConverter; import org.springframework.http.converter.reactive.HttpMessageConverter; import org.springframework.http.converter.reactive.ResourceHttpMessageConverter; @@ -59,11 +59,10 @@ import org.springframework.web.reactive.accept.RequestedContentTypeResolver; import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange; -import org.springframework.web.server.session.WebSessionManager; +import org.springframework.web.server.session.MockWebSessionManager; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; -import static org.mockito.Mockito.mock; import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.http.MediaType.APPLICATION_JSON_UTF8; import static org.springframework.web.reactive.HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE; @@ -85,7 +84,7 @@ public class MessageConverterResultHandlerTests { public void setUp() throws Exception { this.resultHandler = createResultHandler(); ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/path")); - this.exchange = new DefaultServerWebExchange(request, this.response, mock(WebSessionManager.class)); + this.exchange = new DefaultServerWebExchange(request, this.response, new MockWebSessionManager()); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/PathVariableMapMethodArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/PathVariableMapMethodArgumentResolverTests.java index bdbf50e541..1399871183 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/PathVariableMapMethodArgumentResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/PathVariableMapMethodArgumentResolverTests.java @@ -36,12 +36,12 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.reactive.HandlerMapping; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.MockWebSessionManager; import org.springframework.web.server.session.WebSessionManager; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; /** * Unit tests for {@link PathVariableMapMethodArgumentResolver}. @@ -64,7 +64,7 @@ public class PathVariableMapMethodArgumentResolverTests { this.resolver = new PathVariableMapMethodArgumentResolver(); ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); - WebSessionManager sessionManager = mock(WebSessionManager.class); + WebSessionManager sessionManager = new MockWebSessionManager(); this.exchange = new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); Method method = getClass().getMethod("handle", Map.class, Map.class, Map.class); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/PathVariableMethodArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/PathVariableMethodArgumentResolverTests.java index 0f4dacb00f..8e93eb0d54 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/PathVariableMethodArgumentResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/PathVariableMethodArgumentResolverTests.java @@ -39,12 +39,12 @@ import org.springframework.web.reactive.HandlerMapping; import org.springframework.web.server.ServerErrorException; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.MockWebSessionManager; import org.springframework.web.server.session.WebSessionManager; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; /** * Unit tests for {@link PathVariableMethodArgumentResolver}. @@ -67,7 +67,7 @@ public class PathVariableMethodArgumentResolverTests { this.resolver = new PathVariableMethodArgumentResolver(conversionService, null); ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); - WebSessionManager sessionManager = mock(WebSessionManager.class); + WebSessionManager sessionManager = new MockWebSessionManager(); this.exchange = new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); Method method = getClass().getMethod("handle", String.class, String.class); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestAttributeMethodArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestAttributeMethodArgumentResolverTests.java index 19c7c2e64e..2fd3e49103 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestAttributeMethodArgumentResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestAttributeMethodArgumentResolverTests.java @@ -40,6 +40,7 @@ import org.springframework.web.bind.annotation.RequestAttribute; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebInputException; import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.MockWebSessionManager; import org.springframework.web.server.session.WebSessionManager; import static org.junit.Assert.assertEquals; @@ -48,7 +49,6 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; /** @@ -73,7 +73,7 @@ public class RequestAttributeMethodArgumentResolverTests { this.resolver = new RequestAttributeMethodArgumentResolver(cs, context.getBeanFactory()); ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); - WebSessionManager sessionManager = mock(WebSessionManager.class); + WebSessionManager sessionManager = new MockWebSessionManager(); this.exchange = new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); this.handleMethod = ReflectionUtils.findMethod(getClass(), "handleWithRequestAttribute", (Class[]) null); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMapMethodArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMapMethodArgumentResolverTests.java index 50ac008eb9..864dd389f7 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMapMethodArgumentResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMapMethodArgumentResolverTests.java @@ -37,12 +37,12 @@ import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.MockWebSessionManager; import org.springframework.web.server.session.WebSessionManager; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; /** * Unit tests for {@link RequestHeaderMapMethodArgumentResolver}. @@ -72,7 +72,7 @@ public class RequestHeaderMapMethodArgumentResolverTests { paramUnsupported = new SynthesizingMethodParameter(method, 3); ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); - WebSessionManager sessionManager = mock(WebSessionManager.class); + WebSessionManager sessionManager = new MockWebSessionManager(); this.exchange = new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolverTests.java index c2d76f5fa4..bda7710c5f 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolverTests.java @@ -43,13 +43,13 @@ import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebInputException; import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.MockWebSessionManager; import org.springframework.web.server.session.WebSessionManager; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; /** * Unit tests for {@link RequestHeaderMethodArgumentResolver}. @@ -91,7 +91,7 @@ public class RequestHeaderMethodArgumentResolverTests { this.paramInstant = new SynthesizingMethodParameter(method, 7); ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); - WebSessionManager sessionManager = mock(WebSessionManager.class); + WebSessionManager sessionManager = new MockWebSessionManager(); this.exchange = new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestParamMapMethodArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestParamMapMethodArgumentResolverTests.java index a8ba097182..c9d7afca9b 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestParamMapMethodArgumentResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestParamMapMethodArgumentResolverTests.java @@ -37,12 +37,12 @@ import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.MockWebSessionManager; import org.springframework.web.server.session.WebSessionManager; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; /** * Unit tests for {@link RequestParamMapMethodArgumentResolver}. @@ -67,7 +67,7 @@ public class RequestParamMapMethodArgumentResolverTests { this.resolver = new RequestParamMapMethodArgumentResolver(); ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); - WebSessionManager sessionManager = mock(WebSessionManager.class); + WebSessionManager sessionManager = new MockWebSessionManager(); this.exchange = new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); Method method = getClass().getMethod("params", Map.class, MultiValueMap.class, Map.class, Map.class); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestParamMethodArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestParamMethodArgumentResolverTests.java index 26b4ea8e5c..bb6a23f096 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestParamMethodArgumentResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestParamMethodArgumentResolverTests.java @@ -43,6 +43,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebInputException; import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.MockWebSessionManager; import org.springframework.web.server.session.WebSessionManager; import static org.junit.Assert.assertArrayEquals; @@ -50,7 +51,6 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; /** * Unit tests for {@link RequestParamMethodArgumentResolver}. @@ -82,7 +82,7 @@ public class RequestParamMethodArgumentResolverTests { Method method = ReflectionUtils.findMethod(getClass(), "handle", (Class[]) null); ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); - WebSessionManager sessionManager = mock(WebSessionManager.class); + WebSessionManager sessionManager = new MockWebSessionManager(); this.exchange = new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); this.paramNamedDefaultValueString = new SynthesizingMethodParameter(method, 0); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java index 793fc1216f..c259e9e915 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java @@ -27,8 +27,6 @@ import reactor.core.publisher.Mono; import org.springframework.core.ResolvableType; import org.springframework.core.codec.support.ByteBufferEncoder; -import org.springframework.http.codec.json.JacksonJsonEncoder; -import org.springframework.http.codec.xml.Jaxb2Encoder; import org.springframework.core.codec.support.StringEncoder; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.convert.support.GenericConversionService; @@ -36,6 +34,8 @@ import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutu import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; +import org.springframework.http.codec.json.JacksonJsonEncoder; +import org.springframework.http.codec.xml.Jaxb2Encoder; import org.springframework.http.converter.reactive.CodecHttpMessageConverter; import org.springframework.http.converter.reactive.HttpMessageConverter; import org.springframework.http.converter.reactive.ResourceHttpMessageConverter; @@ -53,10 +53,9 @@ import org.springframework.web.reactive.accept.RequestedContentTypeResolver; import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange; -import org.springframework.web.server.session.WebSessionManager; +import org.springframework.web.server.session.MockWebSessionManager; import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.mock; /** @@ -83,7 +82,7 @@ public class ResponseBodyResultHandlerTests { public void setUp() throws Exception { this.resultHandler = createHandler(); ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/path")); - this.exchange = new DefaultServerWebExchange(request, this.response, mock(WebSessionManager.class)); + this.exchange = new DefaultServerWebExchange(request, this.response, new MockWebSessionManager()); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java index edca26d48a..48585447fa 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java @@ -31,8 +31,6 @@ import rx.Single; import org.springframework.core.ResolvableType; import org.springframework.core.codec.support.ByteBufferEncoder; -import org.springframework.http.codec.json.JacksonJsonEncoder; -import org.springframework.http.codec.xml.Jaxb2Encoder; import org.springframework.core.codec.support.StringEncoder; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter; @@ -41,6 +39,8 @@ import org.springframework.core.io.buffer.support.DataBufferTestUtils; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.http.codec.json.JacksonJsonEncoder; +import org.springframework.http.codec.xml.Jaxb2Encoder; import org.springframework.http.converter.reactive.CodecHttpMessageConverter; import org.springframework.http.converter.reactive.HttpMessageConverter; import org.springframework.http.converter.reactive.ResourceHttpMessageConverter; @@ -55,13 +55,12 @@ import org.springframework.web.reactive.accept.RequestedContentTypeResolver; import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange; -import org.springframework.web.server.session.WebSessionManager; +import org.springframework.web.server.session.MockWebSessionManager; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; import static org.springframework.core.ResolvableType.forClassWithGenerics; /** @@ -89,7 +88,7 @@ public class ResponseEntityResultHandlerTests { public void setUp() throws Exception { this.resultHandler = createHandler(); ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/path")); - this.exchange = new DefaultServerWebExchange(request, this.response, mock(WebSessionManager.class)); + this.exchange = new DefaultServerWebExchange(request, this.response, new MockWebSessionManager()); } private ResponseEntityResultHandler createHandler(HttpMessageConverter... converters) { diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SessionAttributeMethodArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SessionAttributeMethodArgumentResolverTests.java index 4eb0cab9d5..008352b278 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SessionAttributeMethodArgumentResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SessionAttributeMethodArgumentResolverTests.java @@ -41,6 +41,7 @@ import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebInputException; import org.springframework.web.server.WebSession; import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.MockWebSessionManager; import org.springframework.web.server.session.WebSessionManager; import static org.junit.Assert.assertEquals; @@ -76,14 +77,13 @@ public class SessionAttributeMethodArgumentResolverTests { ConversionService cs = new DefaultConversionService(); this.resolver = new SessionAttributeMethodArgumentResolver(cs, context.getBeanFactory()); - ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); - WebSessionManager sessionManager = mock(WebSessionManager.class); - this.exchange = new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); - this.session = mock(WebSession.class); - when(sessionManager.getSession(this.exchange)).thenReturn(Mono.just(this.session)); when(this.session.getAttribute(any())).thenReturn(Optional.empty()); + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); + WebSessionManager sessionManager = new MockWebSessionManager(this.session); + this.exchange = new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); + this.handleMethod = ReflectionUtils.findMethod(getClass(), "handleWithSessionAttribute", (Class[]) null); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/server/handler/ExceptionHandlingHttpHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/server/handler/ExceptionHandlingHttpHandlerTests.java index 214fc0cf42..6152a48e2f 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/server/handler/ExceptionHandlingHttpHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/server/handler/ExceptionHandlingHttpHandlerTests.java @@ -26,14 +26,14 @@ import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.MockServerHttpRequest; import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebExceptionHandler; import org.springframework.web.server.WebHandler; -import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.MockWebSessionManager; import org.springframework.web.server.session.WebSessionManager; import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.mock; /** * @author Rossen Stoyanchev @@ -51,7 +51,7 @@ public class ExceptionHandlingHttpHandlerTests { @Before public void setUp() throws Exception { URI uri = new URI("http://localhost:8080"); - WebSessionManager sessionManager = mock(WebSessionManager.class); + WebSessionManager sessionManager = new MockWebSessionManager(); MockServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, uri); this.response = new MockServerHttpResponse(); this.exchange = new DefaultServerWebExchange(request, this.response, sessionManager); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/server/session/MockWebSessionManager.java b/spring-web-reactive/src/test/java/org/springframework/web/server/session/MockWebSessionManager.java new file mode 100644 index 0000000000..5697d40f84 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/server/session/MockWebSessionManager.java @@ -0,0 +1,51 @@ +/* + * 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.web.server.session; + +import reactor.core.publisher.Mono; + +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebSession; + +/** + * Mock implementation of {@link WebSessionManager}. + * + * @author Rossen Stoyanchev + */ +public class MockWebSessionManager implements WebSessionManager { + + private final Mono session; + + + public MockWebSessionManager() { + this(Mono.empty()); + } + + public MockWebSessionManager(WebSession session) { + this(Mono.just(session)); + } + + public MockWebSessionManager(Mono session) { + this.session = session; + } + + + @Override + public Mono getSession(ServerWebExchange exchange) { + return this.session; + } + +} From 2f2546c8a454bd63181d793e31acdf0252b3ff4c Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 30 Jun 2016 16:00:49 -0400 Subject: [PATCH 445/498] Simplify initialization of WebSession Mono The DefaultWebSessionManager now uses Mono.defer to protect the call to getSession from parsing session cookies immediately. This allows pre-initializing the Mono upfront vs using a lock. --- .../adapter/DefaultServerWebExchange.java | 17 ++--------------- .../session/DefaultWebSessionManager.java | 13 +++++++------ 2 files changed, 9 insertions(+), 21 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java b/spring-web-reactive/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java index 295db4f880..bbd7342fca 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java @@ -39,15 +39,9 @@ public class DefaultServerWebExchange implements ServerWebExchange { private final ServerHttpResponse response; - private final WebSessionManager sessionManager; - - private final Map attributes = new ConcurrentHashMap<>(); - private final Object createSessionLock = new Object(); - - private Mono sessionMono; - + private final Mono sessionMono; public DefaultServerWebExchange(ServerHttpRequest request, ServerHttpResponse response, @@ -58,7 +52,7 @@ public class DefaultServerWebExchange implements ServerWebExchange { Assert.notNull(response, "'sessionManager' is required."); this.request = request; this.response = response; - this.sessionManager = sessionManager; + this.sessionMono = sessionManager.getSession(this).cache(); } @@ -84,13 +78,6 @@ public class DefaultServerWebExchange implements ServerWebExchange { @Override public Mono getSession() { - if (this.sessionMono == null) { - synchronized (this.createSessionLock) { - if (this.sessionMono == null) { - this.sessionMono = this.sessionManager.getSession(this).cache(); - } - } - } return this.sessionMono; } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/session/DefaultWebSessionManager.java b/spring-web-reactive/src/main/java/org/springframework/web/server/session/DefaultWebSessionManager.java index 64a5e58b2e..6e51319ac2 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/server/session/DefaultWebSessionManager.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/session/DefaultWebSessionManager.java @@ -99,12 +99,13 @@ public class DefaultWebSessionManager implements WebSessionManager { @Override public Mono getSession(ServerWebExchange exchange) { - return Flux.fromIterable(getSessionIdResolver().resolveSessionIds(exchange)) - .concatMap(this.sessionStore::retrieveSession) - .next() - .then(session -> validateSession(exchange, session)) - .otherwiseIfEmpty(createSession(exchange)) - .map(session -> extendSession(exchange, session)); + return Mono.defer(() -> + Flux.fromIterable(getSessionIdResolver().resolveSessionIds(exchange)) + .concatMap(this.sessionStore::retrieveSession) + .next() + .then(session -> validateSession(exchange, session)) + .otherwiseIfEmpty(createSession(exchange)) + .map(session -> extendSession(exchange, session))); } protected Mono validateSession(ServerWebExchange exchange, WebSession session) { From 952395192fb9c878272487a0d487a5fe177f7dbd Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 30 Jun 2016 16:12:36 -0400 Subject: [PATCH 446/498] Collapse core.codec and core.codec.support into one --- .../core/codec/{support => }/AbstractDecoder.java | 2 +- .../core/codec/{support => }/AbstractEncoder.java | 2 +- .../codec/{support => }/AbstractSingleValueEncoder.java | 2 +- .../core/codec/{support => }/ByteBufferDecoder.java | 2 +- .../core/codec/{support => }/ByteBufferEncoder.java | 2 +- .../core/codec/{support => }/ResourceDecoder.java | 3 ++- .../core/codec/{support => }/ResourceEncoder.java | 3 ++- .../core/codec/{support => }/StringDecoder.java | 2 +- .../core/codec/{support => }/StringEncoder.java | 2 +- .../java/org/springframework/core/codec/package-info.java | 5 +++-- .../springframework/core/codec/support/package-info.java | 4 ---- .../org/springframework/http/codec/SseEventEncoder.java | 2 +- .../http/codec/json/JacksonJsonDecoder.java | 2 +- .../http/codec/json/JacksonJsonEncoder.java | 2 +- .../http/codec/json/JsonObjectDecoder.java | 2 +- .../org/springframework/http/codec/xml/Jaxb2Decoder.java | 2 +- .../org/springframework/http/codec/xml/Jaxb2Encoder.java | 2 +- .../springframework/http/codec/xml/XmlEventDecoder.java | 2 +- .../converter/reactive/ResourceHttpMessageConverter.java | 4 ++-- .../springframework/web/client/reactive/WebClient.java | 8 ++++---- .../web/reactive/config/WebReactiveConfiguration.java | 8 ++++---- .../method/annotation/RequestMappingHandlerAdapter.java | 4 ++-- .../core/codec/{support => }/ByteBufferDecoderTests.java | 3 ++- .../core/codec/{support => }/ByteBufferEncoderTests.java | 3 ++- .../core/codec/{support => }/ResourceDecoderTests.java | 3 ++- .../core/codec/{support => }/ResourceEncoderTests.java | 3 ++- .../core/codec/{support => }/StringDecoderTests.java | 3 ++- .../core/codec/{support => }/StringEncoderTests.java | 3 ++- .../web/reactive/DispatcherHandlerErrorTests.java | 2 +- .../reactive/config/WebReactiveConfigurationTests.java | 4 ++-- .../annotation/MessageConverterResultHandlerTests.java | 4 ++-- .../annotation/RequestBodyArgumentResolverTests.java | 2 +- .../method/annotation/ResponseBodyResultHandlerTests.java | 4 ++-- .../annotation/ResponseEntityResultHandlerTests.java | 4 ++-- .../result/method/annotation/SseIntegrationTests.java | 4 ++-- .../result/view/HttpMessageConverterViewTests.java | 2 +- 36 files changed, 58 insertions(+), 53 deletions(-) rename spring-web-reactive/src/main/java/org/springframework/core/codec/{support => }/AbstractDecoder.java (96%) rename spring-web-reactive/src/main/java/org/springframework/core/codec/{support => }/AbstractEncoder.java (96%) rename spring-web-reactive/src/main/java/org/springframework/core/codec/{support => }/AbstractSingleValueEncoder.java (97%) rename spring-web-reactive/src/main/java/org/springframework/core/codec/{support => }/ByteBufferDecoder.java (97%) rename spring-web-reactive/src/main/java/org/springframework/core/codec/{support => }/ByteBufferEncoder.java (97%) rename spring-web-reactive/src/main/java/org/springframework/core/codec/{support => }/ResourceDecoder.java (96%) rename spring-web-reactive/src/main/java/org/springframework/core/codec/{support => }/ResourceEncoder.java (95%) rename spring-web-reactive/src/main/java/org/springframework/core/codec/{support => }/StringDecoder.java (98%) rename spring-web-reactive/src/main/java/org/springframework/core/codec/{support => }/StringEncoder.java (97%) delete mode 100644 spring-web-reactive/src/main/java/org/springframework/core/codec/support/package-info.java rename spring-web-reactive/src/test/java/org/springframework/core/codec/{support => }/ByteBufferDecoderTests.java (95%) rename spring-web-reactive/src/test/java/org/springframework/core/codec/{support => }/ByteBufferEncoderTests.java (96%) rename spring-web-reactive/src/test/java/org/springframework/core/codec/{support => }/ResourceDecoderTests.java (96%) rename spring-web-reactive/src/test/java/org/springframework/core/codec/{support => }/ResourceEncoderTests.java (96%) rename spring-web-reactive/src/test/java/org/springframework/core/codec/{support => }/StringDecoderTests.java (97%) rename spring-web-reactive/src/test/java/org/springframework/core/codec/{support => }/StringEncoderTests.java (96%) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/AbstractDecoder.java similarity index 96% rename from spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractDecoder.java rename to spring-web-reactive/src/main/java/org/springframework/core/codec/AbstractDecoder.java index dde5ee7d6f..5e925c5ca3 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/AbstractDecoder.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.core.codec.support; +package org.springframework.core.codec; import java.util.Arrays; import java.util.Collections; diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/AbstractEncoder.java similarity index 96% rename from spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractEncoder.java rename to spring-web-reactive/src/main/java/org/springframework/core/codec/AbstractEncoder.java index 9bb7cfd315..10a60be070 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/AbstractEncoder.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.core.codec.support; +package org.springframework.core.codec; import java.util.Arrays; import java.util.Collections; diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractSingleValueEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/AbstractSingleValueEncoder.java similarity index 97% rename from spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractSingleValueEncoder.java rename to spring-web-reactive/src/main/java/org/springframework/core/codec/AbstractSingleValueEncoder.java index c023670d8b..ca83b76c79 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/AbstractSingleValueEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/AbstractSingleValueEncoder.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.core.codec.support; +package org.springframework.core.codec; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/ByteBufferDecoder.java similarity index 97% rename from spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferDecoder.java rename to spring-web-reactive/src/main/java/org/springframework/core/codec/ByteBufferDecoder.java index 3f9ead7694..7f9c8ea6db 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/ByteBufferDecoder.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.core.codec.support; +package org.springframework.core.codec; import java.nio.ByteBuffer; diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/ByteBufferEncoder.java similarity index 97% rename from spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferEncoder.java rename to spring-web-reactive/src/main/java/org/springframework/core/codec/ByteBufferEncoder.java index c6e69a494e..b3a1cc2f0d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ByteBufferEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/ByteBufferEncoder.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.core.codec.support; +package org.springframework.core.codec; import java.nio.ByteBuffer; diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ResourceDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/ResourceDecoder.java similarity index 96% rename from spring-web-reactive/src/main/java/org/springframework/core/codec/support/ResourceDecoder.java rename to spring-web-reactive/src/main/java/org/springframework/core/codec/ResourceDecoder.java index d36aba1854..5694887b4b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ResourceDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/ResourceDecoder.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.core.codec.support; +package org.springframework.core.codec; import java.io.ByteArrayInputStream; @@ -23,6 +23,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.core.ResolvableType; +import org.springframework.core.codec.AbstractDecoder; import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.InputStreamResource; import org.springframework.core.io.Resource; diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ResourceEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/ResourceEncoder.java similarity index 95% rename from spring-web-reactive/src/main/java/org/springframework/core/codec/support/ResourceEncoder.java rename to spring-web-reactive/src/main/java/org/springframework/core/codec/ResourceEncoder.java index 5498ac153e..8c9d0bd362 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ResourceEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/ResourceEncoder.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.core.codec.support; +package org.springframework.core.codec; import java.io.IOException; import java.io.InputStream; @@ -22,6 +22,7 @@ import java.io.InputStream; import reactor.core.publisher.Flux; import org.springframework.core.ResolvableType; +import org.springframework.core.codec.AbstractSingleValueEncoder; import org.springframework.core.io.Resource; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/StringDecoder.java similarity index 98% rename from spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java rename to spring-web-reactive/src/main/java/org/springframework/core/codec/StringDecoder.java index 6ef47d1184..b5936aabca 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/StringDecoder.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.core.codec.support; +package org.springframework.core.codec; import java.nio.CharBuffer; import java.nio.charset.Charset; diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/StringEncoder.java similarity index 97% rename from spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringEncoder.java rename to spring-web-reactive/src/main/java/org/springframework/core/codec/StringEncoder.java index 97094910a0..915980f492 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/StringEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/StringEncoder.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.core.codec.support; +package org.springframework.core.codec; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/package-info.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/package-info.java index 9509c9b2f2..5a7d3df6f9 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/package-info.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/package-info.java @@ -1,5 +1,6 @@ /** - * Root-package for codecs used to transform streams of bytes. - * Used for example for serialization/deserialization. + * Provides {@link org.springframework.core.codec.Encoder} and + * {@link org.springframework.core.codec.Decoder} abstractions for converting + * to and from between a stream of bytes and a stream of Java objects. */ package org.springframework.core.codec; diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/package-info.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/package-info.java deleted file mode 100644 index e915f2a1a2..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * This package contains codec implementations. - */ -package org.springframework.core.codec.support; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/codec/SseEventEncoder.java b/spring-web-reactive/src/main/java/org/springframework/http/codec/SseEventEncoder.java index 5cbac76600..f5dfa2f9b1 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/codec/SseEventEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/codec/SseEventEncoder.java @@ -27,7 +27,7 @@ import reactor.core.publisher.Mono; import org.springframework.core.ResolvableType; import org.springframework.core.codec.CodecException; import org.springframework.core.codec.Encoder; -import org.springframework.core.codec.support.AbstractEncoder; +import org.springframework.core.codec.AbstractEncoder; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.FlushingDataBuffer; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/codec/json/JacksonJsonDecoder.java b/spring-web-reactive/src/main/java/org/springframework/http/codec/json/JacksonJsonDecoder.java index e5d51005ef..39bae68c96 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/codec/json/JacksonJsonDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/codec/json/JacksonJsonDecoder.java @@ -29,7 +29,7 @@ import reactor.core.publisher.Flux; import org.springframework.core.ResolvableType; import org.springframework.core.codec.CodecException; import org.springframework.core.codec.Decoder; -import org.springframework.core.codec.support.AbstractDecoder; +import org.springframework.core.codec.AbstractDecoder; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.support.DataBufferUtils; import org.springframework.util.Assert; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/codec/json/JacksonJsonEncoder.java b/spring-web-reactive/src/main/java/org/springframework/http/codec/json/JacksonJsonEncoder.java index 69cabc1c0e..862edd712a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/codec/json/JacksonJsonEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/codec/json/JacksonJsonEncoder.java @@ -31,7 +31,7 @@ import reactor.core.publisher.Mono; import org.springframework.core.ResolvableType; import org.springframework.core.codec.CodecException; -import org.springframework.core.codec.support.AbstractEncoder; +import org.springframework.core.codec.AbstractEncoder; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.util.Assert; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/codec/json/JsonObjectDecoder.java b/spring-web-reactive/src/main/java/org/springframework/http/codec/json/JsonObjectDecoder.java index 82b298e847..1f8caf1cd9 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/codec/json/JsonObjectDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/codec/json/JsonObjectDecoder.java @@ -28,7 +28,7 @@ import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import org.springframework.core.ResolvableType; -import org.springframework.core.codec.support.AbstractDecoder; +import org.springframework.core.codec.AbstractDecoder; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.support.DataBufferUtils; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/codec/xml/Jaxb2Decoder.java b/spring-web-reactive/src/main/java/org/springframework/http/codec/xml/Jaxb2Decoder.java index 4054f3b2e1..d0dfe8ae6d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/codec/xml/Jaxb2Decoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/codec/xml/Jaxb2Decoder.java @@ -36,7 +36,7 @@ import reactor.core.publisher.Mono; import org.springframework.core.ResolvableType; import org.springframework.core.codec.CodecException; -import org.springframework.core.codec.support.AbstractDecoder; +import org.springframework.core.codec.AbstractDecoder; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.util.ClassUtils; import org.springframework.util.MimeType; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/codec/xml/Jaxb2Encoder.java b/spring-web-reactive/src/main/java/org/springframework/http/codec/xml/Jaxb2Encoder.java index b7eb53f895..b80bb2a324 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/codec/xml/Jaxb2Encoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/codec/xml/Jaxb2Encoder.java @@ -26,7 +26,7 @@ import javax.xml.bind.annotation.XmlType; import reactor.core.publisher.Flux; import org.springframework.core.ResolvableType; -import org.springframework.core.codec.support.AbstractSingleValueEncoder; +import org.springframework.core.codec.AbstractSingleValueEncoder; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.util.ClassUtils; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/codec/xml/XmlEventDecoder.java b/spring-web-reactive/src/main/java/org/springframework/http/codec/xml/XmlEventDecoder.java index 7bac139387..7009a00b12 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/codec/xml/XmlEventDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/codec/xml/XmlEventDecoder.java @@ -35,7 +35,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.core.ResolvableType; -import org.springframework.core.codec.support.AbstractDecoder; +import org.springframework.core.codec.AbstractDecoder; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.support.DataBufferUtils; import org.springframework.util.ClassUtils; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/ResourceHttpMessageConverter.java b/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/ResourceHttpMessageConverter.java index 158ef29847..d98e7c1a35 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/ResourceHttpMessageConverter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/ResourceHttpMessageConverter.java @@ -25,8 +25,8 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.core.ResolvableType; -import org.springframework.core.codec.support.ResourceDecoder; -import org.springframework.core.codec.support.ResourceEncoder; +import org.springframework.core.codec.ResourceDecoder; +import org.springframework.core.codec.ResourceEncoder; import org.springframework.core.io.InputStreamResource; import org.springframework.core.io.Resource; import org.springframework.core.io.support.ResourceUtils2; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebClient.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebClient.java index af50d86d77..270eeb53aa 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebClient.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebClient.java @@ -24,12 +24,12 @@ import reactor.core.publisher.Mono; import org.springframework.core.codec.Decoder; import org.springframework.core.codec.Encoder; -import org.springframework.core.codec.support.ByteBufferDecoder; -import org.springframework.core.codec.support.ByteBufferEncoder; +import org.springframework.core.codec.ByteBufferDecoder; +import org.springframework.core.codec.ByteBufferEncoder; import org.springframework.http.codec.json.JacksonJsonDecoder; import org.springframework.http.codec.json.JacksonJsonEncoder; -import org.springframework.core.codec.support.StringDecoder; -import org.springframework.core.codec.support.StringEncoder; +import org.springframework.core.codec.StringDecoder; +import org.springframework.core.codec.StringEncoder; import org.springframework.http.HttpStatus; import org.springframework.http.client.reactive.ClientHttpRequest; import org.springframework.http.client.reactive.ClientHttpRequestFactory; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java index a7b1011722..51eb9234c4 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java @@ -30,14 +30,14 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.codec.Decoder; import org.springframework.core.codec.Encoder; -import org.springframework.core.codec.support.ByteBufferDecoder; -import org.springframework.core.codec.support.ByteBufferEncoder; +import org.springframework.core.codec.ByteBufferDecoder; +import org.springframework.core.codec.ByteBufferEncoder; import org.springframework.http.codec.json.JacksonJsonDecoder; import org.springframework.http.codec.json.JacksonJsonEncoder; import org.springframework.http.codec.xml.Jaxb2Decoder; import org.springframework.http.codec.xml.Jaxb2Encoder; -import org.springframework.core.codec.support.StringDecoder; -import org.springframework.core.codec.support.StringEncoder; +import org.springframework.core.codec.StringDecoder; +import org.springframework.core.codec.StringEncoder; import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.ConverterRegistry; import org.springframework.core.convert.support.GenericConversionService; 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 dba37ad1f8..7da5694419 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 @@ -31,8 +31,8 @@ import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.config.ConfigurableBeanFactory; -import org.springframework.core.codec.support.ByteBufferDecoder; -import org.springframework.core.codec.support.StringDecoder; +import org.springframework.core.codec.ByteBufferDecoder; +import org.springframework.core.codec.StringDecoder; import org.springframework.core.convert.ConversionService; import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.http.converter.reactive.CodecHttpMessageConverter; diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ByteBufferDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/ByteBufferDecoderTests.java similarity index 95% rename from spring-web-reactive/src/test/java/org/springframework/core/codec/support/ByteBufferDecoderTests.java rename to spring-web-reactive/src/test/java/org/springframework/core/codec/ByteBufferDecoderTests.java index cc59004367..d44f7cb544 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ByteBufferDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/ByteBufferDecoderTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.core.codec.support; +package org.springframework.core.codec; import java.nio.ByteBuffer; @@ -24,6 +24,7 @@ import reactor.core.publisher.Flux; import reactor.core.test.TestSubscriber; import org.springframework.core.ResolvableType; +import org.springframework.core.codec.ByteBufferDecoder; import org.springframework.core.io.buffer.AbstractDataBufferAllocatingTestCase; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.MediaType; diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ByteBufferEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/ByteBufferEncoderTests.java similarity index 96% rename from spring-web-reactive/src/test/java/org/springframework/core/codec/support/ByteBufferEncoderTests.java rename to spring-web-reactive/src/test/java/org/springframework/core/codec/ByteBufferEncoderTests.java index 60458231c6..5160395698 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ByteBufferEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/ByteBufferEncoderTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.core.codec.support; +package org.springframework.core.codec; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; @@ -26,6 +26,7 @@ import reactor.core.publisher.Flux; import reactor.core.test.TestSubscriber; import org.springframework.core.ResolvableType; +import org.springframework.core.codec.ByteBufferEncoder; import org.springframework.core.io.buffer.AbstractDataBufferAllocatingTestCase; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.MediaType; diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ResourceDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/ResourceDecoderTests.java similarity index 96% rename from spring-web-reactive/src/test/java/org/springframework/core/codec/support/ResourceDecoderTests.java rename to spring-web-reactive/src/test/java/org/springframework/core/codec/ResourceDecoderTests.java index d9372f2163..22b8e5a426 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ResourceDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/ResourceDecoderTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.core.codec.support; +package org.springframework.core.codec; import java.io.IOException; @@ -23,6 +23,7 @@ import reactor.core.publisher.Flux; import reactor.core.test.TestSubscriber; import org.springframework.core.ResolvableType; +import org.springframework.core.codec.ResourceDecoder; import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.InputStreamResource; import org.springframework.core.io.Resource; diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ResourceEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/ResourceEncoderTests.java similarity index 96% rename from spring-web-reactive/src/test/java/org/springframework/core/codec/support/ResourceEncoderTests.java rename to spring-web-reactive/src/test/java/org/springframework/core/codec/ResourceEncoderTests.java index 9e28305c87..fb747bb3a0 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/ResourceEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/ResourceEncoderTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.core.codec.support; +package org.springframework.core.codec; import java.nio.charset.StandardCharsets; @@ -24,6 +24,7 @@ import reactor.core.publisher.Mono; import reactor.core.test.TestSubscriber; import org.springframework.core.ResolvableType; +import org.springframework.core.codec.ResourceEncoder; import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.InputStreamResource; import org.springframework.core.io.Resource; diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/StringDecoderTests.java similarity index 97% rename from spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringDecoderTests.java rename to spring-web-reactive/src/test/java/org/springframework/core/codec/StringDecoderTests.java index 62187a69aa..505dca462f 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/StringDecoderTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.core.codec.support; +package org.springframework.core.codec; import org.junit.Before; import org.junit.Test; @@ -22,6 +22,7 @@ import reactor.core.publisher.Flux; import reactor.core.test.TestSubscriber; import org.springframework.core.ResolvableType; +import org.springframework.core.codec.StringDecoder; import org.springframework.core.io.buffer.AbstractDataBufferAllocatingTestCase; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.MediaType; diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/StringEncoderTests.java similarity index 96% rename from spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringEncoderTests.java rename to spring-web-reactive/src/test/java/org/springframework/core/codec/StringEncoderTests.java index 296b562e75..850775311f 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/StringEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/StringEncoderTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.core.codec.support; +package org.springframework.core.codec; import java.nio.charset.StandardCharsets; @@ -26,6 +26,7 @@ import reactor.core.publisher.Flux; import reactor.core.test.TestSubscriber; import org.springframework.core.ResolvableType; +import org.springframework.core.codec.StringEncoder; import org.springframework.core.io.buffer.AbstractDataBufferAllocatingTestCase; import org.springframework.core.io.buffer.support.DataBufferUtils; import org.springframework.http.MediaType; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java index 993c992882..e6ba0b2163 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java @@ -29,7 +29,7 @@ import reactor.core.util.SignalKind; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.core.codec.support.StringEncoder; +import org.springframework.core.codec.StringEncoder; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.io.buffer.DataBuffer; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/WebReactiveConfigurationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/WebReactiveConfigurationTests.java index 870ae3ac79..25b75c0680 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/WebReactiveConfigurationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/WebReactiveConfigurationTests.java @@ -34,8 +34,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.core.ResolvableType; -import org.springframework.core.codec.support.StringDecoder; -import org.springframework.core.codec.support.StringEncoder; +import org.springframework.core.codec.StringDecoder; +import org.springframework.core.codec.StringEncoder; import org.springframework.core.convert.ConversionService; import org.springframework.core.io.Resource; import org.springframework.http.HttpMethod; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterResultHandlerTests.java index c00e02fab3..b0f15af7d0 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterResultHandlerTests.java @@ -37,8 +37,8 @@ import reactor.core.test.TestSubscriber; import rx.Observable; import org.springframework.core.ResolvableType; -import org.springframework.core.codec.support.ByteBufferEncoder; -import org.springframework.core.codec.support.StringEncoder; +import org.springframework.core.codec.ByteBufferEncoder; +import org.springframework.core.codec.StringEncoder; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter; import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java index 142d9ed3de..b462d2f114 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java @@ -44,7 +44,7 @@ import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.annotation.SynthesizingMethodParameter; import org.springframework.core.codec.Decoder; import org.springframework.http.codec.json.JacksonJsonDecoder; -import org.springframework.core.codec.support.StringDecoder; +import org.springframework.core.codec.StringDecoder; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter; import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java index c259e9e915..3d3ac4f3aa 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java @@ -26,8 +26,8 @@ import org.junit.Test; import reactor.core.publisher.Mono; import org.springframework.core.ResolvableType; -import org.springframework.core.codec.support.ByteBufferEncoder; -import org.springframework.core.codec.support.StringEncoder; +import org.springframework.core.codec.ByteBufferEncoder; +import org.springframework.core.codec.StringEncoder; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java index 48585447fa..7d01f3c9f8 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java @@ -30,8 +30,8 @@ import reactor.core.test.TestSubscriber; import rx.Single; import org.springframework.core.ResolvableType; -import org.springframework.core.codec.support.ByteBufferEncoder; -import org.springframework.core.codec.support.StringEncoder; +import org.springframework.core.codec.ByteBufferEncoder; +import org.springframework.core.codec.StringEncoder; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter; import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java index 74bc385ca8..79beae54ef 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java @@ -30,10 +30,10 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.codec.Encoder; -import org.springframework.core.codec.support.ByteBufferDecoder; +import org.springframework.core.codec.ByteBufferDecoder; import org.springframework.http.codec.json.JacksonJsonDecoder; import org.springframework.http.codec.json.JacksonJsonEncoder; -import org.springframework.core.codec.support.StringDecoder; +import org.springframework.core.codec.StringDecoder; import org.springframework.http.MediaType; import org.springframework.http.client.reactive.ReactorHttpClientRequestFactory; import org.springframework.http.codec.SseEventEncoder; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/HttpMessageConverterViewTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/HttpMessageConverterViewTests.java index a6a24ac4f9..0f6cc3a3a0 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/HttpMessageConverterViewTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/HttpMessageConverterViewTests.java @@ -34,7 +34,7 @@ import reactor.core.test.TestSubscriber; import org.springframework.core.ResolvableType; import org.springframework.http.codec.json.JacksonJsonEncoder; import org.springframework.http.codec.xml.Jaxb2Encoder; -import org.springframework.core.codec.support.StringEncoder; +import org.springframework.core.codec.StringEncoder; import org.springframework.core.io.buffer.support.DataBufferTestUtils; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; From 12d7b78169274c99862a22a292d6d09b381fe04a Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Fri, 1 Jul 2016 16:59:50 +0200 Subject: [PATCH 447/498] Refactor reactive type conversion support This commit replaces Reactive Streams converters for RxJava1 and CompletableFuture with Reactor specific ones. The results in conversion that preserves stream semantics, i.e. Mono vs Flux. For example this is allowed: Flux -> Observable Mono -> Single Mono -> CompletableFuture This is not allowed: Flux -> Single Mono -> Observable Flux -> CompletableFuture As a result it is now possible to check through the ConversionService if a target type to convert to is a stream of many or of one which is useful for decoding purposes. The commit also adds PublisherToFluxConverter to allow conversion from raw Publisher to Flux. The reverse is not necessary since Flux is a Publisher and it's a no-op conversion. --- ... => MonoToCompletableFutureConverter.java} | 8 +-- .../support/PublisherToFluxConverter.java | 51 +++++++++++++++++++ ...er.java => ReactorToRxJava1Converter.java} | 17 ++++--- .../config/WebReactiveConfiguration.java | 14 ++--- .../result/SimpleResultHandlerTests.java | 12 ++--- .../MessageConverterResultHandlerTests.java | 11 ++-- .../RequestBodyArgumentResolverTests.java | 15 +++--- .../ResponseBodyResultHandlerTests.java | 10 ++-- .../ResponseEntityResultHandlerTests.java | 12 +++-- .../ViewResolutionResultHandlerTests.java | 6 ++- 10 files changed, 108 insertions(+), 48 deletions(-) rename spring-web-reactive/src/main/java/org/springframework/core/convert/support/{ReactiveStreamsToCompletableFutureConverter.java => MonoToCompletableFutureConverter.java} (84%) create mode 100644 spring-web-reactive/src/main/java/org/springframework/core/convert/support/PublisherToFluxConverter.java rename spring-web-reactive/src/main/java/org/springframework/core/convert/support/{ReactiveStreamsToRxJava1Converter.java => ReactorToRxJava1Converter.java} (74%) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToCompletableFutureConverter.java b/spring-web-reactive/src/main/java/org/springframework/core/convert/support/MonoToCompletableFutureConverter.java similarity index 84% rename from spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToCompletableFutureConverter.java rename to spring-web-reactive/src/main/java/org/springframework/core/convert/support/MonoToCompletableFutureConverter.java index 41234dfe57..bd3a06869f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToCompletableFutureConverter.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/convert/support/MonoToCompletableFutureConverter.java @@ -29,12 +29,12 @@ import org.springframework.core.convert.converter.GenericConverter; /** * @author Sebastien Deleuze */ -public class ReactiveStreamsToCompletableFutureConverter implements GenericConverter { +public class MonoToCompletableFutureConverter implements GenericConverter { @Override public Set getConvertibleTypes() { Set pairs = new LinkedHashSet<>(); - pairs.add(new GenericConverter.ConvertiblePair(Publisher.class, CompletableFuture.class)); + pairs.add(new GenericConverter.ConvertiblePair(Mono.class, CompletableFuture.class)); pairs.add(new GenericConverter.ConvertiblePair(CompletableFuture.class, Publisher.class)); return pairs; } @@ -44,10 +44,10 @@ public class ReactiveStreamsToCompletableFutureConverter implements GenericConve if (source == null) { return null; } - else if (CompletableFuture.class.isAssignableFrom(source.getClass())) { + else if (CompletableFuture.class.isAssignableFrom(sourceType.getType())) { return Mono.fromFuture((CompletableFuture) source); } - else if (CompletableFuture.class.isAssignableFrom(targetType.getResolvableType().getRawClass())) { + else if (CompletableFuture.class.isAssignableFrom(targetType.getType())) { return Mono.from((Publisher) source).toFuture(); } return null; diff --git a/spring-web-reactive/src/main/java/org/springframework/core/convert/support/PublisherToFluxConverter.java b/spring-web-reactive/src/main/java/org/springframework/core/convert/support/PublisherToFluxConverter.java new file mode 100644 index 0000000000..6867245c03 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/core/convert/support/PublisherToFluxConverter.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2015 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.convert.support; + +import java.util.LinkedHashSet; +import java.util.Set; + +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.GenericConverter; + +/** + * @author Sebastien Deleuze + */ +public class PublisherToFluxConverter implements GenericConverter { + + @Override + public Set getConvertibleTypes() { + Set pairs = new LinkedHashSet<>(); + pairs.add(new ConvertiblePair(Publisher.class, Flux.class)); + return pairs; + } + + @Override + public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (source == null) { + return null; + } + else if (Publisher.class.isAssignableFrom(sourceType.getType())) { + return Flux.from((Publisher)source); + } + return null; + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToRxJava1Converter.java b/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactorToRxJava1Converter.java similarity index 74% rename from spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToRxJava1Converter.java rename to spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactorToRxJava1Converter.java index a9f6c0557c..36c58916e7 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactiveStreamsToRxJava1Converter.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactorToRxJava1Converter.java @@ -22,6 +22,8 @@ import java.util.Set; import org.reactivestreams.Publisher; import reactor.core.converter.RxJava1ObservableConverter; import reactor.core.converter.RxJava1SingleConverter; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import rx.Observable; import rx.Single; @@ -29,18 +31,17 @@ import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.GenericConverter; /** - * TODO Avoid classpath exception for older RxJava1 version without Single type * @author Stephane Maldini * @author Sebastien Deleuze */ -public final class ReactiveStreamsToRxJava1Converter implements GenericConverter { +public final class ReactorToRxJava1Converter implements GenericConverter { @Override public Set getConvertibleTypes() { Set pairs = new LinkedHashSet<>(); - pairs.add(new GenericConverter.ConvertiblePair(Publisher.class, Observable.class)); + pairs.add(new GenericConverter.ConvertiblePair(Flux.class, Observable.class)); pairs.add(new GenericConverter.ConvertiblePair(Observable.class, Publisher.class)); - pairs.add(new GenericConverter.ConvertiblePair(Publisher.class, Single.class)); + pairs.add(new GenericConverter.ConvertiblePair(Mono.class, Single.class)); pairs.add(new GenericConverter.ConvertiblePair(Single.class, Publisher.class)); return pairs; } @@ -50,16 +51,16 @@ public final class ReactiveStreamsToRxJava1Converter implements GenericConverter if (source == null) { return null; } - if (Observable.class.isAssignableFrom(source.getClass())) { + if (Observable.class.isAssignableFrom(sourceType.getType())) { return RxJava1ObservableConverter.from((Observable) source); } - else if (Observable.class.isAssignableFrom(targetType.getResolvableType().getRawClass())) { + else if (Observable.class.isAssignableFrom(targetType.getType())) { return RxJava1ObservableConverter.from((Publisher) source); } - else if (Single.class.isAssignableFrom(source.getClass())) { + else if (Single.class.isAssignableFrom(sourceType.getType())) { return RxJava1SingleConverter.from((Single) source); } - else if (Single.class.isAssignableFrom(targetType.getResolvableType().getRawClass())) { + else if (Single.class.isAssignableFrom(targetType.getType())) { return RxJava1SingleConverter.from((Publisher) source); } return null; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java index 51eb9234c4..ccf0e53af5 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java @@ -32,6 +32,7 @@ import org.springframework.core.codec.Decoder; import org.springframework.core.codec.Encoder; import org.springframework.core.codec.ByteBufferDecoder; import org.springframework.core.codec.ByteBufferEncoder; +import org.springframework.core.convert.support.PublisherToFluxConverter; import org.springframework.http.codec.json.JacksonJsonDecoder; import org.springframework.http.codec.json.JacksonJsonEncoder; import org.springframework.http.codec.xml.Jaxb2Decoder; @@ -41,8 +42,8 @@ import org.springframework.core.codec.StringEncoder; import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.ConverterRegistry; import org.springframework.core.convert.support.GenericConversionService; -import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter; -import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter; +import org.springframework.core.convert.support.MonoToCompletableFutureConverter; +import org.springframework.core.convert.support.ReactorToRxJava1Converter; import org.springframework.format.Formatter; import org.springframework.http.MediaType; import org.springframework.http.codec.SseEventEncoder; @@ -285,14 +286,15 @@ public class WebReactiveConfiguration implements ApplicationContextAware { * Override to add custom {@link Converter}s and {@link Formatter}s. *

    By default this method method registers: *

      - *
    • {@link ReactiveStreamsToCompletableFutureConverter} - *
    • {@link ReactiveStreamsToRxJava1Converter} + *
    • {@link MonoToCompletableFutureConverter} + *
    • {@link ReactorToRxJava1Converter} *
    */ protected void addFormatters(ConverterRegistry registry) { - registry.addConverter(new ReactiveStreamsToCompletableFutureConverter()); + registry.addConverter(new MonoToCompletableFutureConverter()); + registry.addConverter(new PublisherToFluxConverter()); if (DependencyUtils.hasRxJava1()) { - registry.addConverter(new ReactiveStreamsToRxJava1Converter()); + registry.addConverter(new ReactorToRxJava1Converter()); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/SimpleResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/SimpleResultHandlerTests.java index 52ad7271dd..1256f96bd4 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/SimpleResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/SimpleResultHandlerTests.java @@ -24,13 +24,12 @@ import reactor.core.publisher.Flux; import rx.Observable; import org.springframework.core.ResolvableType; -import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.convert.support.GenericConversionService; -import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter; -import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter; +import org.springframework.core.convert.support.MonoToCompletableFutureConverter; +import org.springframework.core.convert.support.PublisherToFluxConverter; +import org.springframework.core.convert.support.ReactorToRxJava1Converter; import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerResult; -import org.springframework.web.reactive.result.SimpleResultHandler; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -46,8 +45,9 @@ public class SimpleResultHandlerTests { public void supportsWithConversionService() throws NoSuchMethodException { GenericConversionService conversionService = new GenericConversionService(); - conversionService.addConverter(new ReactiveStreamsToCompletableFutureConverter()); - conversionService.addConverter(new ReactiveStreamsToRxJava1Converter()); + conversionService.addConverter(new MonoToCompletableFutureConverter()); + conversionService.addConverter(new PublisherToFluxConverter()); + conversionService.addConverter(new ReactorToRxJava1Converter()); SimpleResultHandler resultHandler = new SimpleResultHandler(conversionService); TestController controller = new TestController(); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterResultHandlerTests.java index b0f15af7d0..62d065193f 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterResultHandlerTests.java @@ -40,8 +40,9 @@ import org.springframework.core.ResolvableType; import org.springframework.core.codec.ByteBufferEncoder; import org.springframework.core.codec.StringEncoder; import org.springframework.core.convert.support.GenericConversionService; -import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter; -import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter; +import org.springframework.core.convert.support.MonoToCompletableFutureConverter; +import org.springframework.core.convert.support.PublisherToFluxConverter; +import org.springframework.core.convert.support.ReactorToRxJava1Converter; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.core.io.buffer.support.DataBufferTestUtils; @@ -136,7 +137,6 @@ public class MessageConverterResultHandlerTests { } @Test // SPR-12811 - @Ignore public void jacksonTypeOfListElement() throws Exception { List body = Arrays.asList(new Foo("foo"), new Bar("bar")); ResolvableType bodyType = ResolvableType.forClassWithGenerics(List.class, ParentClass.class); @@ -185,8 +185,9 @@ public class MessageConverterResultHandlerTests { } GenericConversionService service = new GenericConversionService(); - service.addConverter(new ReactiveStreamsToCompletableFutureConverter()); - service.addConverter(new ReactiveStreamsToRxJava1Converter()); + service.addConverter(new MonoToCompletableFutureConverter()); + service.addConverter(new PublisherToFluxConverter()); + service.addConverter(new ReactorToRxJava1Converter()); RequestedContentTypeResolver resolver = new RequestedContentTypeResolverBuilder().build(); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java index b462d2f114..5e9ecb720f 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java @@ -30,7 +30,6 @@ import java.util.concurrent.CompletableFuture; import javax.xml.bind.annotation.XmlRootElement; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -43,11 +42,12 @@ import org.springframework.core.MethodParameter; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.annotation.SynthesizingMethodParameter; import org.springframework.core.codec.Decoder; +import org.springframework.core.convert.support.PublisherToFluxConverter; import org.springframework.http.codec.json.JacksonJsonDecoder; import org.springframework.core.codec.StringDecoder; import org.springframework.core.convert.support.GenericConversionService; -import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter; -import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter; +import org.springframework.core.convert.support.MonoToCompletableFutureConverter; +import org.springframework.core.convert.support.ReactorToRxJava1Converter; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.HttpMethod; @@ -169,10 +169,7 @@ public class RequestBodyArgumentResolverTests { assertEquals(map, resolveValue("map", Map.class, body)); } - // TODO: @Ignore - @Test - @Ignore public void list() throws Exception { String body = "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]"; assertEquals(Arrays.asList(new TestBean("f1", "b1"), new TestBean("f2", "b2")), @@ -180,7 +177,6 @@ public class RequestBodyArgumentResolverTests { } @Test - @Ignore public void array() throws Exception { String body = "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]"; assertArrayEquals(new TestBean[] {new TestBean("f1", "b1"), new TestBean("f2", "b2")}, @@ -220,8 +216,9 @@ public class RequestBodyArgumentResolverTests { List> converters = new ArrayList<>(); Arrays.asList(decoders).forEach(decoder -> converters.add(new CodecHttpMessageConverter<>(decoder))); GenericConversionService service = new GenericConversionService(); - service.addConverter(new ReactiveStreamsToCompletableFutureConverter()); - service.addConverter(new ReactiveStreamsToRxJava1Converter()); + service.addConverter(new MonoToCompletableFutureConverter()); + service.addConverter(new PublisherToFluxConverter()); + service.addConverter(new ReactorToRxJava1Converter()); return new RequestBodyArgumentResolver(converters, service, new TestBeanValidator()); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java index 3d3ac4f3aa..e99454f126 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java @@ -30,8 +30,9 @@ import org.springframework.core.codec.ByteBufferEncoder; import org.springframework.core.codec.StringEncoder; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.convert.support.GenericConversionService; -import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter; -import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter; +import org.springframework.core.convert.support.MonoToCompletableFutureConverter; +import org.springframework.core.convert.support.PublisherToFluxConverter; +import org.springframework.core.convert.support.ReactorToRxJava1Converter; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.http.codec.json.JacksonJsonEncoder; @@ -100,8 +101,9 @@ public class ResponseBodyResultHandlerTests { converterList = Arrays.asList(converters); } GenericConversionService service = new GenericConversionService(); - service.addConverter(new ReactiveStreamsToCompletableFutureConverter()); - service.addConverter(new ReactiveStreamsToRxJava1Converter()); + service.addConverter(new MonoToCompletableFutureConverter()); + service.addConverter(new PublisherToFluxConverter()); + service.addConverter(new ReactorToRxJava1Converter()); RequestedContentTypeResolver resolver = new RequestedContentTypeResolverBuilder().build(); return new ResponseBodyResultHandler(converterList, new DefaultConversionService(), resolver); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java index 7d01f3c9f8..e65e867a2c 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java @@ -33,8 +33,9 @@ import org.springframework.core.ResolvableType; import org.springframework.core.codec.ByteBufferEncoder; import org.springframework.core.codec.StringEncoder; import org.springframework.core.convert.support.GenericConversionService; -import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter; -import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter; +import org.springframework.core.convert.support.MonoToCompletableFutureConverter; +import org.springframework.core.convert.support.PublisherToFluxConverter; +import org.springframework.core.convert.support.ReactorToRxJava1Converter; import org.springframework.core.io.buffer.support.DataBufferTestUtils; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; @@ -105,8 +106,11 @@ public class ResponseEntityResultHandlerTests { converterList = Arrays.asList(converters); } GenericConversionService service = new GenericConversionService(); - service.addConverter(new ReactiveStreamsToCompletableFutureConverter()); - service.addConverter(new ReactiveStreamsToRxJava1Converter()); + service.addConverter(new MonoToCompletableFutureConverter()); + service.addConverter(new PublisherToFluxConverter()); + service.addConverter(new ReactorToRxJava1Converter()); + + RequestedContentTypeResolver resolver = new RequestedContentTypeResolverBuilder().build(); return new ResponseEntityResultHandler(converterList, service, resolver); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java index 0494253f61..f8f1276142 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java @@ -39,7 +39,8 @@ import org.springframework.core.Ordered; import org.springframework.core.ResolvableType; import org.springframework.core.convert.support.ConfigurableConversionService; import org.springframework.core.convert.support.DefaultConversionService; -import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter; +import org.springframework.core.convert.support.PublisherToFluxConverter; +import org.springframework.core.convert.support.ReactorToRxJava1Converter; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.core.io.buffer.support.DataBufferTestUtils; @@ -252,7 +253,8 @@ public class ViewResolutionResultHandlerTests { private ViewResolutionResultHandler createResultHandler(List defaultViews, ViewResolver... resolvers) { ConfigurableConversionService service = new DefaultConversionService(); - service.addConverter(new ReactiveStreamsToRxJava1Converter()); + service.addConverter(new ReactorToRxJava1Converter()); + service.addConverter(new PublisherToFluxConverter()); List resolverList = Arrays.asList(resolvers); ViewResolutionResultHandler handler = new ViewResolutionResultHandler(resolverList, service); handler.setDefaultViews(defaultViews); From 917a2fb9d00d08e95ac4ae5c3c9c6184569d4031 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Fri, 1 Jul 2016 17:01:29 +0200 Subject: [PATCH 448/498] Add Decoder#decodeOne() This commit adds a Decoder#decodeOne() method in order to handle correctly the streaming versus one value deserialization based on the type provided by the user. For example, if a List parameter is provided in a controller method, Jackson will be called once, while if the user provides a Flux or an Observable parameter, Jackson will be called for each element. --- .../core/codec/AbstractDecoder.java | 9 ++- .../springframework/core/codec/Decoder.java | 15 +++++ .../core/codec/StringDecoder.java | 16 ++++++ .../http/codec/json/JacksonJsonDecoder.java | 31 ++++++++++- .../reactive/CodecHttpMessageConverter.java | 12 ++++ .../reactive/HttpMessageConverter.java | 15 ++++- .../RequestBodyArgumentResolver.java | 55 ++++++++++++------- .../core/codec/StringDecoderTests.java | 15 +++++ .../codec/json/JacksonJsonDecoderTests.java | 8 +-- 9 files changed, 147 insertions(+), 29 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/AbstractDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/AbstractDecoder.java index 5e925c5ca3..1b7af181f0 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/AbstractDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/AbstractDecoder.java @@ -20,8 +20,11 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Mono; + import org.springframework.core.ResolvableType; -import org.springframework.core.codec.Decoder; +import org.springframework.core.io.buffer.DataBuffer; import org.springframework.util.MimeType; /** @@ -51,4 +54,8 @@ public abstract class AbstractDecoder implements Decoder { anyMatch(mt -> mt.isCompatibleWith(mimeType)); } + @Override + public Mono decodeOne(Publisher inputStream, ResolvableType elementType, MimeType mimeType, Object... hints) { + throw new UnsupportedOperationException(); + } } diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/Decoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/Decoder.java index e3b6804f95..bd5e611d9a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/Decoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/Decoder.java @@ -20,6 +20,7 @@ import java.util.List; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.DataBuffer; @@ -60,6 +61,20 @@ public interface Decoder { Flux decode(Publisher inputStream, ResolvableType elementType, MimeType mimeType, Object... hints); + /** + * Decode a {@link DataBuffer} input stream into a Mono of {@code T}. + * + * @param inputStream the {@code DataBuffer} input stream to decode + * @param elementType the expected type of elements in the output stream; + * this type must have been previously passed to the {@link #canDecode} + * method and it must have returned {@code true}. + * @param mimeType the MIME type associated with the input stream, optional + * @param hints additional information about how to do decode, optional + * @return the output stream with the decoded element + */ + Mono decodeOne(Publisher inputStream, ResolvableType elementType, + MimeType mimeType, Object... hints); + /** * Return the list of MIME types this decoder supports. */ diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/StringDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/StringDecoder.java index b5936aabca..7ff0f172e7 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/StringDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/StringDecoder.java @@ -25,6 +25,7 @@ import java.util.function.IntPredicate; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.DataBuffer; @@ -94,6 +95,21 @@ public class StringDecoder extends AbstractDecoder { }); } + @Override + public Mono decodeOne(Publisher inputStream, ResolvableType elementType, + MimeType mimeType, Object... hints) { + + Charset charset = getCharset(mimeType); + return Flux.from(inputStream) + .map(dataBuffer -> { + CharBuffer charBuffer = charset.decode(dataBuffer.asByteBuffer()); + DataBufferUtils.release(dataBuffer); + return charBuffer.toString(); + }) + .collect(StringBuilder::new, StringBuilder::append) + .map(StringBuilder::toString); + } + private static Flux splitOnNewline(DataBuffer dataBuffer) { List results = new ArrayList(); int startIdx = 0; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/codec/json/JacksonJsonDecoder.java b/spring-web-reactive/src/main/java/org/springframework/http/codec/json/JacksonJsonDecoder.java index 39bae68c96..a68189f72c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/codec/json/JacksonJsonDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/codec/json/JacksonJsonDecoder.java @@ -25,6 +25,7 @@ import com.fasterxml.jackson.databind.ObjectReader; import com.fasterxml.jackson.databind.type.TypeFactory; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import org.springframework.core.ResolvableType; import org.springframework.core.codec.CodecException; @@ -46,7 +47,9 @@ public class JacksonJsonDecoder extends AbstractDecoder { private final ObjectMapper mapper; - private final Decoder preProcessor = new JsonObjectDecoder(); + private final Decoder fluxPreProcessor = new JsonObjectDecoder(); + + private final Decoder monoPreProcessor = new JsonObjectDecoder(false); public JacksonJsonDecoder() { @@ -69,7 +72,31 @@ public class JacksonJsonDecoder extends AbstractDecoder { JavaType javaType = typeFactory.constructType(elementType.getType()); ObjectReader reader = this.mapper.readerFor(javaType); - return this.preProcessor.decode(inputStream, elementType, mimeType, hints) + return this.fluxPreProcessor.decode(inputStream, elementType, mimeType, hints) + .map(dataBuffer -> { + try { + Object value = reader.readValue(dataBuffer.asInputStream()); + DataBufferUtils.release(dataBuffer); + return value; + } + catch (IOException e) { + return Flux.error(new CodecException("Error while reading the data", e)); + } + }); + } + + @Override + public Mono decodeOne(Publisher inputStream, ResolvableType elementType, + MimeType mimeType, Object... hints) { + + Assert.notNull(inputStream, "'inputStream' must not be null"); + Assert.notNull(elementType, "'elementType' must not be null"); + TypeFactory typeFactory = this.mapper.getTypeFactory(); + JavaType javaType = typeFactory.constructType(elementType.getType()); + ObjectReader reader = this.mapper.readerFor(javaType); + + return this.monoPreProcessor.decode(inputStream, elementType, mimeType, hints) + .single() .map(dataBuffer -> { try { Object value = reader.readValue(dataBuffer.asInputStream()); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/CodecHttpMessageConverter.java b/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/CodecHttpMessageConverter.java index f61648784e..eabf0a5407 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/CodecHttpMessageConverter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/CodecHttpMessageConverter.java @@ -125,6 +125,18 @@ public class CodecHttpMessageConverter implements HttpMessageConverter { return this.decoder.decode(inputMessage.getBody(), type, contentType); } + @Override + public Mono readOne(ResolvableType type, ReactiveHttpInputMessage inputMessage) { + if (this.decoder == null) { + return Mono.error(new IllegalStateException("No decoder set")); + } + MediaType contentType = inputMessage.getHeaders().getContentType(); + if (contentType == null) { + contentType = MediaType.APPLICATION_OCTET_STREAM; + } + return this.decoder.decodeOne(inputMessage.getBody(), type, contentType); + } + @Override public Mono write(Publisher inputStream, ResolvableType type, MediaType contentType, ReactiveHttpOutputMessage outputMessage) { diff --git a/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/HttpMessageConverter.java b/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/HttpMessageConverter.java index ce001c5d08..0f663457fa 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/HttpMessageConverter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/HttpMessageConverter.java @@ -50,16 +50,27 @@ public interface HttpMessageConverter { List getReadableMediaTypes(); /** - * Read an object of the given type form the given input message, and returns it. + * Read a {@link Flux} of the given type form the given input message, and returns it. * @param type the type of object to return. This type must have previously been * passed to the * {@link #canRead canRead} method of this interface, which must have returned {@code * true}. * @param inputMessage the HTTP input message to read from - * @return the converted object + * @return the converted {@link Flux} of elements */ Flux read(ResolvableType type, ReactiveHttpInputMessage inputMessage); + /** + * Read a {@link Mono} of the given type form the given input message, and returns it. + * @param type the type of object to return. This type must have previously been + * passed to the + * {@link #canRead canRead} method of this interface, which must have returned {@code + * true}. + * @param inputMessage the HTTP input message to read from + * @return the converted {@link Mono} of object + */ + Mono readOne(ResolvableType type, ReactiveHttpInputMessage inputMessage); + /** * Indicates whether the given class can be written by this converter. * @param type the class to test for writability diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java index b58123dadd..3f72586ea8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java @@ -23,6 +23,7 @@ import java.util.stream.Collectors; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import rx.Observable; import org.springframework.core.Conventions; import org.springframework.core.MethodParameter; @@ -122,7 +123,8 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve ResolvableType type = ResolvableType.forMethodParameter(parameter); boolean isAsyncType = isAsyncType(type); - ResolvableType elementType = (isAsyncType ? type.getGeneric(0) : type); + boolean isStreamableType = isStreamableType(type); + ResolvableType elementType = (isStreamableType || isAsyncType ? type.getGeneric(0) : type); MediaType mediaType = exchange.getRequest().getHeaders().getContentType(); if (mediaType == null) { @@ -131,23 +133,33 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve for (HttpMessageConverter converter : getMessageConverters()) { if (converter.canRead(elementType, mediaType)) { - Flux elementFlux = converter.read(elementType, exchange.getRequest()); - if (this.validator != null) { - elementFlux= applyValidationIfApplicable(elementFlux, parameter); - } - - if (Mono.class.equals(type.getRawClass())) { - return Mono.just(Mono.from(elementFlux)); - } - else if (Flux.class.equals(type.getRawClass())) { - return Mono.just(elementFlux); - } - else if (isAsyncType) { - return Mono.just(getConversionService().convert(elementFlux, type.getRawClass())); + if (isStreamableType) { + Publisher elements = converter.read(elementType, exchange.getRequest()); + if (this.validator != null) { + elements= applyValidationIfApplicable(elements, parameter); + } + if (Flux.class.equals(type.getRawClass())) { + return Mono.just(elements); + } + else if (isAsyncType && this.conversionService.canConvert(Flux.class, type.getRawClass())) { + return Mono.just(this.conversionService.convert(elements, type.getRawClass())); + } } else { - return elementFlux.next().map(o -> o); + Mono element = converter.readOne(elementType, exchange.getRequest()); + if (this.validator != null) { + element = Mono.from(applyValidationIfApplicable(element, parameter)); + } + if (Mono.class.equals(type.getRawClass())) { + return Mono.just(element); + } + else if (isAsyncType && this.conversionService.canConvert(Mono.class, type.getRawClass())) { + return Mono.just(this.conversionService.convert(element, type.getRawClass())); + } + else { + return (Mono)element; + } } } } @@ -157,23 +169,28 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve private boolean isAsyncType(ResolvableType type) { return (Mono.class.equals(type.getRawClass()) || Flux.class.equals(type.getRawClass()) || - getConversionService().canConvert(Publisher.class, type.getRawClass())); + getConversionService().canConvert(Mono.class, type.getRawClass()) || + getConversionService().canConvert(Flux.class, type.getRawClass())); } - protected Flux applyValidationIfApplicable(Flux elementFlux, MethodParameter methodParam) { + private boolean isStreamableType(ResolvableType type) { + return this.conversionService.canConvert(Flux.class, type.getRawClass()); + } + + protected Publisher applyValidationIfApplicable(Publisher elements, MethodParameter methodParam) { Annotation[] annotations = methodParam.getParameterAnnotations(); for (Annotation ann : annotations) { Validated validAnnot = AnnotationUtils.getAnnotation(ann, Validated.class); if (validAnnot != null || ann.annotationType().getSimpleName().startsWith("Valid")) { Object hints = (validAnnot != null ? validAnnot.value() : AnnotationUtils.getValue(ann)); Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); - return elementFlux.map(element -> { + return Flux.from(elements).map(element -> { validate(element, validationHints, methodParam); return element; }); } } - return elementFlux; + return elements; } /** diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/StringDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/StringDecoderTests.java index 505dca462f..adce87826a 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/StringDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/StringDecoderTests.java @@ -19,6 +19,7 @@ package org.springframework.core.codec; import org.junit.Before; import org.junit.Test; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import reactor.core.test.TestSubscriber; import org.springframework.core.ResolvableType; @@ -96,4 +97,18 @@ public class StringDecoderTests extends AbstractDataBufferAllocatingTestCase { .assertValues(""); } + @Test + public void decodeOne() throws InterruptedException { + this.decoder = new StringDecoder(false); + Flux source = + Flux.just(stringBuffer("foo"), stringBuffer("bar"), stringBuffer("baz")); + Mono output = + this.decoder.decodeOne(source, ResolvableType.forClass(String.class), null); + TestSubscriber + .subscribe(output) + .assertNoError() + .assertComplete() + .assertValues("foobarbaz"); + } + } diff --git a/spring-web-reactive/src/test/java/org/springframework/http/codec/json/JacksonJsonDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/http/codec/json/JacksonJsonDecoderTests.java index 54f99e66e7..0321bd2347 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/codec/json/JacksonJsonDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/codec/json/JacksonJsonDecoderTests.java @@ -20,9 +20,9 @@ import java.lang.reflect.Method; import java.util.Arrays; import java.util.List; -import org.junit.Ignore; import org.junit.Test; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import reactor.core.test.TestSubscriber; import org.springframework.core.ResolvableType; @@ -30,7 +30,6 @@ import org.springframework.core.io.buffer.AbstractDataBufferAllocatingTestCase; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.MediaType; import org.springframework.http.codec.Pojo; -import org.springframework.http.codec.json.JacksonJsonDecoder; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -61,16 +60,15 @@ public class JacksonJsonDecoderTests extends AbstractDataBufferAllocatingTestCas } @Test - @Ignore // Issue 109 public void decodeToList() throws Exception { Flux source = Flux.just(stringBuffer( "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]")); Method method = getClass().getDeclaredMethod("handle", List.class); ResolvableType elementType = ResolvableType.forMethodParameter(method, 0); - Flux flux = new JacksonJsonDecoder().decode(source, elementType, null); + Mono mono = new JacksonJsonDecoder().decodeOne(source, elementType, null); - TestSubscriber.subscribe(flux).assertNoError().assertComplete(). + TestSubscriber.subscribe(mono).assertNoError().assertComplete(). assertValues(Arrays.asList(new Pojo("f1", "b1"), new Pojo("f2", "b2"))); } From a68ff94fbc323b70115750ad12dbc4b1c8e14657 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 1 Jul 2016 15:59:29 -0400 Subject: [PATCH 449/498] Polish "decodeOne" related changes --- .../core/codec/StringDecoder.java | 39 ++++++------- .../http/codec/json/JacksonJsonDecoder.java | 52 ++++++++--------- .../reactive/CodecHttpMessageConverter.java | 17 +++--- .../RequestBodyArgumentResolver.java | 56 +++++++------------ 4 files changed, 74 insertions(+), 90 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/StringDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/StringDecoder.java index 7ff0f172e7..3c293029bd 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/StringDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/StringDecoder.java @@ -52,8 +52,10 @@ public class StringDecoder extends AbstractDecoder { private static final IntPredicate NEWLINE_DELIMITER = b -> b == '\n' || b == '\r'; + private final boolean splitOnNewline; + /** * Create a {@code StringDecoder} that decodes a bytes stream to a String stream * @@ -74,6 +76,7 @@ public class StringDecoder extends AbstractDecoder { this.splitOnNewline = splitOnNewline; } + @Override public boolean canDecode(ResolvableType elementType, MimeType mimeType, Object... hints) { return super.canDecode(elementType, mimeType, hints) && @@ -83,37 +86,27 @@ public class StringDecoder extends AbstractDecoder { @Override public Flux decode(Publisher inputStream, ResolvableType elementType, MimeType mimeType, Object... hints) { + Flux inputFlux = Flux.from(inputStream); if (this.splitOnNewline) { - inputFlux = inputFlux.flatMap(StringDecoder::splitOnNewline); + inputFlux = Flux.from(inputStream).flatMap(StringDecoder::splitOnNewline); } - Charset charset = getCharset(mimeType); - return inputFlux.map(dataBuffer -> { - CharBuffer charBuffer = charset.decode(dataBuffer.asByteBuffer()); - DataBufferUtils.release(dataBuffer); - return charBuffer.toString(); - }); + return decodeInternal(inputFlux, mimeType); } @Override public Mono decodeOne(Publisher inputStream, ResolvableType elementType, MimeType mimeType, Object... hints) { - Charset charset = getCharset(mimeType); - return Flux.from(inputStream) - .map(dataBuffer -> { - CharBuffer charBuffer = charset.decode(dataBuffer.asByteBuffer()); - DataBufferUtils.release(dataBuffer); - return charBuffer.toString(); - }) - .collect(StringBuilder::new, StringBuilder::append) - .map(StringBuilder::toString); + return decodeInternal(Flux.from(inputStream), mimeType). + collect(StringBuilder::new, StringBuilder::append). + map(StringBuilder::toString); } private static Flux splitOnNewline(DataBuffer dataBuffer) { - List results = new ArrayList(); + List results = new ArrayList<>(); int startIdx = 0; - int endIdx = 0; + int endIdx; final int limit = dataBuffer.readableByteCount(); do { endIdx = dataBuffer.indexOf(NEWLINE_DELIMITER, startIdx); @@ -126,7 +119,15 @@ public class StringDecoder extends AbstractDecoder { DataBufferUtils.release(dataBuffer); return Flux.fromIterable(results); } - + + private Flux decodeInternal(Flux inputFlux, MimeType mimeType) { + Charset charset = getCharset(mimeType); + return inputFlux.map(dataBuffer -> { + CharBuffer charBuffer = charset.decode(dataBuffer.asByteBuffer()); + DataBufferUtils.release(dataBuffer); + return charBuffer.toString(); + }); + } private Charset getCharset(MimeType mimeType) { if (mimeType != null && mimeType.getCharset() != null) { diff --git a/spring-web-reactive/src/main/java/org/springframework/http/codec/json/JacksonJsonDecoder.java b/spring-web-reactive/src/main/java/org/springframework/http/codec/json/JacksonJsonDecoder.java index a68189f72c..36b84d338b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/codec/json/JacksonJsonDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/codec/json/JacksonJsonDecoder.java @@ -29,7 +29,6 @@ import reactor.core.publisher.Mono; import org.springframework.core.ResolvableType; import org.springframework.core.codec.CodecException; -import org.springframework.core.codec.Decoder; import org.springframework.core.codec.AbstractDecoder; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.support.DataBufferUtils; @@ -38,18 +37,26 @@ import org.springframework.util.MimeType; /** - * Decode from a bytes stream of JSON objects to a stream of {@code Object} (POJO). + * Decode a byte stream into JSON and convert to Object's with Jackson. * * @author Sebastien Deleuze + * @author Rossen Stoyanchev + * * @see JacksonJsonEncoder */ public class JacksonJsonDecoder extends AbstractDecoder { + private static final MimeType[] MIME_TYPES = new MimeType[] { + new MimeType("application", "json", StandardCharsets.UTF_8), + new MimeType("application", "*+json", StandardCharsets.UTF_8) + }; + + private final ObjectMapper mapper; - private final Decoder fluxPreProcessor = new JsonObjectDecoder(); + private final JsonObjectDecoder fluxObjectDecoder = new JsonObjectDecoder(true); - private final Decoder monoPreProcessor = new JsonObjectDecoder(false); + private final JsonObjectDecoder monoObjectDecoder = new JsonObjectDecoder(false); public JacksonJsonDecoder() { @@ -57,46 +64,39 @@ public class JacksonJsonDecoder extends AbstractDecoder { } public JacksonJsonDecoder(ObjectMapper mapper) { - super(new MimeType("application", "json", StandardCharsets.UTF_8), - new MimeType("application", "*+json", StandardCharsets.UTF_8)); + super(MIME_TYPES); this.mapper = mapper; } + @Override public Flux decode(Publisher inputStream, ResolvableType elementType, MimeType mimeType, Object... hints) { - Assert.notNull(inputStream, "'inputStream' must not be null"); - Assert.notNull(elementType, "'elementType' must not be null"); - TypeFactory typeFactory = this.mapper.getTypeFactory(); - JavaType javaType = typeFactory.constructType(elementType.getType()); - ObjectReader reader = this.mapper.readerFor(javaType); - - return this.fluxPreProcessor.decode(inputStream, elementType, mimeType, hints) - .map(dataBuffer -> { - try { - Object value = reader.readValue(dataBuffer.asInputStream()); - DataBufferUtils.release(dataBuffer); - return value; - } - catch (IOException e) { - return Flux.error(new CodecException("Error while reading the data", e)); - } - }); + JsonObjectDecoder objectDecoder = this.fluxObjectDecoder; + return decodeInternal(objectDecoder, inputStream, elementType, mimeType, hints); } @Override public Mono decodeOne(Publisher inputStream, ResolvableType elementType, MimeType mimeType, Object... hints) { + JsonObjectDecoder objectDecoder = this.monoObjectDecoder; + return decodeInternal(objectDecoder, inputStream, elementType, mimeType, hints).single(); + } + + private Flux decodeInternal(JsonObjectDecoder objectDecoder, Publisher inputStream, + ResolvableType elementType, MimeType mimeType, Object[] hints) { + Assert.notNull(inputStream, "'inputStream' must not be null"); Assert.notNull(elementType, "'elementType' must not be null"); + TypeFactory typeFactory = this.mapper.getTypeFactory(); JavaType javaType = typeFactory.constructType(elementType.getType()); + ObjectReader reader = this.mapper.readerFor(javaType); - return this.monoPreProcessor.decode(inputStream, elementType, mimeType, hints) - .single() + return objectDecoder.decode(inputStream, elementType, mimeType, hints) .map(dataBuffer -> { try { Object value = reader.readValue(dataBuffer.asInputStream()); @@ -106,7 +106,7 @@ public class JacksonJsonDecoder extends AbstractDecoder { catch (IOException e) { return Flux.error(new CodecException("Error while reading the data", e)); } - }); + }); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/CodecHttpMessageConverter.java b/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/CodecHttpMessageConverter.java index eabf0a5407..60e4b3e3ea 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/CodecHttpMessageConverter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/CodecHttpMessageConverter.java @@ -118,10 +118,7 @@ public class CodecHttpMessageConverter implements HttpMessageConverter { if (this.decoder == null) { return Flux.error(new IllegalStateException("No decoder set")); } - MediaType contentType = inputMessage.getHeaders().getContentType(); - if (contentType == null) { - contentType = MediaType.APPLICATION_OCTET_STREAM; - } + MediaType contentType = getContentType(inputMessage); return this.decoder.decode(inputMessage.getBody(), type, contentType); } @@ -130,13 +127,16 @@ public class CodecHttpMessageConverter implements HttpMessageConverter { if (this.decoder == null) { return Mono.error(new IllegalStateException("No decoder set")); } - MediaType contentType = inputMessage.getHeaders().getContentType(); - if (contentType == null) { - contentType = MediaType.APPLICATION_OCTET_STREAM; - } + MediaType contentType = getContentType(inputMessage); return this.decoder.decodeOne(inputMessage.getBody(), type, contentType); } + private MediaType getContentType(ReactiveHttpInputMessage inputMessage) { + MediaType contentType = inputMessage.getHeaders().getContentType(); + return (contentType != null ? contentType : MediaType.APPLICATION_OCTET_STREAM); + } + + @Override public Mono write(Publisher inputStream, ResolvableType type, MediaType contentType, ReactiveHttpOutputMessage outputMessage) { @@ -181,6 +181,7 @@ public class CodecHttpMessageConverter implements HttpMessageConverter { * @param elementType the type of element for encoding * @return the content type, or {@code null} */ + @SuppressWarnings("UnusedParameters") protected MediaType getDefaultContentType(ResolvableType elementType) { return (!this.writableMediaTypes.isEmpty() ? this.writableMediaTypes.get(0) : null); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java index 3f72586ea8..8ab5cb6c6c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java @@ -23,7 +23,6 @@ import java.util.stream.Collectors; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import rx.Observable; import org.springframework.core.Conventions; import org.springframework.core.MethodParameter; @@ -32,6 +31,7 @@ import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.convert.ConversionService; import org.springframework.http.MediaType; import org.springframework.http.converter.reactive.HttpMessageConverter; +import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.ui.ModelMap; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -122,44 +122,36 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve ServerWebExchange exchange) { ResolvableType type = ResolvableType.forMethodParameter(parameter); - boolean isAsyncType = isAsyncType(type); - boolean isStreamableType = isStreamableType(type); - ResolvableType elementType = (isStreamableType || isAsyncType ? type.getGeneric(0) : type); - MediaType mediaType = exchange.getRequest().getHeaders().getContentType(); + boolean convertFromMono = getConversionService().canConvert(Mono.class, type.getRawClass()); + boolean convertFromFlux = getConversionService().canConvert(Flux.class, type.getRawClass()); + + ResolvableType elementType = convertFromMono || convertFromFlux ? type.getGeneric(0) : type; + + ServerHttpRequest request = exchange.getRequest(); + MediaType mediaType = request.getHeaders().getContentType(); if (mediaType == null) { mediaType = MediaType.APPLICATION_OCTET_STREAM; } for (HttpMessageConverter converter : getMessageConverters()) { if (converter.canRead(elementType, mediaType)) { - - if (isStreamableType) { - Publisher elements = converter.read(elementType, exchange.getRequest()); + if (convertFromFlux) { + Publisher flux = converter.read(elementType, request); if (this.validator != null) { - elements= applyValidationIfApplicable(elements, parameter); - } - if (Flux.class.equals(type.getRawClass())) { - return Mono.just(elements); - } - else if (isAsyncType && this.conversionService.canConvert(Flux.class, type.getRawClass())) { - return Mono.just(this.conversionService.convert(elements, type.getRawClass())); + flux= applyValidationIfApplicable(flux, parameter); } + return Mono.just(this.conversionService.convert(flux, type.getRawClass())); } else { - Mono element = converter.readOne(elementType, exchange.getRequest()); + Mono mono = converter.readOne(elementType, request); if (this.validator != null) { - element = Mono.from(applyValidationIfApplicable(element, parameter)); + mono = Mono.from(applyValidationIfApplicable(mono, parameter)); } - if (Mono.class.equals(type.getRawClass())) { - return Mono.just(element); - } - else if (isAsyncType && this.conversionService.canConvert(Mono.class, type.getRawClass())) { - return Mono.just(this.conversionService.convert(element, type.getRawClass())); - } - else { - return (Mono)element; + if (!convertFromMono) { + return mono.map(value-> value); // TODO: MonoToObjectConverter } + return Mono.just(this.conversionService.convert(mono, type.getRawClass())); } } } @@ -167,16 +159,6 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve return Mono.error(new UnsupportedMediaTypeStatusException(mediaType, this.supportedMediaTypes)); } - private boolean isAsyncType(ResolvableType type) { - return (Mono.class.equals(type.getRawClass()) || Flux.class.equals(type.getRawClass()) || - getConversionService().canConvert(Mono.class, type.getRawClass()) || - getConversionService().canConvert(Flux.class, type.getRawClass())); - } - - private boolean isStreamableType(ResolvableType type) { - return this.conversionService.canConvert(Flux.class, type.getRawClass()); - } - protected Publisher applyValidationIfApplicable(Publisher elements, MethodParameter methodParam) { Annotation[] annotations = methodParam.getParameterAnnotations(); for (Annotation ann : annotations) { @@ -185,7 +167,7 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve Object hints = (validAnnot != null ? validAnnot.value() : AnnotationUtils.getValue(ann)); Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); return Flux.from(elements).map(element -> { - validate(element, validationHints, methodParam); + doValidate(element, validationHints, methodParam); return element; }); } @@ -196,7 +178,7 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve /** * TODO: replace with use of DataBinder */ - private void validate(Object target, Object[] validationHints, MethodParameter methodParam) { + private void doValidate(Object target, Object[] validationHints, MethodParameter methodParam) { String name = Conventions.getVariableNameForParameter(methodParam); Errors errors = new BeanPropertyBindingResult(target, name); if (!ObjectUtils.isEmpty(validationHints) && this.validator instanceof SmartValidator) { From 71f4dff011e7dad78b0b5f4f7fc0bbbcdfbd2c0a Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 1 Jul 2016 16:16:21 -0400 Subject: [PATCH 450/498] Polish validation in RequestBodyArgumentResolver --- .../RequestBodyArgumentResolver.java | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java index 8ab5cb6c6c..3a7370778e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java @@ -18,9 +18,9 @@ package org.springframework.web.reactive.result.method.annotation; import java.lang.annotation.Annotation; import java.util.List; +import java.util.function.Function; import java.util.stream.Collectors; -import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -137,16 +137,16 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve for (HttpMessageConverter converter : getMessageConverters()) { if (converter.canRead(elementType, mediaType)) { if (convertFromFlux) { - Publisher flux = converter.read(elementType, request); + Flux flux = converter.read(elementType, request); if (this.validator != null) { - flux= applyValidationIfApplicable(flux, parameter); + flux = flux.map(applyValidationIfApplicable(parameter)); } return Mono.just(this.conversionService.convert(flux, type.getRawClass())); } else { Mono mono = converter.readOne(elementType, request); if (this.validator != null) { - mono = Mono.from(applyValidationIfApplicable(mono, parameter)); + mono = mono.map(applyValidationIfApplicable(parameter)); } if (!convertFromMono) { return mono.map(value-> value); // TODO: MonoToObjectConverter @@ -159,20 +159,20 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve return Mono.error(new UnsupportedMediaTypeStatusException(mediaType, this.supportedMediaTypes)); } - protected Publisher applyValidationIfApplicable(Publisher elements, MethodParameter methodParam) { + protected Function applyValidationIfApplicable(MethodParameter methodParam) { Annotation[] annotations = methodParam.getParameterAnnotations(); for (Annotation ann : annotations) { Validated validAnnot = AnnotationUtils.getAnnotation(ann, Validated.class); if (validAnnot != null || ann.annotationType().getSimpleName().startsWith("Valid")) { Object hints = (validAnnot != null ? validAnnot.value() : AnnotationUtils.getValue(ann)); - Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); - return Flux.from(elements).map(element -> { - doValidate(element, validationHints, methodParam); + Object[] validHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); + return element -> { + doValidate(element, validHints, methodParam); return element; - }); + }; } } - return elements; + return element -> element; } /** From df64262db6e6dbca68e0f0a572acfe75b0d488ce Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 1 Jul 2016 17:28:20 -0400 Subject: [PATCH 451/498] Complete reactive conversion support refactoring This commit ensures stream semantics (Flux vs Mono) are adhered to also on the target side. --- .../MonoToCompletableFutureConverter.java | 2 +- .../support/ReactorToRxJava1Converter.java | 4 +- .../reactive/result/SimpleResultHandler.java | 21 ++++++- ...AbstractMessageConverterResultHandler.java | 26 ++++---- ...MonoToCompletableFutureConverterTests.java | 58 +++++++++++++++++ .../ReactorToRxJava1ConverterTests.java | 62 +++++++++++++++++++ 6 files changed, 156 insertions(+), 17 deletions(-) create mode 100644 spring-web-reactive/src/test/java/org/springframework/core/convert/support/MonoToCompletableFutureConverterTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/core/convert/support/ReactorToRxJava1ConverterTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/core/convert/support/MonoToCompletableFutureConverter.java b/spring-web-reactive/src/main/java/org/springframework/core/convert/support/MonoToCompletableFutureConverter.java index bd3a06869f..5e4e4453f4 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/convert/support/MonoToCompletableFutureConverter.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/convert/support/MonoToCompletableFutureConverter.java @@ -35,7 +35,7 @@ public class MonoToCompletableFutureConverter implements GenericConverter { public Set getConvertibleTypes() { Set pairs = new LinkedHashSet<>(); pairs.add(new GenericConverter.ConvertiblePair(Mono.class, CompletableFuture.class)); - pairs.add(new GenericConverter.ConvertiblePair(CompletableFuture.class, Publisher.class)); + pairs.add(new GenericConverter.ConvertiblePair(CompletableFuture.class, Mono.class)); return pairs; } diff --git a/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactorToRxJava1Converter.java b/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactorToRxJava1Converter.java index 36c58916e7..9b7a661e21 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactorToRxJava1Converter.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactorToRxJava1Converter.java @@ -40,9 +40,9 @@ public final class ReactorToRxJava1Converter implements GenericConverter { public Set getConvertibleTypes() { Set pairs = new LinkedHashSet<>(); pairs.add(new GenericConverter.ConvertiblePair(Flux.class, Observable.class)); - pairs.add(new GenericConverter.ConvertiblePair(Observable.class, Publisher.class)); + pairs.add(new GenericConverter.ConvertiblePair(Observable.class, Flux.class)); pairs.add(new GenericConverter.ConvertiblePair(Mono.class, Single.class)); - pairs.add(new GenericConverter.ConvertiblePair(Single.class, Publisher.class)); + pairs.add(new GenericConverter.ConvertiblePair(Single.class, Mono.class)); return pairs; } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/SimpleResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/SimpleResultHandler.java index 7f24effc08..3b4818a4ed 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/SimpleResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/SimpleResultHandler.java @@ -19,6 +19,7 @@ package org.springframework.web.reactive.result; import java.util.Optional; import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.core.Ordered; @@ -53,6 +54,13 @@ public class SimpleResultHandler implements Ordered, HandlerResultHandler { } + /** + * Return the configured {@link ConversionService}. + */ + public ConversionService getConversionService() { + return this.conversionService; + } + /** * Set the order for this result handler relative to others. *

    By default this is set to {@link Ordered#LOWEST_PRECEDENCE} and is @@ -76,7 +84,8 @@ public class SimpleResultHandler implements Ordered, HandlerResultHandler { if (Void.TYPE.equals(type.getRawClass())) { return true; } - if (this.conversionService.canConvert(type.getRawClass(), Publisher.class)) { + if (getConversionService().canConvert(type.getRawClass(), Mono.class) || + getConversionService().canConvert(type.getRawClass(), Flux.class)) { Class clazz = result.getReturnValueType().getGeneric(0).getRawClass(); return Void.class.equals(clazz); } @@ -90,11 +99,19 @@ public class SimpleResultHandler implements Ordered, HandlerResultHandler { if (!optional.isPresent()) { return Mono.empty(); } + Object returnValue = optional.get(); if (returnValue instanceof Mono) { return (Mono) returnValue; } - return Mono.from(this.conversionService.convert(returnValue, Publisher.class)); + + ResolvableType returnType = result.getReturnValueType(); + if (getConversionService().canConvert(returnType.getRawClass(), Mono.class)) { + return this.conversionService.convert(returnValue, Mono.class); + } + else { + return this.conversionService.convert(returnValue, Flux.class).single(); + } } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageConverterResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageConverterResultHandler.java index 4394b698a9..cf66171006 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageConverterResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageConverterResultHandler.java @@ -72,21 +72,23 @@ public abstract class AbstractMessageConverterResultHandler extends ContentNegot @SuppressWarnings("unchecked") protected Mono writeBody(ServerWebExchange exchange, Object body, ResolvableType bodyType) { - Publisher publisher; - ResolvableType elementType; + boolean convertToFlux = getConversionService().canConvert(bodyType.getRawClass(), Flux.class); + boolean convertToMono = getConversionService().canConvert(bodyType.getRawClass(), Mono.class); - if (getConversionService().canConvert(bodyType.getRawClass(), Publisher.class)) { - if (body != null) { - publisher = getConversionService().convert(body, Publisher.class); - } - else { - publisher = Mono.empty(); - } - elementType = bodyType.getGeneric(0); + ResolvableType elementType = convertToFlux || convertToMono ? bodyType.getGeneric(0) : bodyType; + + Publisher publisher; + if (body == null) { + publisher = Mono.empty(); + } + else if (convertToMono) { + publisher = getConversionService().convert(body, Mono.class); + } + else if (convertToFlux) { + publisher = getConversionService().convert(body, Flux.class); } else { - publisher = Mono.justOrEmpty(body); - elementType = bodyType; + publisher = Mono.just(body); } if (Void.class.equals(elementType.getRawClass())) { diff --git a/spring-web-reactive/src/test/java/org/springframework/core/convert/support/MonoToCompletableFutureConverterTests.java b/spring-web-reactive/src/test/java/org/springframework/core/convert/support/MonoToCompletableFutureConverterTests.java new file mode 100644 index 0000000000..c62ee2b12e --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/core/convert/support/MonoToCompletableFutureConverterTests.java @@ -0,0 +1,58 @@ +/* + * 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.convert.support; + +import java.util.concurrent.CompletableFuture; + +import org.junit.Before; +import org.junit.Test; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import rx.Observable; +import rx.Single; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * Unit tests for {@link ReactorToRxJava1Converter}. + * @author Rossen Stoyanchev + */ +public class MonoToCompletableFutureConverterTests { + + private GenericConversionService conversionService; + + + @Before + public void setUp() throws Exception { + this.conversionService = new GenericConversionService(); + this.conversionService.addConverter(new MonoToCompletableFutureConverter()); + } + + @Test + public void canConvert() throws Exception { + assertTrue(this.conversionService.canConvert(Mono.class, CompletableFuture.class)); + assertTrue(this.conversionService.canConvert(CompletableFuture.class, Mono.class)); + + assertFalse(this.conversionService.canConvert(Flux.class, CompletableFuture.class)); + assertFalse(this.conversionService.canConvert(CompletableFuture.class, Flux.class)); + + assertFalse(this.conversionService.canConvert(Publisher.class, CompletableFuture.class)); + assertFalse(this.conversionService.canConvert(CompletableFuture.class, Publisher.class)); + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/core/convert/support/ReactorToRxJava1ConverterTests.java b/spring-web-reactive/src/test/java/org/springframework/core/convert/support/ReactorToRxJava1ConverterTests.java new file mode 100644 index 0000000000..bc6d2ac29a --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/core/convert/support/ReactorToRxJava1ConverterTests.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.convert.support; + +import org.junit.Before; +import org.junit.Test; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import rx.Observable; +import rx.Single; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * Unit tests for {@link ReactorToRxJava1Converter}. + * @author Rossen Stoyanchev + */ +public class ReactorToRxJava1ConverterTests { + + private GenericConversionService conversionService; + + + @Before + public void setUp() throws Exception { + this.conversionService = new GenericConversionService(); + this.conversionService.addConverter(new ReactorToRxJava1Converter()); + } + + @Test + public void canConvert() throws Exception { + assertTrue(this.conversionService.canConvert(Flux.class, Observable.class)); + assertTrue(this.conversionService.canConvert(Observable.class, Flux.class)); + + assertTrue(this.conversionService.canConvert(Mono.class, Single.class)); + assertTrue(this.conversionService.canConvert(Single.class, Mono.class)); + + assertFalse(this.conversionService.canConvert(Flux.class, Single.class)); + assertFalse(this.conversionService.canConvert(Single.class, Flux.class)); + + assertFalse(this.conversionService.canConvert(Mono.class, Observable.class)); + assertFalse(this.conversionService.canConvert(Observable.class, Mono.class)); + + assertFalse(this.conversionService.canConvert(Publisher.class, Observable.class)); + assertFalse(this.conversionService.canConvert(Observable.class, Publisher.class)); + } + +} From dffd6d674ab9415caed1b82755634f52383cd7ba Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 1 Jul 2016 18:00:31 -0400 Subject: [PATCH 452/498] Shorten returnValueType name in HandlerResult --- .../web/reactive/HandlerResult.java | 20 +++++++++---------- .../reactive/result/SimpleResultHandler.java | 7 +++---- .../annotation/ResponseBodyResultHandler.java | 4 ++-- .../ResponseEntityResultHandler.java | 6 +++--- .../view/ViewResolutionResultHandler.java | 6 +++--- 5 files changed, 21 insertions(+), 22 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java index 3c211e5644..437283c22e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java @@ -37,7 +37,7 @@ public class HandlerResult { private final Optional returnValue; - private final ResolvableType returnValueType; + private final ResolvableType returnType; private final ModelMap model; @@ -48,25 +48,25 @@ public class HandlerResult { * Create a new {@code HandlerResult}. * @param handler the handler that handled the request * @param returnValue the return value from the handler possibly {@code null} - * @param returnValueType the return value type + * @param returnType the return value type */ - public HandlerResult(Object handler, Object returnValue, ResolvableType returnValueType) { - this(handler, returnValue, returnValueType, null); + public HandlerResult(Object handler, Object returnValue, ResolvableType returnType) { + this(handler, returnValue, returnType, null); } /** * Create a new {@code HandlerResult}. * @param handler the handler that handled the request * @param returnValue the return value from the handler possibly {@code null} - * @param returnValueType the return value type + * @param returnType the return value type * @param model the model used for request handling */ - public HandlerResult(Object handler, Object returnValue, ResolvableType returnValueType, ModelMap model) { + public HandlerResult(Object handler, Object returnValue, ResolvableType returnType, ModelMap model) { Assert.notNull(handler, "'handler' is required"); - Assert.notNull(returnValueType, "'returnValueType' is required"); + Assert.notNull(returnType, "'returnType' is required"); this.handler = handler; this.returnValue = Optional.ofNullable(returnValue); - this.returnValueType = returnValueType; + this.returnType = returnType; this.model = (model != null ? model : new ExtendedModelMap()); } @@ -88,8 +88,8 @@ public class HandlerResult { /** * Return the type of the value returned from the handler. */ - public ResolvableType getReturnValueType() { - return this.returnValueType; + public ResolvableType getReturnType() { + return this.returnType; } /** diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/SimpleResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/SimpleResultHandler.java index 3b4818a4ed..bff817cc2f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/SimpleResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/SimpleResultHandler.java @@ -18,7 +18,6 @@ package org.springframework.web.reactive.result; import java.util.Optional; -import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -80,13 +79,13 @@ public class SimpleResultHandler implements Ordered, HandlerResultHandler { @Override public boolean supports(HandlerResult result) { - ResolvableType type = result.getReturnValueType(); + ResolvableType type = result.getReturnType(); if (Void.TYPE.equals(type.getRawClass())) { return true; } if (getConversionService().canConvert(type.getRawClass(), Mono.class) || getConversionService().canConvert(type.getRawClass(), Flux.class)) { - Class clazz = result.getReturnValueType().getGeneric(0).getRawClass(); + Class clazz = result.getReturnType().getGeneric(0).getRawClass(); return Void.class.equals(clazz); } return false; @@ -105,7 +104,7 @@ public class SimpleResultHandler implements Ordered, HandlerResultHandler { return (Mono) returnValue; } - ResolvableType returnType = result.getReturnValueType(); + ResolvableType returnType = result.getReturnType(); if (getConversionService().canConvert(returnType.getRawClass(), Mono.class)) { return this.conversionService.convert(returnValue, Mono.class); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java index e0ce99ee1c..4ac01a55b3 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java @@ -85,7 +85,7 @@ public class ResponseBodyResultHandler extends AbstractMessageConverterResultHan @Override public boolean supports(HandlerResult result) { - ResolvableType returnType = result.getReturnValueType(); + ResolvableType returnType = result.getReturnType(); if (returnType.getSource() instanceof MethodParameter) { MethodParameter parameter = (MethodParameter) returnType.getSource(); if (hasResponseBodyAnnotation(parameter) && !isHttpEntityType(returnType)) { @@ -118,7 +118,7 @@ public class ResponseBodyResultHandler extends AbstractMessageConverterResultHan @Override public Mono handleResult(ServerWebExchange exchange, HandlerResult result) { Object body = result.getReturnValue().orElse(null); - ResolvableType bodyType = result.getReturnValueType(); + ResolvableType bodyType = result.getReturnType(); return writeBody(exchange, body, bodyType); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java index 561bf0578d..5ebc8f8cfa 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java @@ -78,12 +78,12 @@ public class ResponseEntityResultHandler extends AbstractMessageConverterResultH @Override public boolean supports(HandlerResult result) { - ResolvableType returnType = result.getReturnValueType(); + ResolvableType returnType = result.getReturnType(); if (isSupportedType(returnType)) { return true; } else if (getConversionService().canConvert(returnType.getRawClass(), Mono.class)) { - ResolvableType genericType = result.getReturnValueType().getGeneric(0); + ResolvableType genericType = result.getReturnType().getGeneric(0); return isSupportedType(genericType); } @@ -99,7 +99,7 @@ public class ResponseEntityResultHandler extends AbstractMessageConverterResultH @Override public Mono handleResult(ServerWebExchange exchange, HandlerResult result) { - ResolvableType returnType = result.getReturnValueType(); + ResolvableType returnType = result.getReturnType(); Mono returnValueMono; ResolvableType bodyType; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java index bc28fbc543..2d94119c82 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java @@ -136,7 +136,7 @@ public class ViewResolutionResultHandler extends ContentNegotiatingResultHandler @Override public boolean supports(HandlerResult result) { - Class clazz = result.getReturnValueType().getRawClass(); + Class clazz = result.getReturnType().getRawClass(); if (hasModelAttributeAnnotation(result)) { return true; } @@ -144,7 +144,7 @@ public class ViewResolutionResultHandler extends ContentNegotiatingResultHandler return true; } if (getConversionService().canConvert(clazz, Mono.class)) { - clazz = result.getReturnValueType().getGeneric(0).getRawClass(); + clazz = result.getReturnType().getGeneric(0).getRawClass(); return isSupportedType(clazz); } return false; @@ -171,7 +171,7 @@ public class ViewResolutionResultHandler extends ContentNegotiatingResultHandler Mono valueMono; ResolvableType elementType; - ResolvableType returnType = result.getReturnValueType(); + ResolvableType returnType = result.getReturnType(); if (getConversionService().canConvert(returnType.getRawClass(), Mono.class)) { Optional optionalValue = result.getReturnValue(); From fb2e79604859a5b6fbc9b1c491caee8c3a9dbdcc Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Sat, 2 Jul 2016 12:21:35 -0400 Subject: [PATCH 453/498] HandlerResult now requires MethodParameter as input HandlerAdapter's should always be able to provide a MethodParameter which in turn ensures that HandlerResultHandler's have full type information from method declarations. This commit also introduces ResolvableMethod for use in tests to make it easy to obtain MethodParameter return types. Issue: #128 --- .../web/reactive/HandlerResult.java | 16 +- .../reactive/result/SimpleHandlerAdapter.java | 22 ++- .../result/method/InvocableHandlerMethod.java | 4 +- .../annotation/ResponseBodyResultHandler.java | 9 +- .../view/ViewResolutionResultHandler.java | 17 +- .../web/reactive/result/ResolvableMethod.java | 141 ++++++++++++++++ .../result/SimpleResultHandlerTests.java | 85 ++++------ .../ResponseBodyResultHandlerTests.java | 4 +- .../ResponseEntityResultHandlerTests.java | 78 ++++++--- .../view/HttpMessageConverterViewTests.java | 21 ++- .../view/UrlBasedViewResolverTests.java | 2 - .../ViewResolutionResultHandlerTests.java | 155 +++++++++++------- .../view/freemarker/FreeMarkerViewTests.java | 12 +- 13 files changed, 381 insertions(+), 185 deletions(-) create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/result/ResolvableMethod.java diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java index 437283c22e..f4416eda0e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java @@ -21,6 +21,7 @@ import java.util.function.Function; import reactor.core.publisher.Mono; +import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.ui.ExtendedModelMap; import org.springframework.ui.ModelMap; @@ -35,6 +36,7 @@ public class HandlerResult { private final Object handler; + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") private final Optional returnValue; private final ResolvableType returnType; @@ -50,7 +52,7 @@ public class HandlerResult { * @param returnValue the return value from the handler possibly {@code null} * @param returnType the return value type */ - public HandlerResult(Object handler, Object returnValue, ResolvableType returnType) { + public HandlerResult(Object handler, Object returnValue, MethodParameter returnType) { this(handler, returnValue, returnType, null); } @@ -61,12 +63,12 @@ public class HandlerResult { * @param returnType the return value type * @param model the model used for request handling */ - public HandlerResult(Object handler, Object returnValue, ResolvableType returnType, ModelMap model) { + public HandlerResult(Object handler, Object returnValue, MethodParameter returnType, ModelMap model) { Assert.notNull(handler, "'handler' is required"); Assert.notNull(returnType, "'returnType' is required"); this.handler = handler; this.returnValue = Optional.ofNullable(returnValue); - this.returnType = returnType; + this.returnType = ResolvableType.forMethodParameter(returnType); this.model = (model != null ? model : new ExtendedModelMap()); } @@ -92,6 +94,14 @@ public class HandlerResult { return this.returnType; } + /** + * Return the {@link MethodParameter} from which + * {@link #getReturnType() returnType} was created. + */ + public MethodParameter getReturnTypeSource() { + return (MethodParameter) this.returnType.getSource(); + } + /** * Return the model used during request handling with attributes that may be * used to render HTML templates with. diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/SimpleHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/SimpleHandlerAdapter.java index a4836426e8..0eb5712649 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/SimpleHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/SimpleHandlerAdapter.java @@ -16,14 +16,16 @@ package org.springframework.web.reactive.result; +import java.lang.reflect.Method; + import reactor.core.publisher.Mono; -import org.springframework.core.ResolvableType; +import org.springframework.core.MethodParameter; import org.springframework.web.reactive.DispatcherHandler; import org.springframework.web.reactive.HandlerAdapter; import org.springframework.web.reactive.HandlerResult; -import org.springframework.web.server.WebHandler; import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebHandler; /** * HandlerAdapter that allows using the plain {@link WebHandler} contract with @@ -34,8 +36,18 @@ import org.springframework.web.server.ServerWebExchange; */ public class SimpleHandlerAdapter implements HandlerAdapter { - private static final ResolvableType MONO_VOID = ResolvableType.forClassWithGenerics( - Mono.class, Void.class); + private static final MethodParameter RETURN_TYPE; + + static { + try { + Method method = WebHandler.class.getMethod("handle", ServerWebExchange.class); + RETURN_TYPE = new MethodParameter(method, -1); + } + catch (NoSuchMethodException ex) { + throw new IllegalStateException( + "Failed to initialize the return type for WebHandler: " + ex.getMessage()); + } + } @Override @@ -47,7 +59,7 @@ public class SimpleHandlerAdapter implements HandlerAdapter { public Mono handle(ServerWebExchange exchange, Object handler) { WebHandler webHandler = (WebHandler) handler; Mono mono = webHandler.handle(exchange); - return Mono.just(new HandlerResult(webHandler, mono, MONO_VOID)); + return Mono.just(new HandlerResult(webHandler, mono, RETURN_TYPE)); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java index fc63515005..8cdd19af18 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java @@ -31,7 +31,6 @@ import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.GenericTypeResolver; import org.springframework.core.MethodParameter; import org.springframework.core.ParameterNameDiscoverer; -import org.springframework.core.ResolvableType; import org.springframework.ui.ModelMap; import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; @@ -90,8 +89,7 @@ public class InvocableHandlerMethod extends HandlerMethod { return resolveArguments(exchange, model, providedArgs).then(args -> { try { Object value = doInvoke(args); - ResolvableType type = ResolvableType.forMethodParameter(getReturnType()); - HandlerResult handlerResult = new HandlerResult(this, value, type, model); + HandlerResult handlerResult = new HandlerResult(this, value, getReturnType(), model); return Mono.just(handlerResult); } catch (InvocationTargetException ex) { diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java index 4ac01a55b3..0eb04f7bdb 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java @@ -86,13 +86,8 @@ public class ResponseBodyResultHandler extends AbstractMessageConverterResultHan @Override public boolean supports(HandlerResult result) { ResolvableType returnType = result.getReturnType(); - if (returnType.getSource() instanceof MethodParameter) { - MethodParameter parameter = (MethodParameter) returnType.getSource(); - if (hasResponseBodyAnnotation(parameter) && !isHttpEntityType(returnType)) { - return true; - } - } - return false; + MethodParameter parameter = result.getReturnTypeSource(); + return hasResponseBodyAnnotation(parameter) && !isHttpEntityType(returnType); } private boolean hasResponseBodyAnnotation(MethodParameter parameter) { diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java index 2d94119c82..f46c142128 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java @@ -38,7 +38,6 @@ import org.springframework.http.MediaType; import org.springframework.ui.Model; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.HandlerResultHandler; import org.springframework.web.reactive.accept.HeaderContentTypeResolver; @@ -151,13 +150,8 @@ public class ViewResolutionResultHandler extends ContentNegotiatingResultHandler } private boolean hasModelAttributeAnnotation(HandlerResult result) { - if (result.getHandler() instanceof HandlerMethod) { - MethodParameter returnType = ((HandlerMethod) result.getHandler()).getReturnType(); - if (returnType.hasMethodAnnotation(ModelAttribute.class)) { - return true; - } - } - return false; + MethodParameter returnType = result.getReturnTypeSource(); + return returnType.hasMethodAnnotation(ModelAttribute.class); } private boolean isSupportedType(Class clazz) { @@ -263,14 +257,11 @@ public class ViewResolutionResultHandler extends ContentNegotiatingResultHandler //noinspection unchecked result.getModel().addAllAttributes((Map) value); } - else if (result.getHandler() instanceof HandlerMethod) { - MethodParameter returnType = ((HandlerMethod) result.getHandler()).getReturnType(); + else { + MethodParameter returnType = result.getReturnTypeSource(); String name = getNameForReturnValue(value, returnType); result.getModel().addAttribute(name, value); } - else { - result.getModel().addAttribute(value); - } return value; } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/ResolvableMethod.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/ResolvableMethod.java new file mode 100644 index 0000000000..2a83857cf1 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/ResolvableMethod.java @@ -0,0 +1,141 @@ +/* + * 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.web.reactive.result; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import org.springframework.core.MethodIntrospector; +import org.springframework.core.MethodParameter; +import org.springframework.core.ResolvableType; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; + +/** + * Convenience class for use in tests to resolve a {@link Method} and/or any of + * its {@link MethodParameter}s based on some hints. + * + *

    In tests we often create a class (e.g. TestController) with diverse method + * signatures and annotations to test with. Use of descriptive method and argument + * names combined with using reflection, it becomes challenging to read and write + * tests and it becomes necessary to navigate to the actual method declaration + * which is cumbersome and involves several steps. + * + *

    The idea here is to provide enough hints to resolving a method uniquely + * where the hints document exactly what is being tested and there is usually no + * need to navigate to the actual method declaration. For example if testing + * response handling, the return type may be used as a hint: + * + *

    + * ResolvableMethod resolvableMethod = ResolvableMethod.on(TestController.class);
    +
    + * ResolvableType type = ResolvableType.forClassWithGenerics(Mono.class, View.class);
    + * Method method = resolvableMethod.returning(type).resolve();
    + *
    + * type = ResolvableType.forClassWithGenerics(Mono.class, String.class);
    + * method = resolvableMethod.returning(type).resolve();
    + *
    + * // ...
    + * 
    + * + *

    Additional {@code resolve} methods provide options to obtain one of the method + * arguments or return type as a {@link MethodParameter}. + * + * @author Rossen Stoyanchev + */ +public class ResolvableMethod { + + private final Class targetClass; + + private String methodName; + + private ResolvableType returnType; + + private final List> annotationTypes = new ArrayList<>(4); + + + private ResolvableMethod(Class targetClass) { + this.targetClass = targetClass; + } + + + public ResolvableMethod name(String methodName) { + this.methodName = methodName; + return this; + } + + public ResolvableMethod returning(ResolvableType resolvableType) { + this.returnType = resolvableType; + return this; + } + + public ResolvableMethod annotated(Class annotationType) { + this.annotationTypes.add(annotationType); + return this; + } + + + public Method resolve() { + // String comparison (ResolvableType's with different providers) + String expected = this.returnType != null ? this.returnType.toString() : null; + + Set methods = MethodIntrospector.selectMethods(this.targetClass, + (ReflectionUtils.MethodFilter) method -> { + String actual = ResolvableType.forMethodReturnType(method).toString(); + if (this.methodName != null && !this.methodName.equals(method.getName())) { + return false; + } + if (expected != null) { + if (!actual.equals(expected) && !Object.class.equals(method.getDeclaringClass())) { + return false; + } + } + for (Class annotationType : this.annotationTypes) { + if (AnnotationUtils.findAnnotation(method, annotationType) == null) { + return false; + } + } + return true; + }); + + Assert.isTrue(!methods.isEmpty(), "No matching method: " + this); + Assert.isTrue(methods.size() == 1, "Multiple matching methods: " + this); + + return methods.iterator().next(); + } + + public MethodParameter resolveReturnType() { + Method method = resolve(); + return new MethodParameter(method, -1); + } + + + @Override + public String toString() { + return "Class=" + this.targetClass + ", name= " + this.methodName + + ", returnType=" + this.returnType + ", annotations=" + this.annotationTypes; + } + + + public static ResolvableMethod on(Class clazz) { + return new ResolvableMethod(clazz); + } + +} \ No newline at end of file diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/SimpleResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/SimpleResultHandlerTests.java index 1256f96bd4..0ebeddfa67 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/SimpleResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/SimpleResultHandlerTests.java @@ -18,21 +18,21 @@ package org.springframework.web.reactive.result; import java.util.concurrent.CompletableFuture; +import org.junit.Before; import org.junit.Test; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import rx.Observable; +import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.core.convert.support.MonoToCompletableFutureConverter; import org.springframework.core.convert.support.PublisherToFluxConverter; import org.springframework.core.convert.support.ReactorToRxJava1Converter; -import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerResult; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertEquals; /** * Unit tests for {@link SimpleResultHandler}. @@ -41,72 +41,55 @@ import static org.junit.Assert.assertTrue; */ public class SimpleResultHandlerTests { - @Test - public void supportsWithConversionService() throws NoSuchMethodException { + private SimpleResultHandler resultHandler; + + @Before + public void setUp() throws Exception { GenericConversionService conversionService = new GenericConversionService(); conversionService.addConverter(new MonoToCompletableFutureConverter()); conversionService.addConverter(new PublisherToFluxConverter()); conversionService.addConverter(new ReactorToRxJava1Converter()); - - SimpleResultHandler resultHandler = new SimpleResultHandler(conversionService); - TestController controller = new TestController(); - - HandlerMethod hm = new HandlerMethod(controller, TestController.class.getMethod("voidReturnValue")); - ResolvableType type = ResolvableType.forMethodParameter(hm.getReturnType()); - assertTrue(resultHandler.supports(createHandlerResult(hm, type))); - - hm = new HandlerMethod(controller, TestController.class.getMethod("publisherString")); - type = ResolvableType.forMethodParameter(hm.getReturnType()); - assertFalse(resultHandler.supports(createHandlerResult(hm, type))); - - hm = new HandlerMethod(controller, TestController.class.getMethod("publisherVoid")); - type = ResolvableType.forMethodParameter(hm.getReturnType()); - assertTrue(resultHandler.supports(createHandlerResult(hm, type))); - - hm = new HandlerMethod(controller, TestController.class.getMethod("streamVoid")); - type = ResolvableType.forMethodParameter(hm.getReturnType()); - assertTrue(resultHandler.supports(createHandlerResult(hm, type))); - - hm = new HandlerMethod(controller, TestController.class.getMethod("observableVoid")); - type = ResolvableType.forMethodParameter(hm.getReturnType()); - assertTrue(resultHandler.supports(createHandlerResult(hm, type))); - - hm = new HandlerMethod(controller, TestController.class.getMethod("completableFutureVoid")); - type = ResolvableType.forMethodParameter(hm.getReturnType()); - assertTrue(resultHandler.supports(createHandlerResult(hm, type))); + this.resultHandler = new SimpleResultHandler(conversionService); } - private HandlerResult createHandlerResult(HandlerMethod hm, ResolvableType type) { - return new HandlerResult(hm, null, type); + + @Test + public void supportsWithConversionService() throws NoSuchMethodException { + testSupports(ResolvableType.forClass(void.class), true); + testSupports(ResolvableType.forClassWithGenerics(Publisher.class, Void.class), true); + testSupports(ResolvableType.forClassWithGenerics(Flux.class, Void.class), true); + testSupports(ResolvableType.forClassWithGenerics(Observable.class, Void.class), true); + testSupports(ResolvableType.forClassWithGenerics(CompletableFuture.class, Void.class), true); + + testSupports(ResolvableType.forClass(String.class), false); + testSupports(ResolvableType.forClassWithGenerics(Publisher.class, String.class), false); + } + + private void testSupports(ResolvableType type, boolean result) { + MethodParameter param = ResolvableMethod.on(TestController.class).returning(type).resolveReturnType(); + HandlerResult handlerResult = new HandlerResult(new TestController(), null, param); + assertEquals(result, this.resultHandler.supports(handlerResult)); } @SuppressWarnings("unused") private static class TestController { - public void voidReturnValue() { - } + public void voidReturn() { } - public Publisher publisherString() { - return null; - } + public Publisher publisherString() { return null; } - public Publisher publisherVoid() { - return null; - } + public Flux flux() { return null; } - public Flux streamVoid() { - return null; - } + public Observable observable() { return null; } - public Observable observableVoid() { - return null; - } + public CompletableFuture completableFuture() { return null; } + + public String string() { return null; } + + public Publisher publisher() { return null; } - public CompletableFuture completableFutureVoid() { - return null; - } } } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java index e99454f126..38da2644c9 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java @@ -25,7 +25,6 @@ import org.junit.Before; import org.junit.Test; import reactor.core.publisher.Mono; -import org.springframework.core.ResolvableType; import org.springframework.core.codec.ByteBufferEncoder; import org.springframework.core.codec.StringEncoder; import org.springframework.core.convert.support.DefaultConversionService; @@ -123,8 +122,7 @@ public class ResponseBodyResultHandlerTests { private void testSupports(Object controller, String method, boolean result) throws NoSuchMethodException { HandlerMethod hm = handlerMethod(controller, method); - ResolvableType type = ResolvableType.forMethodParameter(hm.getReturnType()); - HandlerResult handlerResult = new HandlerResult(hm, null, type, new ExtendedModelMap()); + HandlerResult handlerResult = new HandlerResult(hm, null, hm.getReturnType(), new ExtendedModelMap()); assertEquals(result, this.resultHandler.supports(handlerResult)); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java index e65e867a2c..c90ddb8334 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java @@ -29,6 +29,7 @@ import reactor.core.publisher.Mono; import reactor.core.test.TestSubscriber; import rx.Single; +import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.core.codec.ByteBufferEncoder; import org.springframework.core.codec.StringEncoder; @@ -48,12 +49,11 @@ import org.springframework.http.converter.reactive.ResourceHttpMessageConverter; import org.springframework.http.server.reactive.MockServerHttpRequest; import org.springframework.http.server.reactive.MockServerHttpResponse; import org.springframework.http.server.reactive.ServerHttpRequest; -import org.springframework.ui.ExtendedModelMap; -import org.springframework.ui.ModelMap; import org.springframework.util.ObjectUtils; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.accept.RequestedContentTypeResolver; import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder; +import org.springframework.web.reactive.result.ResolvableMethod; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange; import org.springframework.web.server.session.MockWebSessionManager; @@ -75,9 +75,6 @@ import static org.springframework.core.ResolvableType.forClassWithGenerics; */ public class ResponseEntityResultHandlerTests { - private static final Object HANDLER = new Object(); - - private ResponseEntityResultHandler resultHandler; private MockServerHttpResponse response = new MockServerHttpResponse(); @@ -119,23 +116,22 @@ public class ResponseEntityResultHandlerTests { @Test @SuppressWarnings("ConstantConditions") public void supports() throws NoSuchMethodException { - ModelMap model = new ExtendedModelMap(); Object value = null; - ResolvableType type = responseEntityType(String.class); - assertTrue(this.resultHandler.supports(new HandlerResult(HANDLER, value, type, model))); + ResolvableType type = responseEntity(String.class); + assertTrue(this.resultHandler.supports(handlerResult(value, type))); - type = forClassWithGenerics(Mono.class, responseEntityType(String.class)); - assertTrue(this.resultHandler.supports(new HandlerResult(HANDLER, value, type, model))); + type = classWithGenerics(Mono.class, responseEntity(String.class)); + assertTrue(this.resultHandler.supports(handlerResult(value, type))); - type = forClassWithGenerics(Single.class, responseEntityType(String.class)); - assertTrue(this.resultHandler.supports(new HandlerResult(HANDLER, value, type, model))); + type = classWithGenerics(Single.class, responseEntity(String.class)); + assertTrue(this.resultHandler.supports(handlerResult(value, type))); - type = forClassWithGenerics(CompletableFuture.class, responseEntityType(String.class)); - assertTrue(this.resultHandler.supports(new HandlerResult(HANDLER, value, type, model))); + type = classWithGenerics(CompletableFuture.class, responseEntity(String.class)); + assertTrue(this.resultHandler.supports(handlerResult(value, type))); type = ResolvableType.forClass(String.class); - assertFalse(this.resultHandler.supports(new HandlerResult(HANDLER, value, type, model))); + assertFalse(this.resultHandler.supports(handlerResult(value, type))); } @Test @@ -145,8 +141,9 @@ public class ResponseEntityResultHandlerTests { @Test public void statusCode() throws Exception { - ResolvableType type = responseEntityType(Void.class); - HandlerResult result = new HandlerResult(HANDLER, ResponseEntity.noContent().build(), type); + ResponseEntity value = ResponseEntity.noContent().build(); + ResolvableType type = responseEntity(Void.class); + HandlerResult result = handlerResult(value, type); this.resultHandler.handleResult(exchange, result).block(Duration.ofSeconds(5)); assertEquals(HttpStatus.NO_CONTENT, this.response.getStatus()); @@ -157,8 +154,9 @@ public class ResponseEntityResultHandlerTests { @Test public void headers() throws Exception { URI location = new URI("/path"); - ResolvableType type = responseEntityType(Void.class); - HandlerResult result = new HandlerResult(HANDLER, ResponseEntity.created(location).build(), type); + ResolvableType type = responseEntity(Void.class); + ResponseEntity value = ResponseEntity.created(location).build(); + HandlerResult result = handlerResult(value, type); this.resultHandler.handleResult(this.exchange, result).block(Duration.ofSeconds(5)); assertEquals(HttpStatus.CREATED, this.response.getStatus()); @@ -170,25 +168,25 @@ public class ResponseEntityResultHandlerTests { @Test public void handleReturnTypes() throws Exception { Object returnValue = ResponseEntity.ok("abc"); - ResolvableType returnType = responseEntityType(String.class); + ResolvableType returnType = responseEntity(String.class); testHandle(returnValue, returnType); returnValue = Mono.just(ResponseEntity.ok("abc")); - returnType = forClassWithGenerics(Mono.class, responseEntityType(String.class)); + returnType = forClassWithGenerics(Mono.class, responseEntity(String.class)); testHandle(returnValue, returnType); returnValue = Mono.just(ResponseEntity.ok("abc")); - returnType = forClassWithGenerics(Single.class, responseEntityType(String.class)); + returnType = forClassWithGenerics(Single.class, responseEntity(String.class)); testHandle(returnValue, returnType); returnValue = Mono.just(ResponseEntity.ok("abc")); - returnType = forClassWithGenerics(CompletableFuture.class, responseEntityType(String.class)); + returnType = forClassWithGenerics(CompletableFuture.class, responseEntity(String.class)); testHandle(returnValue, returnType); } - private void testHandle(Object returnValue, ResolvableType returnType) { - HandlerResult result = new HandlerResult(HANDLER, returnValue, returnType); + private void testHandle(Object returnValue, ResolvableType type) { + HandlerResult result = handlerResult(returnValue, type); this.resultHandler.handleResult(this.exchange, result).block(Duration.ofSeconds(5)); assertEquals(HttpStatus.OK, this.response.getStatus()); @@ -197,8 +195,17 @@ public class ResponseEntityResultHandlerTests { } - private ResolvableType responseEntityType(Class bodyType) { - return forClassWithGenerics(ResponseEntity.class, bodyType); + private ResolvableType responseEntity(Class bodyType) { + return classWithGenerics(ResponseEntity.class, ResolvableType.forClass(bodyType)); + } + + private ResolvableType classWithGenerics(Class sourceType, ResolvableType genericType) { + return ResolvableType.forClassWithGenerics(sourceType, genericType); + } + + private HandlerResult handlerResult(Object returnValue, ResolvableType type) { + MethodParameter param = ResolvableMethod.on(TestController.class).returning(type).resolveReturnType(); + return new HandlerResult(new TestController(), returnValue, param); } private void assertResponseBody(String responseBody) { @@ -207,4 +214,21 @@ public class ResponseEntityResultHandlerTests { DataBufferTestUtils.dumpString(buf, Charset.forName("UTF-8")))); } + + @SuppressWarnings("unused") + private static class TestController { + + ResponseEntity responseEntityString() { return null; } + + ResponseEntity responseEntityVoid() { return null; } + + Mono> mono() { return null; } + + Single> single() { return null; } + + CompletableFuture> completableFuture() { return null; } + + String string() { return null; } + } + } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/HttpMessageConverterViewTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/HttpMessageConverterViewTests.java index 0f6cc3a3a0..4fb874abc5 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/HttpMessageConverterViewTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/HttpMessageConverterViewTests.java @@ -31,19 +31,20 @@ import org.junit.Before; import org.junit.Test; import reactor.core.test.TestSubscriber; -import org.springframework.core.ResolvableType; -import org.springframework.http.codec.json.JacksonJsonEncoder; -import org.springframework.http.codec.xml.Jaxb2Encoder; +import org.springframework.core.MethodParameter; import org.springframework.core.codec.StringEncoder; import org.springframework.core.io.buffer.support.DataBufferTestUtils; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; +import org.springframework.http.codec.json.JacksonJsonEncoder; +import org.springframework.http.codec.xml.Jaxb2Encoder; import org.springframework.http.server.reactive.MockServerHttpRequest; import org.springframework.http.server.reactive.MockServerHttpResponse; import org.springframework.ui.ExtendedModelMap; import org.springframework.ui.ModelMap; import org.springframework.util.MimeType; import org.springframework.web.reactive.HandlerResult; +import org.springframework.web.reactive.result.ResolvableMethod; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange; import org.springframework.web.server.session.DefaultWebSessionManager; @@ -62,18 +63,17 @@ import static org.junit.Assert.fail; */ public class HttpMessageConverterViewTests { - private HttpMessageConverterView view; + private HttpMessageConverterView view = new HttpMessageConverterView(new JacksonJsonEncoder()); private HandlerResult result; - private ModelMap model; + private ModelMap model = new ExtendedModelMap(); @Before public void setup() throws Exception { - this.view = new HttpMessageConverterView(new JacksonJsonEncoder()); - this.model = new ExtendedModelMap(); - this.result = new HandlerResult(new Object(), null, ResolvableType.NONE, model); + MethodParameter param = ResolvableMethod.on(this.getClass()).name("handle").resolveReturnType(); + this.result = new HandlerResult(this, null, param, this.model); } @@ -176,4 +176,9 @@ public class HttpMessageConverterViewTests { } + @SuppressWarnings("unused") + private String handle() { + return null; + } + } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/UrlBasedViewResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/UrlBasedViewResolverTests.java index 98d16dc17a..11d61c1dcf 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/UrlBasedViewResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/UrlBasedViewResolverTests.java @@ -19,11 +19,9 @@ import java.util.Locale; import java.util.Map; import org.junit.Test; -import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.context.support.StaticApplicationContext; -import org.springframework.core.io.buffer.DataBuffer; import org.springframework.web.server.ServerWebExchange; import static org.junit.Assert.assertNotNull; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java index f8f1276142..4f8b7ed511 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java @@ -35,6 +35,7 @@ import reactor.core.publisher.Mono; import reactor.core.test.TestSubscriber; import rx.Single; +import org.springframework.core.MethodParameter; import org.springframework.core.Ordered; import org.springframework.core.ResolvableType; import org.springframework.core.convert.support.ConfigurableConversionService; @@ -49,13 +50,12 @@ import org.springframework.http.MediaType; import org.springframework.http.server.reactive.MockServerHttpRequest; import org.springframework.http.server.reactive.MockServerHttpResponse; import org.springframework.http.server.reactive.ServerHttpResponse; -import org.springframework.stereotype.Controller; import org.springframework.ui.ExtendedModelMap; import org.springframework.ui.Model; import org.springframework.ui.ModelMap; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerResult; +import org.springframework.web.reactive.result.ResolvableMethod; import org.springframework.web.server.NotAcceptableStatusException; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange; @@ -90,26 +90,19 @@ public class ViewResolutionResultHandlerTests { @Test public void supports() throws Exception { - Object handler = new Object(); - HandlerMethod hm = handlerMethod(new TestController(), "modelAttributeMethod"); - testSupports(handler, ResolvableType.forClass(String.class), true); - testSupports(handler, ResolvableType.forClass(View.class), true); - testSupports(handler, ResolvableType.forClassWithGenerics(Mono.class, String.class), true); - testSupports(handler, ResolvableType.forClassWithGenerics(Mono.class, View.class), true); - testSupports(handler, ResolvableType.forClassWithGenerics(Single.class, String.class), true); - testSupports(handler, ResolvableType.forClassWithGenerics(Single.class, View.class), true); - testSupports(handler, ResolvableType.forClass(Model.class), true); - testSupports(handler, ResolvableType.forClass(Map.class), true); - testSupports(handler, ResolvableType.forClass(TestBean.class), true); - testSupports(handler, ResolvableType.forClass(Integer.class), false); - testSupports(hm, ResolvableType.forMethodParameter(hm.getReturnType()), true); - } + testSupports(ResolvableType.forClass(String.class), true); + testSupports(ResolvableType.forClass(View.class), true); + testSupports(ResolvableType.forClassWithGenerics(Mono.class, String.class), true); + testSupports(ResolvableType.forClassWithGenerics(Mono.class, View.class), true); + testSupports(ResolvableType.forClassWithGenerics(Single.class, String.class), true); + testSupports(ResolvableType.forClassWithGenerics(Single.class, View.class), true); + testSupports(ResolvableType.forClass(Model.class), true); + testSupports(ResolvableType.forClass(Map.class), true); + testSupports(ResolvableType.forClass(TestBean.class), true); + testSupports(ResolvableType.forClass(Integer.class), false); - private void testSupports(Object handler, ResolvableType returnType, boolean result) { - ViewResolutionResultHandler resultHandler = createResultHandler(mock(ViewResolver.class)); - HandlerResult handlerResult = new HandlerResult(handler, null, returnType, new ExtendedModelMap()); - assertEquals(result, resultHandler.supports(handlerResult)); + testSupports(resolvableMethod().annotated(ModelAttribute.class), true); } @Test @@ -125,53 +118,50 @@ public class ViewResolutionResultHandlerTests { @Test public void handleReturnValueTypes() throws Exception { - Object handler = new Object(); Object returnValue; ResolvableType returnType; ViewResolver resolver = new TestViewResolver("account"); - returnValue = new TestView("account"); returnType = ResolvableType.forClass(View.class); - testHandle("/path", handler, returnValue, returnType, "account: {id=123}"); + returnValue = new TestView("account"); + testHandle("/path", returnType, returnValue, "account: {id=123}"); - returnValue = Mono.just(new TestView("account")); returnType = ResolvableType.forClassWithGenerics(Mono.class, View.class); - testHandle("/path", handler, returnValue, returnType, "account: {id=123}"); + returnValue = Mono.just(new TestView("account")); + testHandle("/path", returnType, returnValue, "account: {id=123}"); - returnValue = "account"; returnType = ResolvableType.forClass(String.class); - testHandle("/path", handler, returnValue, returnType, "account: {id=123}", resolver); + returnValue = "account"; + testHandle("/path", returnType, returnValue, "account: {id=123}", resolver); - returnValue = Mono.just("account"); returnType = ResolvableType.forClassWithGenerics(Mono.class, String.class); - testHandle("/path", handler, returnValue, returnType, "account: {id=123}", resolver); + returnValue = Mono.just("account"); + testHandle("/path", returnType, returnValue, "account: {id=123}", resolver); - returnValue = new ExtendedModelMap().addAttribute("name", "Joe"); returnType = ResolvableType.forClass(Model.class); - testHandle("/account", handler, returnValue, returnType, "account: {id=123, name=Joe}", resolver); + returnValue = new ExtendedModelMap().addAttribute("name", "Joe"); + testHandle("/account", returnType, returnValue, "account: {id=123, name=Joe}", resolver); - returnValue = Collections.singletonMap("name", "Joe"); returnType = ResolvableType.forClass(Map.class); - testHandle("/account", handler, returnValue, returnType, "account: {id=123, name=Joe}", resolver); + returnValue = Collections.singletonMap("name", "Joe"); + testHandle("/account", returnType, returnValue, "account: {id=123, name=Joe}", resolver); - HandlerMethod hm = handlerMethod(new TestController(), "modelAttributeMethod"); - returnValue = "Joe"; - returnType = ResolvableType.forMethodParameter(hm.getReturnType()); - testHandle("/account", hm, returnValue, returnType, "account: {id=123, name=Joe}", resolver); - - returnValue = new TestBean("Joe"); returnType = ResolvableType.forClass(TestBean.class); - testHandle("/account", handler, returnValue, returnType, "account: {id=123, testBean=TestBean[name=Joe]}", resolver); + returnValue = new TestBean("Joe"); + String responseBody = "account: {id=123, testBean=TestBean[name=Joe]}"; + testHandle("/account", returnType, returnValue, responseBody, resolver); + + testHandle("/account", resolvableMethod().annotated(ModelAttribute.class), + 99L, "account: {id=123, num=99}", resolver); } @Test public void handleWithMultipleResolvers() throws Exception { - Object handler = new Object(); Object returnValue = "profile"; ResolvableType returnType = ResolvableType.forClass(String.class); ViewResolver[] resolvers = {new TestViewResolver("account"), new TestViewResolver("profile")}; - testHandle("/account", handler, returnValue, returnType, "profile: {id=123}", resolvers); + testHandle("/account", returnType, returnValue, "profile: {id=123}", resolvers); } @Test @@ -180,11 +170,11 @@ public class ViewResolutionResultHandlerTests { testDefaultViewName(Mono.empty(), ResolvableType.forClassWithGenerics(Mono.class, String.class)); } - private void testDefaultViewName(Object returnValue, ResolvableType returnType) + private void testDefaultViewName(Object returnValue, ResolvableType type) throws URISyntaxException { ModelMap model = new ExtendedModelMap().addAttribute("id", "123"); - HandlerResult result = new HandlerResult(new Object(), returnValue, returnType, model); + HandlerResult result = new HandlerResult(new Object(), returnValue, returnType(type), model); ViewResolutionResultHandler handler = createResultHandler(new TestViewResolver("account")); this.request.setUri(new URI("/account")); @@ -203,9 +193,9 @@ public class ViewResolutionResultHandlerTests { @Test public void unresolvedViewName() throws Exception { String returnValue = "account"; - ResolvableType returnType = ResolvableType.forClass(String.class); + ResolvableType type = ResolvableType.forClass(String.class); ExtendedModelMap model = new ExtendedModelMap(); - HandlerResult handlerResult = new HandlerResult(new Object(), returnValue, returnType, model); + HandlerResult handlerResult = new HandlerResult(new Object(), returnValue, returnType(type), model); this.request.setUri(new URI("/path")); Mono mono = createResultHandler().handleResult(this.exchange, handlerResult); @@ -217,7 +207,8 @@ public class ViewResolutionResultHandlerTests { public void contentNegotiation() throws Exception { TestBean value = new TestBean("Joe"); ResolvableType type = ResolvableType.forClass(TestBean.class); - HandlerResult handlerResult = new HandlerResult(new Object(), value, type, new ExtendedModelMap()); + ExtendedModelMap model = new ExtendedModelMap(); + HandlerResult handlerResult = new HandlerResult(new Object(), value, returnType(type), model); this.request.getHeaders().setAccept(Collections.singletonList(APPLICATION_JSON)); this.request.setUri(new URI("/account")); @@ -236,7 +227,8 @@ public class ViewResolutionResultHandlerTests { public void contentNegotiationWith406() throws Exception { TestBean value = new TestBean("Joe"); ResolvableType type = ResolvableType.forClass(TestBean.class); - HandlerResult handlerResult = new HandlerResult(new Object(), value, type, new ExtendedModelMap()); + ExtendedModelMap model = new ExtendedModelMap(); + HandlerResult handlerResult = new HandlerResult(new Object(), value, returnType(type), model); this.request.getHeaders().setAccept(Collections.singletonList(APPLICATION_JSON)); this.request.setUri(new URI("/account")); @@ -247,6 +239,26 @@ public class ViewResolutionResultHandlerTests { } + private MethodParameter returnType(ResolvableType type) { + return resolvableMethod().returning(type).resolveReturnType(); + } + + private ResolvableMethod resolvableMethod() { + return ResolvableMethod.on(TestController.class); + } + + private void testSupports(ResolvableType type, boolean result) { + testSupports(resolvableMethod().returning(type), result); + } + + private void testSupports(ResolvableMethod resolvableMethod, boolean result) { + ViewResolutionResultHandler resultHandler = createResultHandler(mock(ViewResolver.class)); + MethodParameter returnType = resolvableMethod.resolveReturnType(); + ExtendedModelMap model = new ExtendedModelMap(); + HandlerResult handlerResult = new HandlerResult(new Object(), null, returnType, model); + assertEquals(result, resultHandler.supports(handlerResult)); + } + private ViewResolutionResultHandler createResultHandler(ViewResolver... resolvers) { return createResultHandler(Collections.emptyList(), resolvers); } @@ -261,15 +273,18 @@ public class ViewResolutionResultHandlerTests { return handler; } - private HandlerMethod handlerMethod(Object controller, String method) throws NoSuchMethodException { - return new HandlerMethod(controller, controller.getClass().getMethod(method)); + private void testHandle(String path, ResolvableType returnType, Object returnValue, + String responseBody, ViewResolver... resolvers) throws URISyntaxException { + + testHandle(path, resolvableMethod().returning(returnType), returnValue, responseBody, resolvers); } - private void testHandle(String path, Object handler, Object returnValue, ResolvableType returnType, + private void testHandle(String path, ResolvableMethod resolvableMethod, Object returnValue, String responseBody, ViewResolver... resolvers) throws URISyntaxException { ModelMap model = new ExtendedModelMap().addAttribute("id", "123"); - HandlerResult result = new HandlerResult(handler, returnValue, returnType, model); + MethodParameter returnType = resolvableMethod.resolveReturnType(); + HandlerResult result = new HandlerResult(new Object(), returnValue, returnType, model); this.request.setUri(new URI(path)); createResultHandler(resolvers).handleResult(this.exchange, result).block(Duration.ofSeconds(5)); assertResponseBody(responseBody); @@ -350,15 +365,6 @@ public class ViewResolutionResultHandlerTests { } } - @Controller @SuppressWarnings("unused") - private static class TestController { - - @ModelAttribute("name") - public String modelAttributeMethod() { - return null; - } - } - private static class TestBean { private final String name; @@ -377,4 +383,31 @@ public class ViewResolutionResultHandlerTests { } } + @SuppressWarnings("unused") + private static class TestController { + + String string() { return null; } + + View view() { return null; } + + Mono monoString() { return null; } + + Mono monoView() { return null; } + + Single singleString() { return null; } + + Single singleView() { return null; } + + Model model() { return null; } + + Map map() { return null; } + + TestBean testBean() { return null; } + + Integer integer() { return null; } + + @ModelAttribute("num") + Long longAttribute() { return null; } + } + } \ No newline at end of file diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerViewTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerViewTests.java index 5407b2822b..fac9daa7ff 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerViewTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerViewTests.java @@ -29,13 +29,14 @@ import reactor.core.test.TestSubscriber; import org.springframework.context.ApplicationContextException; import org.springframework.context.support.GenericApplicationContext; -import org.springframework.core.ResolvableType; +import org.springframework.core.MethodParameter; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpMethod; import org.springframework.http.server.reactive.MockServerHttpRequest; import org.springframework.http.server.reactive.MockServerHttpResponse; import org.springframework.ui.ExtendedModelMap; import org.springframework.ui.ModelMap; +import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange; @@ -125,7 +126,8 @@ public class FreeMarkerViewTests { ModelMap model = new ExtendedModelMap(); model.addAttribute("hello", "hi FreeMarker"); - HandlerResult result = new HandlerResult(new Object(), "", ResolvableType.NONE, model); + MethodParameter returnType = new MethodParameter(getClass().getDeclaredMethod("handle"), -1); + HandlerResult result = new HandlerResult(new Object(), "", returnType, model); view.render(result, null, this.exchange); TestSubscriber @@ -142,4 +144,10 @@ public class FreeMarkerViewTests { return new String(bytes, UTF_8); } + + @SuppressWarnings("unused") + private String handle() { + return null; + } + } From 8c7658144284f2f47124c6ac8131f53320091551 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Sun, 3 Jul 2016 21:36:18 -0400 Subject: [PATCH 454/498] Provide rich type information to ConversionService When using the ConversionService to check and bridge to and from reactive types we now generallly provide the full type information available from method signatures. However that full type information is not always necessary such as when we perform additional checks on the generics of the reactive type (e.g. Mono). This allows us to switch to use DefaultFormattingConversionService instead of GenericConversionService while also ensuring that the CollectionToObjectConverter doesn't think it can convert List to any reactive type. The ObjectToObjectConverter can also interfere because it is smart enough to find the "from(Publisher)" method on Flux and Mono. To make up for that on the response side we now check if a type is assignable to Publisher first in which case it is a simple cast. In turn that means we don't need a PublisherToFluxConverter which can be problematic in its own right because it can convert from Mono to Flux which technically doesn't lose data but switches stream semantics. Issue: #124, #128 --- .../support/PublisherToFluxConverter.java | 51 --------------- .../config/WebReactiveConfiguration.java | 37 +++++------ .../reactive/result/SimpleResultHandler.java | 35 ++++++----- ...AbstractMessageConverterResultHandler.java | 45 ++++++++----- .../RequestBodyArgumentResolver.java | 15 +++-- .../annotation/ResponseBodyResultHandler.java | 3 +- .../ResponseEntityResultHandler.java | 18 ++++-- .../result/SimpleResultHandlerTests.java | 23 ++++--- .../MessageConverterResultHandlerTests.java | 63 ++++++++++++++----- .../RequestBodyArgumentResolverTests.java | 16 +++-- .../ResponseBodyResultHandlerTests.java | 7 +-- .../ResponseEntityResultHandlerTests.java | 7 +-- .../ViewResolutionResultHandlerTests.java | 12 ++-- 13 files changed, 177 insertions(+), 155 deletions(-) delete mode 100644 spring-web-reactive/src/main/java/org/springframework/core/convert/support/PublisherToFluxConverter.java diff --git a/spring-web-reactive/src/main/java/org/springframework/core/convert/support/PublisherToFluxConverter.java b/spring-web-reactive/src/main/java/org/springframework/core/convert/support/PublisherToFluxConverter.java deleted file mode 100644 index 6867245c03..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/core/convert/support/PublisherToFluxConverter.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2002-2015 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.convert.support; - -import java.util.LinkedHashSet; -import java.util.Set; - -import org.reactivestreams.Publisher; -import reactor.core.publisher.Flux; - -import org.springframework.core.convert.TypeDescriptor; -import org.springframework.core.convert.converter.GenericConverter; - -/** - * @author Sebastien Deleuze - */ -public class PublisherToFluxConverter implements GenericConverter { - - @Override - public Set getConvertibleTypes() { - Set pairs = new LinkedHashSet<>(); - pairs.add(new ConvertiblePair(Publisher.class, Flux.class)); - return pairs; - } - - @Override - public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { - if (source == null) { - return null; - } - else if (Publisher.class.isAssignableFrom(sourceType.getType())) { - return Flux.from((Publisher)source); - } - return null; - } - -} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java index ccf0e53af5..65cf61f880 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java @@ -28,25 +28,25 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.core.codec.Decoder; -import org.springframework.core.codec.Encoder; import org.springframework.core.codec.ByteBufferDecoder; import org.springframework.core.codec.ByteBufferEncoder; -import org.springframework.core.convert.support.PublisherToFluxConverter; +import org.springframework.core.codec.Decoder; +import org.springframework.core.codec.Encoder; +import org.springframework.core.codec.StringDecoder; +import org.springframework.core.codec.StringEncoder; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.support.MonoToCompletableFutureConverter; +import org.springframework.core.convert.support.ReactorToRxJava1Converter; +import org.springframework.format.Formatter; +import org.springframework.format.FormatterRegistry; +import org.springframework.format.support.DefaultFormattingConversionService; +import org.springframework.format.support.FormattingConversionService; +import org.springframework.http.MediaType; +import org.springframework.http.codec.SseEventEncoder; import org.springframework.http.codec.json.JacksonJsonDecoder; import org.springframework.http.codec.json.JacksonJsonEncoder; import org.springframework.http.codec.xml.Jaxb2Decoder; import org.springframework.http.codec.xml.Jaxb2Encoder; -import org.springframework.core.codec.StringDecoder; -import org.springframework.core.codec.StringEncoder; -import org.springframework.core.convert.converter.Converter; -import org.springframework.core.convert.converter.ConverterRegistry; -import org.springframework.core.convert.support.GenericConversionService; -import org.springframework.core.convert.support.MonoToCompletableFutureConverter; -import org.springframework.core.convert.support.ReactorToRxJava1Converter; -import org.springframework.format.Formatter; -import org.springframework.http.MediaType; -import org.springframework.http.codec.SseEventEncoder; import org.springframework.http.converter.reactive.CodecHttpMessageConverter; import org.springframework.http.converter.reactive.HttpMessageConverter; import org.springframework.http.converter.reactive.ResourceHttpMessageConverter; @@ -271,17 +271,13 @@ public class WebReactiveConfiguration implements ApplicationContextAware { protected void extendMessageConverters(List> converters) { } - // TODO: switch to DefaultFormattingConversionService - @Bean - public GenericConversionService mvcConversionService() { - GenericConversionService service = new GenericConversionService(); + public FormattingConversionService mvcConversionService() { + FormattingConversionService service = new DefaultFormattingConversionService(); addFormatters(service); return service; } - // TODO: switch to FormatterRegistry - /** * Override to add custom {@link Converter}s and {@link Formatter}s. *

    By default this method method registers: @@ -290,9 +286,8 @@ public class WebReactiveConfiguration implements ApplicationContextAware { *

  • {@link ReactorToRxJava1Converter} * */ - protected void addFormatters(ConverterRegistry registry) { + protected void addFormatters(FormatterRegistry registry) { registry.addConverter(new MonoToCompletableFutureConverter()); - registry.addConverter(new PublisherToFluxConverter()); if (DependencyUtils.hasRxJava1()) { registry.addConverter(new ReactorToRxJava1Converter()); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/SimpleResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/SimpleResultHandler.java index bff817cc2f..b3161c86e8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/SimpleResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/SimpleResultHandler.java @@ -18,12 +18,14 @@ package org.springframework.web.reactive.result; import java.util.Optional; +import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.core.Ordered; import org.springframework.core.ResolvableType; import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; import org.springframework.util.Assert; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.HandlerResultHandler; @@ -42,6 +44,11 @@ import org.springframework.web.server.ServerWebExchange; */ public class SimpleResultHandler implements Ordered, HandlerResultHandler { + protected static final TypeDescriptor MONO_TYPE = TypeDescriptor.valueOf(Mono.class); + + protected static final TypeDescriptor FLUX_TYPE = TypeDescriptor.valueOf(Flux.class); + + private ConversionService conversionService; private int order = Ordered.LOWEST_PRECEDENCE; @@ -83,14 +90,19 @@ public class SimpleResultHandler implements Ordered, HandlerResultHandler { if (Void.TYPE.equals(type.getRawClass())) { return true; } - if (getConversionService().canConvert(type.getRawClass(), Mono.class) || - getConversionService().canConvert(type.getRawClass(), Flux.class)) { + TypeDescriptor source = new TypeDescriptor(result.getReturnTypeSource()); + if (Publisher.class.isAssignableFrom(type.getRawClass()) || + canConvert(source, MONO_TYPE) || canConvert(source, FLUX_TYPE)) { Class clazz = result.getReturnType().getGeneric(0).getRawClass(); return Void.class.equals(clazz); } return false; } + private boolean canConvert(TypeDescriptor source, TypeDescriptor target) { + return getConversionService().canConvert(source, target); + } + @SuppressWarnings("unchecked") @Override public Mono handleResult(ServerWebExchange exchange, HandlerResult result) { @@ -98,19 +110,14 @@ public class SimpleResultHandler implements Ordered, HandlerResultHandler { if (!optional.isPresent()) { return Mono.empty(); } - - Object returnValue = optional.get(); - if (returnValue instanceof Mono) { - return (Mono) returnValue; - } - - ResolvableType returnType = result.getReturnType(); - if (getConversionService().canConvert(returnType.getRawClass(), Mono.class)) { - return this.conversionService.convert(returnValue, Mono.class); - } - else { - return this.conversionService.convert(returnValue, Flux.class).single(); + Object value = optional.get(); + if (Publisher.class.isAssignableFrom(result.getReturnType().getRawClass())) { + return Mono.from((Publisher) value).then(); } + TypeDescriptor source = new TypeDescriptor(result.getReturnTypeSource()); + return canConvert(source, MONO_TYPE) ? + ((Mono) getConversionService().convert(value, source, MONO_TYPE)) : + ((Flux) getConversionService().convert(value, source, FLUX_TYPE)).single(); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageConverterResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageConverterResultHandler.java index cf66171006..95f5ca57b4 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageConverterResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageConverterResultHandler.java @@ -22,8 +22,10 @@ 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.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; import org.springframework.http.MediaType; import org.springframework.http.converter.reactive.HttpMessageConverter; import org.springframework.http.server.reactive.ServerHttpResponse; @@ -41,6 +43,11 @@ import org.springframework.web.server.ServerWebExchange; */ public abstract class AbstractMessageConverterResultHandler extends ContentNegotiatingResultHandlerSupport { + protected static final TypeDescriptor MONO_TYPE = TypeDescriptor.valueOf(Mono.class); + + protected static final TypeDescriptor FLUX_TYPE = TypeDescriptor.valueOf(Flux.class); + + private final List> messageConverters; @@ -70,28 +77,34 @@ public abstract class AbstractMessageConverterResultHandler extends ContentNegot @SuppressWarnings("unchecked") - protected Mono writeBody(ServerWebExchange exchange, Object body, ResolvableType bodyType) { + protected Mono writeBody(ServerWebExchange exchange, Object body, + ResolvableType bodyType, MethodParameter bodyTypeParameter) { - boolean convertToFlux = getConversionService().canConvert(bodyType.getRawClass(), Flux.class); - boolean convertToMono = getConversionService().canConvert(bodyType.getRawClass(), Mono.class); + Publisher publisher = null; + ResolvableType elementType; - ResolvableType elementType = convertToFlux || convertToMono ? bodyType.getGeneric(0) : bodyType; - - Publisher publisher; - if (body == null) { - publisher = Mono.empty(); - } - else if (convertToMono) { - publisher = getConversionService().convert(body, Mono.class); - } - else if (convertToFlux) { - publisher = getConversionService().convert(body, Flux.class); + if (Publisher.class.isAssignableFrom(bodyType.getRawClass())) { + publisher = (Publisher) body; } else { - publisher = Mono.just(body); + TypeDescriptor descriptor = new TypeDescriptor(bodyTypeParameter); + if (getConversionService().canConvert(descriptor, MONO_TYPE)) { + publisher = (Publisher) getConversionService().convert(body, descriptor, MONO_TYPE); + } + else if (getConversionService().canConvert(descriptor, FLUX_TYPE)) { + publisher = (Publisher) getConversionService().convert(body, descriptor, FLUX_TYPE); + } } - if (Void.class.equals(elementType.getRawClass())) { + if (publisher != null) { + elementType = bodyType.getGeneric(0); + } + else { + elementType = bodyType; + publisher = Mono.justOrEmpty(body); + } + + if (void.class == elementType.getRawClass() || Void.class == elementType.getRawClass()) { return Mono.from((Publisher) publisher); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java index 3a7370778e..52ece42bbc 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java @@ -29,6 +29,7 @@ import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; import org.springframework.http.MediaType; import org.springframework.http.converter.reactive.HttpMessageConverter; import org.springframework.http.server.reactive.ServerHttpRequest; @@ -57,6 +58,11 @@ import org.springframework.web.server.UnsupportedMediaTypeStatusException; */ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolver { + private static final TypeDescriptor MONO_TYPE = TypeDescriptor.valueOf(Mono.class); + + private static final TypeDescriptor FLUX_TYPE = TypeDescriptor.valueOf(Flux.class); + + private final List> messageConverters; private final ConversionService conversionService; @@ -123,8 +129,9 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve ResolvableType type = ResolvableType.forMethodParameter(parameter); - boolean convertFromMono = getConversionService().canConvert(Mono.class, type.getRawClass()); - boolean convertFromFlux = getConversionService().canConvert(Flux.class, type.getRawClass()); + TypeDescriptor typeDescriptor = new TypeDescriptor(parameter); + boolean convertFromMono = getConversionService().canConvert(MONO_TYPE, typeDescriptor); + boolean convertFromFlux = getConversionService().canConvert(FLUX_TYPE, typeDescriptor); ResolvableType elementType = convertFromMono || convertFromFlux ? type.getGeneric(0) : type; @@ -141,7 +148,7 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve if (this.validator != null) { flux = flux.map(applyValidationIfApplicable(parameter)); } - return Mono.just(this.conversionService.convert(flux, type.getRawClass())); + return Mono.just(getConversionService().convert(flux, FLUX_TYPE, typeDescriptor)); } else { Mono mono = converter.readOne(elementType, request); @@ -151,7 +158,7 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve if (!convertFromMono) { return mono.map(value-> value); // TODO: MonoToObjectConverter } - return Mono.just(this.conversionService.convert(mono, type.getRawClass())); + return Mono.just(getConversionService().convert(mono, MONO_TYPE, typeDescriptor)); } } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java index 0eb04f7bdb..54cbbcc85e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java @@ -114,7 +114,8 @@ public class ResponseBodyResultHandler extends AbstractMessageConverterResultHan public Mono handleResult(ServerWebExchange exchange, HandlerResult result) { Object body = result.getReturnValue().orElse(null); ResolvableType bodyType = result.getReturnType(); - return writeBody(exchange, body, bodyType); + MethodParameter bodyTypeParameter = result.getReturnTypeSource(); + return writeBody(exchange, body, bodyType, bodyTypeParameter); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java index 5ebc8f8cfa..2079db0bd5 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java @@ -20,6 +20,7 @@ import java.util.Optional; import reactor.core.publisher.Mono; +import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.core.convert.ConversionService; import org.springframework.http.HttpEntity; @@ -85,7 +86,6 @@ public class ResponseEntityResultHandler extends AbstractMessageConverterResultH else if (getConversionService().canConvert(returnType.getRawClass(), Mono.class)) { ResolvableType genericType = result.getReturnType().getGeneric(0); return isSupportedType(genericType); - } return false; } @@ -100,17 +100,25 @@ public class ResponseEntityResultHandler extends AbstractMessageConverterResultH public Mono handleResult(ServerWebExchange exchange, HandlerResult result) { ResolvableType returnType = result.getReturnType(); - Mono returnValueMono; - ResolvableType bodyType; + ResolvableType bodyType; + MethodParameter bodyTypeParameter; + + Mono returnValueMono; Optional optional = result.getReturnValue(); + if (optional.isPresent() && getConversionService().canConvert(returnType.getRawClass(), Mono.class)) { returnValueMono = getConversionService().convert(optional.get(), Mono.class); - bodyType = returnType.getGeneric(0).getGeneric(0); + bodyType = returnType.getGeneric(0, 0); + bodyTypeParameter = new MethodParameter(result.getReturnTypeSource()); + bodyTypeParameter.increaseNestingLevel(); + bodyTypeParameter.increaseNestingLevel(); } else { returnValueMono = Mono.justOrEmpty(optional); bodyType = returnType.getGeneric(0); + bodyTypeParameter = new MethodParameter(result.getReturnTypeSource()); + bodyTypeParameter.increaseNestingLevel(); } return returnValueMono.then(returnValue -> { @@ -132,7 +140,7 @@ public class ResponseEntityResultHandler extends AbstractMessageConverterResultH .forEach(entry -> responseHeaders.put(entry.getKey(), entry.getValue())); } - return writeBody(exchange, httpEntity.getBody(), bodyType); + return writeBody(exchange, httpEntity.getBody(), bodyType, bodyTypeParameter); }); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/SimpleResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/SimpleResultHandlerTests.java index 0ebeddfa67..7aba79ee55 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/SimpleResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/SimpleResultHandlerTests.java @@ -16,6 +16,7 @@ package org.springframework.web.reactive.result; +import java.util.List; import java.util.concurrent.CompletableFuture; import org.junit.Before; @@ -26,10 +27,10 @@ import rx.Observable; import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; -import org.springframework.core.convert.support.GenericConversionService; import org.springframework.core.convert.support.MonoToCompletableFutureConverter; -import org.springframework.core.convert.support.PublisherToFluxConverter; import org.springframework.core.convert.support.ReactorToRxJava1Converter; +import org.springframework.format.support.DefaultFormattingConversionService; +import org.springframework.format.support.FormattingConversionService; import org.springframework.web.reactive.HandlerResult; import static org.junit.Assert.assertEquals; @@ -46,16 +47,15 @@ public class SimpleResultHandlerTests { @Before public void setUp() throws Exception { - GenericConversionService conversionService = new GenericConversionService(); - conversionService.addConverter(new MonoToCompletableFutureConverter()); - conversionService.addConverter(new PublisherToFluxConverter()); - conversionService.addConverter(new ReactorToRxJava1Converter()); - this.resultHandler = new SimpleResultHandler(conversionService); + FormattingConversionService service = new DefaultFormattingConversionService(); + service.addConverter(new MonoToCompletableFutureConverter()); + service.addConverter(new ReactorToRxJava1Converter()); + this.resultHandler = new SimpleResultHandler(service); } @Test - public void supportsWithConversionService() throws NoSuchMethodException { + public void supports() throws NoSuchMethodException { testSupports(ResolvableType.forClass(void.class), true); testSupports(ResolvableType.forClassWithGenerics(Publisher.class, Void.class), true); testSupports(ResolvableType.forClassWithGenerics(Flux.class, Void.class), true); @@ -66,6 +66,11 @@ public class SimpleResultHandlerTests { testSupports(ResolvableType.forClassWithGenerics(Publisher.class, String.class), false); } + @Test + public void supportsUsesGenericTypeInformation() throws Exception { + testSupports(ResolvableType.forClassWithGenerics(List.class, Void.class), false); + } + private void testSupports(ResolvableType type, boolean result) { MethodParameter param = ResolvableMethod.on(TestController.class).returning(type).resolveReturnType(); HandlerResult handlerResult = new HandlerResult(new TestController(), null, param); @@ -90,6 +95,8 @@ public class SimpleResultHandlerTests { public Publisher publisher() { return null; } + public List list() { return null; } + } } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterResultHandlerTests.java index 62d065193f..cde0c86a90 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterResultHandlerTests.java @@ -36,12 +36,12 @@ import reactor.core.publisher.Mono; import reactor.core.test.TestSubscriber; import rx.Observable; +import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.core.codec.ByteBufferEncoder; import org.springframework.core.codec.StringEncoder; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.core.convert.support.MonoToCompletableFutureConverter; -import org.springframework.core.convert.support.PublisherToFluxConverter; import org.springframework.core.convert.support.ReactorToRxJava1Converter; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; @@ -58,6 +58,7 @@ import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.util.ObjectUtils; import org.springframework.web.reactive.accept.RequestedContentTypeResolver; import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder; +import org.springframework.web.reactive.result.ResolvableMethod; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange; import org.springframework.web.server.session.MockWebSessionManager; @@ -92,8 +93,8 @@ public class MessageConverterResultHandlerTests { @Test // SPR-12894 public void useDefaultContentType() throws Exception { Resource body = new ClassPathResource("logo.png", getClass()); - ResolvableType bodyType = ResolvableType.forType(Resource.class); - this.resultHandler.writeBody(this.exchange, body, bodyType).block(Duration.ofSeconds(5)); + ResolvableType type = ResolvableType.forType(Resource.class); + this.resultHandler.writeBody(this.exchange, body, type, returnType(type)).block(Duration.ofSeconds(5)); assertEquals("image/x-png", this.response.getHeaders().getFirst("Content-Type")); } @@ -104,22 +105,22 @@ public class MessageConverterResultHandlerTests { Collections.singleton(APPLICATION_JSON)); String body = "foo"; - ResolvableType bodyType = ResolvableType.forType(String.class); - this.resultHandler.writeBody(this.exchange, body, bodyType).block(Duration.ofSeconds(5)); + ResolvableType type = ResolvableType.forType(String.class); + this.resultHandler.writeBody(this.exchange, body, type, returnType(type)).block(Duration.ofSeconds(5)); assertEquals(APPLICATION_JSON_UTF8, this.response.getHeaders().getContentType()); } @Test public void voidReturnType() throws Exception { - testVoidReturnType(null, ResolvableType.forType(Void.class)); + testVoidReturnType(null, ResolvableType.forType(void.class)); testVoidReturnType(Mono.empty(), ResolvableType.forClassWithGenerics(Mono.class, Void.class)); testVoidReturnType(Flux.empty(), ResolvableType.forClassWithGenerics(Flux.class, Void.class)); testVoidReturnType(Observable.empty(), ResolvableType.forClassWithGenerics(Observable.class, Void.class)); } - private void testVoidReturnType(Object body, ResolvableType bodyType) { - this.resultHandler.writeBody(this.exchange, body, bodyType).block(Duration.ofSeconds(5)); + private void testVoidReturnType(Object body, ResolvableType type) { + this.resultHandler.writeBody(this.exchange, body, type, returnType(type)).block(Duration.ofSeconds(5)); assertNull(this.response.getHeaders().get("Content-Type")); assertNull(this.response.getBody()); @@ -128,10 +129,10 @@ public class MessageConverterResultHandlerTests { @Test // SPR-13135 public void unsupportedReturnType() throws Exception { ByteArrayOutputStream body = new ByteArrayOutputStream(); - ResolvableType bodyType = ResolvableType.forType(OutputStream.class); + ResolvableType type = ResolvableType.forType(OutputStream.class); HttpMessageConverter converter = new CodecHttpMessageConverter<>(new ByteBufferEncoder()); - Mono mono = createResultHandler(converter).writeBody(this.exchange, body, bodyType); + Mono mono = createResultHandler(converter).writeBody(this.exchange, body, type, returnType(type)); TestSubscriber.subscribe(mono).assertError(IllegalStateException.class); } @@ -139,8 +140,8 @@ public class MessageConverterResultHandlerTests { @Test // SPR-12811 public void jacksonTypeOfListElement() throws Exception { List body = Arrays.asList(new Foo("foo"), new Bar("bar")); - ResolvableType bodyType = ResolvableType.forClassWithGenerics(List.class, ParentClass.class); - this.resultHandler.writeBody(this.exchange, body, bodyType).block(Duration.ofSeconds(5)); + ResolvableType type = ResolvableType.forClassWithGenerics(List.class, ParentClass.class); + this.resultHandler.writeBody(this.exchange, body, type, returnType(type)).block(Duration.ofSeconds(5)); assertEquals(APPLICATION_JSON_UTF8, this.response.getHeaders().getContentType()); assertResponseBody("[{\"type\":\"foo\",\"parentProperty\":\"foo\"}," + @@ -151,8 +152,8 @@ public class MessageConverterResultHandlerTests { @Ignore public void jacksonTypeWithSubType() throws Exception { SimpleBean body = new SimpleBean(123L, "foo"); - ResolvableType bodyType = ResolvableType.forClass(Identifiable.class); - this.resultHandler.writeBody(this.exchange, body, bodyType).block(Duration.ofSeconds(5)); + ResolvableType type = ResolvableType.forClass(Identifiable.class); + this.resultHandler.writeBody(this.exchange, body, type, returnType(type)).block(Duration.ofSeconds(5)); assertEquals(APPLICATION_JSON_UTF8, this.response.getHeaders().getContentType()); assertResponseBody("{\"id\":123,\"name\":\"foo\"}"); @@ -162,14 +163,18 @@ public class MessageConverterResultHandlerTests { @Ignore public void jacksonTypeWithSubTypeOfListElement() throws Exception { List body = Arrays.asList(new SimpleBean(123L, "foo"), new SimpleBean(456L, "bar")); - ResolvableType bodyType = ResolvableType.forClassWithGenerics(List.class, Identifiable.class); - this.resultHandler.writeBody(this.exchange, body, bodyType).block(Duration.ofSeconds(5)); + ResolvableType type = ResolvableType.forClassWithGenerics(List.class, Identifiable.class); + this.resultHandler.writeBody(this.exchange, body, type, returnType(type)).block(Duration.ofSeconds(5)); assertEquals(APPLICATION_JSON_UTF8, this.response.getHeaders().getContentType()); assertResponseBody("[{\"id\":123,\"name\":\"foo\"},{\"id\":456,\"name\":\"bar\"}]"); } + private MethodParameter returnType(ResolvableType bodyType) { + return ResolvableMethod.on(TestController.class).returning(bodyType).resolveReturnType(); + } + private AbstractMessageConverterResultHandler createResultHandler(HttpMessageConverter... converters) { List> converterList; if (ObjectUtils.isEmpty(converters)) { @@ -186,7 +191,6 @@ public class MessageConverterResultHandlerTests { GenericConversionService service = new GenericConversionService(); service.addConverter(new MonoToCompletableFutureConverter()); - service.addConverter(new PublisherToFluxConverter()); service.addConverter(new ReactorToRxJava1Converter()); RequestedContentTypeResolver resolver = new RequestedContentTypeResolverBuilder().build(); @@ -267,4 +271,29 @@ public class MessageConverterResultHandlerTests { } } + @SuppressWarnings("unused") + private static class TestController { + + Resource resource() { return null; } + + String string() { return null; } + + void voidReturn() { } + + Mono monoVoid() { return null; } + + Flux fluxVoid() { return null; } + + Observable observableVoid() { return null; } + + OutputStream outputStream() { return null; } + + List listParentClass() { return null; } + + Identifiable identifiable() { return null; } + + List listIdentifiable() { return null; } + + } + } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java index 5e9ecb720f..d0171d69da 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java @@ -42,16 +42,16 @@ import org.springframework.core.MethodParameter; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.annotation.SynthesizingMethodParameter; import org.springframework.core.codec.Decoder; -import org.springframework.core.convert.support.PublisherToFluxConverter; -import org.springframework.http.codec.json.JacksonJsonDecoder; import org.springframework.core.codec.StringDecoder; -import org.springframework.core.convert.support.GenericConversionService; import org.springframework.core.convert.support.MonoToCompletableFutureConverter; import org.springframework.core.convert.support.ReactorToRxJava1Converter; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DefaultDataBufferFactory; +import org.springframework.format.support.DefaultFormattingConversionService; +import org.springframework.format.support.FormattingConversionService; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; +import org.springframework.http.codec.json.JacksonJsonDecoder; import org.springframework.http.converter.reactive.CodecHttpMessageConverter; import org.springframework.http.converter.reactive.HttpMessageConverter; import org.springframework.http.server.reactive.MockServerHttpRequest; @@ -202,23 +202,29 @@ public class RequestBodyArgumentResolverTests { @SuppressWarnings("unchecked") private T resolveValue(String paramName, Class valueType, String body) { + this.request.getHeaders().setContentType(MediaType.APPLICATION_JSON); this.request.writeWith(Flux.just(dataBuffer(body))); + Mono result = this.resolver.resolveArgument(parameter(paramName), this.model, this.exchange); Object value = result.block(Duration.ofSeconds(5)); + assertNotNull(value); assertTrue("Actual type: " + value.getClass(), valueType.isAssignableFrom(value.getClass())); + return (T) value; } @SuppressWarnings("Convert2MethodRef") private RequestBodyArgumentResolver resolver(Decoder... decoders) { + List> converters = new ArrayList<>(); Arrays.asList(decoders).forEach(decoder -> converters.add(new CodecHttpMessageConverter<>(decoder))); - GenericConversionService service = new GenericConversionService(); + + FormattingConversionService service = new DefaultFormattingConversionService(); service.addConverter(new MonoToCompletableFutureConverter()); - service.addConverter(new PublisherToFluxConverter()); service.addConverter(new ReactorToRxJava1Converter()); + return new RequestBodyArgumentResolver(converters, service, new TestBeanValidator()); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java index 38da2644c9..06efa14061 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java @@ -28,10 +28,10 @@ import reactor.core.publisher.Mono; import org.springframework.core.codec.ByteBufferEncoder; import org.springframework.core.codec.StringEncoder; import org.springframework.core.convert.support.DefaultConversionService; -import org.springframework.core.convert.support.GenericConversionService; import org.springframework.core.convert.support.MonoToCompletableFutureConverter; -import org.springframework.core.convert.support.PublisherToFluxConverter; import org.springframework.core.convert.support.ReactorToRxJava1Converter; +import org.springframework.format.support.DefaultFormattingConversionService; +import org.springframework.format.support.FormattingConversionService; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.http.codec.json.JacksonJsonEncoder; @@ -99,9 +99,8 @@ public class ResponseBodyResultHandlerTests { else { converterList = Arrays.asList(converters); } - GenericConversionService service = new GenericConversionService(); + FormattingConversionService service = new DefaultFormattingConversionService(); service.addConverter(new MonoToCompletableFutureConverter()); - service.addConverter(new PublisherToFluxConverter()); service.addConverter(new ReactorToRxJava1Converter()); RequestedContentTypeResolver resolver = new RequestedContentTypeResolverBuilder().build(); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java index c90ddb8334..b54994f170 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java @@ -33,11 +33,11 @@ import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.core.codec.ByteBufferEncoder; import org.springframework.core.codec.StringEncoder; -import org.springframework.core.convert.support.GenericConversionService; import org.springframework.core.convert.support.MonoToCompletableFutureConverter; -import org.springframework.core.convert.support.PublisherToFluxConverter; import org.springframework.core.convert.support.ReactorToRxJava1Converter; import org.springframework.core.io.buffer.support.DataBufferTestUtils; +import org.springframework.format.support.DefaultFormattingConversionService; +import org.springframework.format.support.FormattingConversionService; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -102,9 +102,8 @@ public class ResponseEntityResultHandlerTests { else { converterList = Arrays.asList(converters); } - GenericConversionService service = new GenericConversionService(); + FormattingConversionService service = new DefaultFormattingConversionService(); service.addConverter(new MonoToCompletableFutureConverter()); - service.addConverter(new PublisherToFluxConverter()); service.addConverter(new ReactorToRxJava1Converter()); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java index 4f8b7ed511..03b6581a24 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java @@ -38,13 +38,13 @@ import rx.Single; import org.springframework.core.MethodParameter; import org.springframework.core.Ordered; import org.springframework.core.ResolvableType; -import org.springframework.core.convert.support.ConfigurableConversionService; -import org.springframework.core.convert.support.DefaultConversionService; -import org.springframework.core.convert.support.PublisherToFluxConverter; +import org.springframework.core.convert.support.MonoToCompletableFutureConverter; import org.springframework.core.convert.support.ReactorToRxJava1Converter; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.core.io.buffer.support.DataBufferTestUtils; +import org.springframework.format.support.DefaultFormattingConversionService; +import org.springframework.format.support.FormattingConversionService; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.http.server.reactive.MockServerHttpRequest; @@ -264,10 +264,12 @@ public class ViewResolutionResultHandlerTests { } private ViewResolutionResultHandler createResultHandler(List defaultViews, ViewResolver... resolvers) { - ConfigurableConversionService service = new DefaultConversionService(); + + FormattingConversionService service = new DefaultFormattingConversionService(); + service.addConverter(new MonoToCompletableFutureConverter()); service.addConverter(new ReactorToRxJava1Converter()); - service.addConverter(new PublisherToFluxConverter()); List resolverList = Arrays.asList(resolvers); + ViewResolutionResultHandler handler = new ViewResolutionResultHandler(resolverList, service); handler.setDefaultViews(defaultViews); return handler; From b7b0313c212e8d84586b40bae1d9257389e29817 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Mon, 4 Jul 2016 08:45:47 +0200 Subject: [PATCH 455/498] Speed up RequestMappingIntegrationTests Reactor Core + Spring Reactive now handle correctly shorter interval for the stream result test. --- .../annotation/RequestMappingIntegrationTests.java | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java index 826efa4bbd..58b132dfbe 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java @@ -18,7 +18,6 @@ package org.springframework.web.reactive.result.method.annotation; import java.net.URI; import java.nio.ByteBuffer; -import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -434,13 +433,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @RequestMapping("/stream-result") public Publisher stringStreamResponseBody() { - /* - TODO: replace the following line with: - return Flux.interval(Duration.ofMillis(100)).take(5); - to make the build last shorter. Unfortunately, this hangs the build as of - 20160317. - */ - return Flux.interval(Duration.ofSeconds(1)).take(5); + return Flux.interval(100).take(5); } @RequestMapping("/raw-flux") From 0f01729b27e1cdfa37a871e6d10e9d9027c683f0 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Mon, 4 Jul 2016 09:52:39 +0200 Subject: [PATCH 456/498] Allow to configure Tomcat baseDir This commit also set Tomcat baseDir to java.io.tmpdir for integration tests in order to avoid creation of temporary directories in the project root. --- .../server/reactive/boot/TomcatHttpServer.java | 15 +++++++++++++-- .../AbstractHttpHandlerIntegrationTests.java | 5 ++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/TomcatHttpServer.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/TomcatHttpServer.java index d0170d890b..acbb4568d3 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/TomcatHttpServer.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/TomcatHttpServer.java @@ -20,13 +20,11 @@ import java.io.File; import org.apache.catalina.Context; import org.apache.catalina.LifecycleException; -import org.apache.catalina.core.StandardHost; import org.apache.catalina.startup.Tomcat; import org.springframework.beans.factory.InitializingBean; import org.springframework.http.server.reactive.ServletHttpHandlerAdapter; import org.springframework.util.Assert; -import org.springframework.util.SocketUtils; /** @@ -38,6 +36,16 @@ public class TomcatHttpServer extends HttpServerSupport implements InitializingB private boolean running; + private String baseDir; + + + public TomcatHttpServer() { + } + + public TomcatHttpServer(String baseDir) { + this.baseDir = baseDir; + } + @Override public boolean isRunning() { @@ -48,6 +56,9 @@ public class TomcatHttpServer extends HttpServerSupport implements InitializingB public void afterPropertiesSet() throws Exception { this.tomcatServer = new Tomcat(); + if (this.baseDir != null) { + this.tomcatServer.setBaseDir(baseDir); + } this.tomcatServer.setHostname(getHost()); this.tomcatServer.setPort(getPort()); diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AbstractHttpHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AbstractHttpHandlerIntegrationTests.java index 83781e29f9..4a2b94e2db 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AbstractHttpHandlerIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AbstractHttpHandlerIntegrationTests.java @@ -16,6 +16,8 @@ package org.springframework.http.server.reactive; +import java.io.File; + import org.junit.After; import org.junit.Before; import org.junit.runner.RunWith; @@ -41,11 +43,12 @@ public abstract class AbstractHttpHandlerIntegrationTests { @Parameterized.Parameters(name = "server [{0}]") public static Object[][] arguments() { + File base = new File(System.getProperty("java.io.tmpdir")); return new Object[][] { {new JettyHttpServer()}, {new RxNettyHttpServer()}, {new ReactorHttpServer()}, - {new TomcatHttpServer()}, + {new TomcatHttpServer(base.getAbsolutePath())}, {new UndertowHttpServer()} }; } From f254680f291334ced941d539ceab7c90f5954371 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Mon, 4 Jul 2016 10:06:02 +0200 Subject: [PATCH 457/498] Remove unused imports --- .../java/org/springframework/core/codec/AbstractEncoder.java | 1 - .../java/org/springframework/core/codec/ResourceDecoder.java | 1 - .../java/org/springframework/core/codec/ResourceEncoder.java | 1 - .../http/server/reactive/AbstractResponseBodySubscriber.java | 1 - .../http/server/reactive/boot/HttpServerSupport.java | 2 -- .../web/reactive/result/condition/MediaTypeExpression.java | 1 - .../web/reactive/result/condition/NameValueExpression.java | 2 -- .../web/reactive/result/condition/RequestCondition.java | 2 -- .../org/springframework/web/reactive/result/view/View.java | 3 --- .../org/springframework/core/codec/ByteBufferDecoderTests.java | 1 - .../org/springframework/core/codec/ByteBufferEncoderTests.java | 1 - .../org/springframework/core/codec/ResourceDecoderTests.java | 1 - .../org/springframework/core/codec/ResourceEncoderTests.java | 1 - .../org/springframework/core/codec/StringDecoderTests.java | 1 - .../org/springframework/core/codec/StringEncoderTests.java | 1 - .../convert/support/MonoToCompletableFutureConverterTests.java | 2 -- .../http/codec/json/JacksonJsonEncoderTests.java | 1 - .../springframework/http/codec/xml/XmlEventDecoderTests.java | 1 - .../http/server/reactive/ChannelSendOperatorTests.java | 1 - .../web/reactive/result/WebHandlerIntegrationTests.java | 2 -- .../reactive/result/view/freemarker/FreeMarkerViewTests.java | 1 - 21 files changed, 28 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/AbstractEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/AbstractEncoder.java index 10a60be070..35ce3803bf 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/AbstractEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/AbstractEncoder.java @@ -21,7 +21,6 @@ import java.util.Collections; import java.util.List; import org.springframework.core.ResolvableType; -import org.springframework.core.codec.Encoder; import org.springframework.util.MimeType; /** diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/ResourceDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/ResourceDecoder.java index 5694887b4b..a7a9d45660 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/ResourceDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/ResourceDecoder.java @@ -23,7 +23,6 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.core.ResolvableType; -import org.springframework.core.codec.AbstractDecoder; import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.InputStreamResource; import org.springframework.core.io.Resource; diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/ResourceEncoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/ResourceEncoder.java index 8c9d0bd362..3823c4d709 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/ResourceEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/ResourceEncoder.java @@ -22,7 +22,6 @@ import java.io.InputStream; import reactor.core.publisher.Flux; import org.springframework.core.ResolvableType; -import org.springframework.core.codec.AbstractSingleValueEncoder; import org.springframework.core.io.Resource; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractResponseBodySubscriber.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractResponseBodySubscriber.java index e1b6457453..049d74b205 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractResponseBodySubscriber.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractResponseBodySubscriber.java @@ -26,7 +26,6 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; -import reactor.core.util.BackpressureUtils; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.FlushingDataBuffer; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/HttpServerSupport.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/HttpServerSupport.java index bb710f40b4..c238f60e55 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/HttpServerSupport.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/HttpServerSupport.java @@ -16,8 +16,6 @@ package org.springframework.http.server.reactive.boot; - -import org.springframework.beans.factory.InitializingBean; import org.springframework.http.server.reactive.HttpHandler; import org.springframework.util.SocketUtils; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/MediaTypeExpression.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/MediaTypeExpression.java index 6e9a3265d7..23b3a695b9 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/MediaTypeExpression.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/MediaTypeExpression.java @@ -17,7 +17,6 @@ package org.springframework.web.reactive.result.condition; import org.springframework.http.MediaType; -import org.springframework.web.bind.annotation.RequestMapping; /** * A contract for media type expressions (e.g. "text/plain", "!text/plain") as diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/NameValueExpression.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/NameValueExpression.java index 8818330c02..1f4dd561ae 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/NameValueExpression.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/NameValueExpression.java @@ -16,8 +16,6 @@ package org.springframework.web.reactive.result.condition; -import org.springframework.web.bind.annotation.RequestMapping; - /** * A contract for {@code "name!=value"} style expression used to specify request * parameters and request header conditions in {@code @RequestMapping}. diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/RequestCondition.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/RequestCondition.java index 99bc2f2aa8..d21f85829b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/RequestCondition.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/RequestCondition.java @@ -16,8 +16,6 @@ package org.springframework.web.reactive.result.condition; -import javax.servlet.http.HttpServletRequest; - import org.springframework.web.server.ServerWebExchange; /** diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/View.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/View.java index a6e82eb517..3b9ee66f56 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/View.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/View.java @@ -16,12 +16,9 @@ package org.springframework.web.reactive.result.view; import java.util.List; -import java.util.Optional; -import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.MediaType; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.server.ServerWebExchange; diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/ByteBufferDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/ByteBufferDecoderTests.java index d44f7cb544..8162200f34 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/ByteBufferDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/ByteBufferDecoderTests.java @@ -24,7 +24,6 @@ import reactor.core.publisher.Flux; import reactor.core.test.TestSubscriber; import org.springframework.core.ResolvableType; -import org.springframework.core.codec.ByteBufferDecoder; import org.springframework.core.io.buffer.AbstractDataBufferAllocatingTestCase; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.MediaType; diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/ByteBufferEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/ByteBufferEncoderTests.java index 5160395698..dc44221899 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/ByteBufferEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/ByteBufferEncoderTests.java @@ -26,7 +26,6 @@ import reactor.core.publisher.Flux; import reactor.core.test.TestSubscriber; import org.springframework.core.ResolvableType; -import org.springframework.core.codec.ByteBufferEncoder; import org.springframework.core.io.buffer.AbstractDataBufferAllocatingTestCase; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.MediaType; diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/ResourceDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/ResourceDecoderTests.java index 22b8e5a426..15db097214 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/ResourceDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/ResourceDecoderTests.java @@ -23,7 +23,6 @@ import reactor.core.publisher.Flux; import reactor.core.test.TestSubscriber; import org.springframework.core.ResolvableType; -import org.springframework.core.codec.ResourceDecoder; import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.InputStreamResource; import org.springframework.core.io.Resource; diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/ResourceEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/ResourceEncoderTests.java index fb747bb3a0..75bb8a07c3 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/ResourceEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/ResourceEncoderTests.java @@ -24,7 +24,6 @@ import reactor.core.publisher.Mono; import reactor.core.test.TestSubscriber; import org.springframework.core.ResolvableType; -import org.springframework.core.codec.ResourceEncoder; import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.InputStreamResource; import org.springframework.core.io.Resource; diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/StringDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/StringDecoderTests.java index adce87826a..c6624efdeb 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/StringDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/StringDecoderTests.java @@ -23,7 +23,6 @@ import reactor.core.publisher.Mono; import reactor.core.test.TestSubscriber; import org.springframework.core.ResolvableType; -import org.springframework.core.codec.StringDecoder; import org.springframework.core.io.buffer.AbstractDataBufferAllocatingTestCase; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.MediaType; diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/StringEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/StringEncoderTests.java index 850775311f..6bcf51c6ca 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/StringEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/StringEncoderTests.java @@ -26,7 +26,6 @@ import reactor.core.publisher.Flux; import reactor.core.test.TestSubscriber; import org.springframework.core.ResolvableType; -import org.springframework.core.codec.StringEncoder; import org.springframework.core.io.buffer.AbstractDataBufferAllocatingTestCase; import org.springframework.core.io.buffer.support.DataBufferUtils; import org.springframework.http.MediaType; diff --git a/spring-web-reactive/src/test/java/org/springframework/core/convert/support/MonoToCompletableFutureConverterTests.java b/spring-web-reactive/src/test/java/org/springframework/core/convert/support/MonoToCompletableFutureConverterTests.java index c62ee2b12e..64b33cc9b8 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/convert/support/MonoToCompletableFutureConverterTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/convert/support/MonoToCompletableFutureConverterTests.java @@ -22,8 +22,6 @@ import org.junit.Test; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import rx.Observable; -import rx.Single; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; diff --git a/spring-web-reactive/src/test/java/org/springframework/http/codec/json/JacksonJsonEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/http/codec/json/JacksonJsonEncoderTests.java index cc7bf4d4bd..6f33966118 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/codec/json/JacksonJsonEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/codec/json/JacksonJsonEncoderTests.java @@ -28,7 +28,6 @@ import org.springframework.core.io.buffer.AbstractDataBufferAllocatingTestCase; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.MediaType; import org.springframework.http.codec.Pojo; -import org.springframework.http.codec.json.JacksonJsonEncoder; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; diff --git a/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/XmlEventDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/XmlEventDecoderTests.java index fa942c9b94..09a82eb9ac 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/XmlEventDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/XmlEventDecoderTests.java @@ -23,7 +23,6 @@ import reactor.core.publisher.Flux; import reactor.core.test.TestSubscriber; import org.springframework.core.io.buffer.AbstractDataBufferAllocatingTestCase; -import org.springframework.http.codec.xml.XmlEventDecoder; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ChannelSendOperatorTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ChannelSendOperatorTests.java index e6c056b7f8..730e88f088 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ChannelSendOperatorTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ChannelSendOperatorTests.java @@ -20,7 +20,6 @@ import java.util.Arrays; import java.util.List; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; import org.junit.Before; import org.junit.Test; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/WebHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/WebHandlerIntegrationTests.java index 0f45e8826a..73c45012ae 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/WebHandlerIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/WebHandlerIntegrationTests.java @@ -26,11 +26,9 @@ import org.junit.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import org.springframework.beans.MutablePropertyValues; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.context.support.StaticApplicationContext; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DefaultDataBufferFactory; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerViewTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerViewTests.java index fac9daa7ff..147c269acc 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerViewTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerViewTests.java @@ -36,7 +36,6 @@ import org.springframework.http.server.reactive.MockServerHttpRequest; import org.springframework.http.server.reactive.MockServerHttpResponse; import org.springframework.ui.ExtendedModelMap; import org.springframework.ui.ModelMap; -import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange; From a1d4fe69389da2de69ce8c9c6d3953807f86f623 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Mon, 4 Jul 2016 10:17:54 +0200 Subject: [PATCH 458/498] Upgrade dependencies --- spring-web-reactive/build.gradle | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index 874db4012b..1ddf309863 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -27,14 +27,14 @@ repositories { } ext { - springVersion = '4.3.0.BUILD-SNAPSHOT' + springVersion = '4.3.0.RELEASE' reactorVersion = '2.5.0.BUILD-SNAPSHOT' reactorNettyVersion = '2.5.0.BUILD-SNAPSHOT' - rxJavaVersion = '1.1.5' - tomcatVersion = '8.5.2' - jettyVersion = '9.3.8.v20160314' - nettyVersion = '4.1.0.Final' - jacksonVersion = '2.7.3' + rxJavaVersion = '1.1.6' + tomcatVersion = '8.5.3' + jettyVersion = '9.3.10.v20160621' + nettyVersion = '4.1.2.Final' + jacksonVersion = '2.7.5' javadocLinks = [ "http://docs.oracle.com/javase/8/docs/api/", From b7b6704c7e4368e9b3ed68d0f241ec1e325ef851 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Mon, 4 Jul 2016 11:05:49 +0200 Subject: [PATCH 459/498] Support rx.Completable --- .../convert/support/ReactorToRxJava1Converter.java | 10 ++++++++++ .../support/ReactorToRxJava1ConverterTests.java | 7 +++++++ 2 files changed, 17 insertions(+) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactorToRxJava1Converter.java b/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactorToRxJava1Converter.java index 9b7a661e21..67c66bae9d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactorToRxJava1Converter.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactorToRxJava1Converter.java @@ -20,10 +20,12 @@ import java.util.LinkedHashSet; import java.util.Set; import org.reactivestreams.Publisher; +import reactor.core.converter.RxJava1CompletableConverter; import reactor.core.converter.RxJava1ObservableConverter; import reactor.core.converter.RxJava1SingleConverter; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import rx.Completable; import rx.Observable; import rx.Single; @@ -43,6 +45,8 @@ public final class ReactorToRxJava1Converter implements GenericConverter { pairs.add(new GenericConverter.ConvertiblePair(Observable.class, Flux.class)); pairs.add(new GenericConverter.ConvertiblePair(Mono.class, Single.class)); pairs.add(new GenericConverter.ConvertiblePair(Single.class, Mono.class)); + pairs.add(new GenericConverter.ConvertiblePair(Mono.class, Completable.class)); + pairs.add(new GenericConverter.ConvertiblePair(Completable.class, Mono.class)); return pairs; } @@ -63,6 +67,12 @@ public final class ReactorToRxJava1Converter implements GenericConverter { else if (Single.class.isAssignableFrom(targetType.getType())) { return RxJava1SingleConverter.from((Publisher) source); } + else if (Completable.class.isAssignableFrom(sourceType.getType())) { + return RxJava1CompletableConverter.from((Completable) source); + } + else if (Completable.class.isAssignableFrom(targetType.getType())) { + return RxJava1CompletableConverter.from((Publisher) source); + } return null; } diff --git a/spring-web-reactive/src/test/java/org/springframework/core/convert/support/ReactorToRxJava1ConverterTests.java b/spring-web-reactive/src/test/java/org/springframework/core/convert/support/ReactorToRxJava1ConverterTests.java index bc6d2ac29a..427e9be4ef 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/convert/support/ReactorToRxJava1ConverterTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/convert/support/ReactorToRxJava1ConverterTests.java @@ -20,6 +20,7 @@ import org.junit.Test; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import rx.Completable; import rx.Observable; import rx.Single; @@ -49,9 +50,15 @@ public class ReactorToRxJava1ConverterTests { assertTrue(this.conversionService.canConvert(Mono.class, Single.class)); assertTrue(this.conversionService.canConvert(Single.class, Mono.class)); + assertTrue(this.conversionService.canConvert(Mono.class, Completable.class)); + assertTrue(this.conversionService.canConvert(Completable.class, Mono.class)); + assertFalse(this.conversionService.canConvert(Flux.class, Single.class)); assertFalse(this.conversionService.canConvert(Single.class, Flux.class)); + assertFalse(this.conversionService.canConvert(Flux.class, Completable.class)); + assertFalse(this.conversionService.canConvert(Completable.class, Flux.class)); + assertFalse(this.conversionService.canConvert(Mono.class, Observable.class)); assertFalse(this.conversionService.canConvert(Observable.class, Mono.class)); From c1518c3fdef8822e26ca4c82ad43a46dbb5dede1 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Mon, 4 Jul 2016 12:03:04 +0200 Subject: [PATCH 460/498] Avoid exception while setting the status code of a committed response Before this commit, it was not possible to set the status code of an HTTP response without throwing an exception if it was already committed. The consequence was a lot of errors in the logs for long lived HTTP exchanges like Server-Sent Events for example. After this commit, ServerHttpResponse#setStatusCode() returns true if the operation succeeded and false if the status code has not been set because the response has already been committed. In term of implementation, that makes status code managed consistently with headers and cookies: AbstractServerHttpResponse#setStatusCode() stores the status code that will be effectively set later in the response lifecycle by the HTTP server via AbstractServerHttpResponse#writeStatusCode() when the response will be committed. --- .../reactive/AbstractServerHttpResponse.java | 33 +++++++++++++++++++ .../reactive/ReactorServerHttpResponse.java | 13 +++++--- .../reactive/RxNettyServerHttpResponse.java | 13 +++++--- .../server/reactive/ServerHttpResponse.java | 5 ++- .../reactive/ServletServerHttpResponse.java | 11 ++++--- .../reactive/UndertowServerHttpResponse.java | 14 ++++---- .../reactive/MockServerHttpResponse.java | 3 +- .../reactive/ServerHttpResponseTests.java | 14 ++++++-- 8 files changed, 82 insertions(+), 24 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java index 2e478a1db4..a1132ef6a9 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java @@ -21,12 +21,15 @@ import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Supplier; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseCookie; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -38,15 +41,20 @@ import org.springframework.util.MultiValueMap; * Base class for {@link ServerHttpResponse} implementations. * * @author Rossen Stoyanchev + * @author Sebastien Deleuze */ public abstract class AbstractServerHttpResponse implements ServerHttpResponse { + private static Log logger = LogFactory.getLog(AbstractServerHttpResponse.class); + private static final int STATE_NEW = 1; private static final int STATE_COMMITTING = 2; private static final int STATE_COMMITTED = 3; + private HttpStatus statusCode; + private final HttpHeaders headers; private final MultiValueMap cookies; @@ -70,6 +78,10 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse { return this.dataBufferFactory; } + protected HttpStatus getStatusCode() { + return statusCode; + } + @Override public HttpHeaders getHeaders() { if (STATE_COMMITTED == this.state.get()) { @@ -106,6 +118,7 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse { }); mono = mono.then(() -> { this.state.set(STATE_COMMITTED); + writeStatusCode(); writeHeaders(); writeCookies(); return Mono.empty(); @@ -114,6 +127,12 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse { return mono; } + /** + * Implement this method to write the status code to the underlying response. + * This method is called once only. + */ + protected abstract void writeStatusCode(); + /** * Implement this method to apply header changes from {@link #getHeaders()} * to the underlying response. This method is called once only. @@ -143,4 +162,18 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse { return applyBeforeCommit(); } + @Override + public boolean setStatusCode(HttpStatus statusCode) { + Assert.notNull(statusCode); + if (STATE_NEW == this.state.get()) { + this.statusCode = statusCode; + return true; + } + else if (logger.isDebugEnabled()) { + logger.debug("Can't set the status " + statusCode.toString() + + " because the HTTP response has already been committed"); + } + return false; + } + } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java index fd2467ec09..5ca6ca3bc8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java @@ -60,11 +60,6 @@ public class ReactorServerHttpResponse extends AbstractServerHttpResponse return this.channel; } - @Override - public void setStatusCode(HttpStatus status) { - getReactorChannel().status(HttpResponseStatus.valueOf(status.value())); - } - @Override protected Mono writeWithInternal(Publisher publisher) { return Flux.from(publisher) @@ -75,6 +70,14 @@ public class ReactorServerHttpResponse extends AbstractServerHttpResponse .then(); } + @Override + protected void writeStatusCode() { + HttpStatus statusCode = this.getStatusCode(); + if (statusCode != null) { + getReactorChannel().status(HttpResponseStatus.valueOf(statusCode.value())); + } + } + @Override protected void writeHeaders() { for (String name : getHeaders().keySet()) { diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java index 61aac8cc6f..80aadad457 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java @@ -59,11 +59,6 @@ public class RxNettyServerHttpResponse extends AbstractServerHttpResponse { return this.response; } - @Override - public void setStatusCode(HttpStatus status) { - this.response.setStatus(HttpResponseStatus.valueOf(status.value())); - } - @Override protected Mono writeWithInternal(Publisher body) { Observable content = RxJava1ObservableConverter.from(body).map(this::toByteBuf); @@ -75,6 +70,14 @@ public class RxNettyServerHttpResponse extends AbstractServerHttpResponse { return (buffer instanceof FlushingDataBuffer ? new FlushingByteBuf(byteBuf) : byteBuf); } + @Override + protected void writeStatusCode() { + HttpStatus statusCode = this.getStatusCode(); + if (statusCode != null) { + this.response.setStatus(HttpResponseStatus.valueOf(statusCode.value())); + } + } + @Override protected void writeHeaders() { for (String name : getHeaders().keySet()) { diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java index 4e641499f5..c458cfcc7e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java @@ -27,14 +27,17 @@ import org.springframework.util.MultiValueMap; * Represents a reactive server-side HTTP response. * * @author Arjen Poutsma + * @author Sebastien Deleuze */ public interface ServerHttpResponse extends ReactiveHttpOutputMessage { /** * Set the HTTP status code of the response. * @param status the HTTP status as an {@link HttpStatus} enum value + * @return {@code false} if the status code has not been set because the HTTP response + * is already committed, {@code true} if it has been set correctly. */ - void setStatusCode(HttpStatus status); + boolean setStatusCode(HttpStatus status); /** * Return a mutable map with the cookies to send to the server. diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java index 926315c204..60cca26f23 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java @@ -63,13 +63,16 @@ public class ServletServerHttpResponse extends AbstractServerHttpResponse { } @Override - public void setStatusCode(HttpStatus status) { - getServletResponse().setStatus(status.value()); + protected Mono writeWithInternal(Publisher publisher) { + return this.responseBodyWriter.apply(publisher); } @Override - protected Mono writeWithInternal(Publisher publisher) { - return this.responseBodyWriter.apply(publisher); + protected void writeStatusCode() { + HttpStatus statusCode = this.getStatusCode(); + if (statusCode != null) { + getServletResponse().setStatus(statusCode.value()); + } } @Override diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java index 0dfb54f26b..73fba1d28a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java @@ -66,12 +66,6 @@ public class UndertowServerHttpResponse extends AbstractServerHttpResponse return this.exchange; } - @Override - public void setStatusCode(HttpStatus status) { - Assert.notNull(status); - getUndertowExchange().setStatusCode(status.value()); - } - @Override protected Mono writeWithInternal(Publisher publisher) { return Mono.from(s -> { @@ -107,6 +101,14 @@ public class UndertowServerHttpResponse extends AbstractServerHttpResponse } } + @Override + protected void writeStatusCode() { + HttpStatus statusCode = this.getStatusCode(); + if (statusCode != null) { + getUndertowExchange().setStatusCode(statusCode.value()); + } + } + @Override protected void writeHeaders() { for (Map.Entry> entry : getHeaders().entrySet()) { diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java index 30dbda1681..c12ba69f94 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java @@ -49,8 +49,9 @@ public class MockServerHttpResponse implements ServerHttpResponse { @Override - public void setStatusCode(HttpStatus status) { + public boolean setStatusCode(HttpStatus status) { this.status = status; + return true; } public HttpStatus getStatus() { diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java index 6eff9434bf..a5e5637605 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java @@ -28,7 +28,6 @@ import reactor.core.publisher.Mono; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DefaultDataBufferFactory; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseCookie; import static junit.framework.TestCase.assertTrue; @@ -36,6 +35,7 @@ import static org.junit.Assert.*; /** * @author Rossen Stoyanchev + * @author Sebastien Deleuze */ public class ServerHttpResponseTests { @@ -47,6 +47,7 @@ public class ServerHttpResponseTests { TestServerHttpResponse response = new TestServerHttpResponse(); response.writeWith(Flux.just(wrap("a"), wrap("b"), wrap("c"))).block(); + assertTrue(response.statusCodeWritten); assertTrue(response.headersWritten); assertTrue(response.cookiesWritten); @@ -62,6 +63,7 @@ public class ServerHttpResponseTests { IllegalStateException error = new IllegalStateException("boo"); response.writeWith(Flux.error(error)).otherwise(ex -> Mono.empty()).block(); + assertFalse(response.statusCodeWritten); assertFalse(response.headersWritten); assertFalse(response.cookiesWritten); assertTrue(response.body.isEmpty()); @@ -72,6 +74,7 @@ public class ServerHttpResponseTests { TestServerHttpResponse response = new TestServerHttpResponse(); response.setComplete().block(); + assertTrue(response.statusCodeWritten); assertTrue(response.headersWritten); assertTrue(response.cookiesWritten); assertTrue(response.body.isEmpty()); @@ -87,6 +90,7 @@ public class ServerHttpResponseTests { }); response.writeWith(Flux.just(wrap("a"), wrap("b"), wrap("c"))).block(); + assertTrue(response.statusCodeWritten); assertTrue(response.headersWritten); assertTrue(response.cookiesWritten); assertSame(cookie, response.getCookies().getFirst("ID")); @@ -104,6 +108,7 @@ public class ServerHttpResponseTests { response.beforeCommit(() -> Mono.error(error)); response.writeWith(Flux.just(wrap("a"), wrap("b"), wrap("c"))).block(); + assertTrue("beforeCommit action errors should be ignored", response.statusCodeWritten); assertTrue("beforeCommit action errors should be ignored", response.headersWritten); assertTrue("beforeCommit action errors should be ignored", response.cookiesWritten); assertNull(response.getCookies().get("ID")); @@ -124,6 +129,7 @@ public class ServerHttpResponseTests { }); response.setComplete().block(); + assertTrue(response.statusCodeWritten); assertTrue(response.headersWritten); assertTrue(response.cookiesWritten); assertTrue(response.body.isEmpty()); @@ -139,6 +145,8 @@ public class ServerHttpResponseTests { private static class TestServerHttpResponse extends AbstractServerHttpResponse { + private boolean statusCodeWritten; + private boolean headersWritten; private boolean cookiesWritten; @@ -150,7 +158,9 @@ public class ServerHttpResponseTests { } @Override - public void setStatusCode(HttpStatus status) { + public void writeStatusCode() { + assertFalse(this.statusCodeWritten); + this.statusCodeWritten = true; } @Override From 7ed03d01c66f1f5f504bd0130b6eb473603697de Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Mon, 4 Jul 2016 15:45:32 +0200 Subject: [PATCH 461/498] Fix JacksonJsonEncoder for streams with more than 2 elements --- .../http/codec/json/JacksonJsonEncoder.java | 2 +- .../http/codec/json/JacksonJsonEncoderTests.java | 14 +++++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/codec/json/JacksonJsonEncoder.java b/spring-web-reactive/src/main/java/org/springframework/http/codec/json/JacksonJsonEncoder.java index 862edd712a..7499826d7c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/codec/json/JacksonJsonEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/codec/json/JacksonJsonEncoder.java @@ -83,7 +83,7 @@ public class JacksonJsonEncoder extends AbstractEncoder { Mono startArray = Mono.just(bufferFactory.wrap(START_ARRAY_BUFFER)); Flux arraySeparators = - Mono.just(bufferFactory.wrap(SEPARATOR_BUFFER)).repeat(); + Mono.fromSupplier(() -> bufferFactory.wrap(SEPARATOR_BUFFER)).repeat(); Mono endArray = Mono.just(bufferFactory.wrap(END_ARRAY_BUFFER)); diff --git a/spring-web-reactive/src/test/java/org/springframework/http/codec/json/JacksonJsonEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/http/codec/json/JacksonJsonEncoderTests.java index 6f33966118..ebcfe78b19 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/codec/json/JacksonJsonEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/codec/json/JacksonJsonEncoderTests.java @@ -52,7 +52,11 @@ public class JacksonJsonEncoderTests extends AbstractDataBufferAllocatingTestCas @Test public void encode() { - Flux source = Flux.just(new Pojo("foofoo", "barbar"), new Pojo("foofoofoo", "barbarbar")); + Flux source = Flux.just( + new Pojo("foo", "bar"), + new Pojo("foofoo", "barbar"), + new Pojo("foofoofoo", "barbarbar") + ); ResolvableType type = ResolvableType.forClass(Pojo.class); Flux output = @@ -62,11 +66,15 @@ public class JacksonJsonEncoderTests extends AbstractDataBufferAllocatingTestCas .subscribe(output) .assertComplete() .assertNoError() - .assertValuesWith(stringConsumer("["), + .assertValuesWith( + stringConsumer("["), + stringConsumer("{\"foo\":\"foo\",\"bar\":\"bar\"}"), + stringConsumer(","), stringConsumer("{\"foo\":\"foofoo\",\"bar\":\"barbar\"}"), stringConsumer(","), stringConsumer("{\"foo\":\"foofoofoo\",\"bar\":\"barbarbar\"}"), - stringConsumer("]")); + stringConsumer("]") + ); } @Test From 8690464a9b07b6e00c5d469ba0fde356b6d14e7f Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Mon, 4 Jul 2016 15:51:44 +0200 Subject: [PATCH 462/498] Remove @Ignore on streamResult() test This test was fixed by previous commit that allows to JSON encode correctly streams with more than 2 elements. --- .../method/annotation/RequestMappingIntegrationTests.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java index 58b132dfbe..b29495fd99 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java @@ -25,7 +25,6 @@ import java.util.concurrent.CompletableFuture; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; -import org.junit.Ignore; import org.junit.Test; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -150,7 +149,6 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati } @Test - @Ignore public void streamResult() throws Exception { URI url = new URI("http://localhost:" + port + "/stream-result"); RequestEntity request = RequestEntity.get(url).build(); From 7401d10f9142bc6242a8f0f2e48d0b63518fb045 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 5 Jul 2016 11:06:02 -0400 Subject: [PATCH 463/498] Polish AbstractServerHttpResponse --- .../reactive/AbstractServerHttpResponse.java | 66 ++++++++++--------- .../reactive/ReactorServerHttpResponse.java | 19 +++--- .../reactive/RxNettyServerHttpResponse.java | 18 ++--- .../reactive/ServletServerHttpResponse.java | 15 ++--- .../reactive/UndertowServerHttpResponse.java | 19 +++--- 5 files changed, 75 insertions(+), 62 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java index a1132ef6a9..2aafef5d7a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java @@ -45,7 +45,7 @@ import org.springframework.util.MultiValueMap; */ public abstract class AbstractServerHttpResponse implements ServerHttpResponse { - private static Log logger = LogFactory.getLog(AbstractServerHttpResponse.class); + private Log logger = LogFactory.getLog(getClass()); private static final int STATE_NEW = 1; @@ -53,17 +53,20 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse { private static final int STATE_COMMITTED = 3; + + private final DataBufferFactory dataBufferFactory; + + private HttpStatus statusCode; private final HttpHeaders headers; private final MultiValueMap cookies; - private final AtomicInteger state = new AtomicInteger(STATE_NEW); - private final List>> beforeCommitActions = new ArrayList<>(4); - private final DataBufferFactory dataBufferFactory; + private final AtomicInteger state = new AtomicInteger(STATE_NEW); + public AbstractServerHttpResponse(DataBufferFactory dataBufferFactory) { Assert.notNull(dataBufferFactory, "'dataBufferFactory' must not be null"); @@ -73,13 +76,29 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse { this.cookies = new LinkedMultiValueMap(); } + @Override public final DataBufferFactory bufferFactory() { return this.dataBufferFactory; } + + @Override + public boolean setStatusCode(HttpStatus statusCode) { + Assert.notNull(statusCode); + if (STATE_NEW == this.state.get()) { + this.statusCode = statusCode; + return true; + } + else if (logger.isDebugEnabled()) { + logger.debug("Can't set the status " + statusCode.toString() + + " because the HTTP response has already been committed"); + } + return false; + } + protected HttpStatus getStatusCode() { - return statusCode; + return this.statusCode; } @Override @@ -100,6 +119,12 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse { return this.cookies; } + @Override + public void beforeCommit(Supplier> action) { + Assert.notNull(action); + this.beforeCommitActions.add(action); + } + @Override public Mono writeWith(Publisher publisher) { return new ChannelSendOperator<>(publisher, writePublisher -> @@ -127,6 +152,12 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse { return mono; } + /** + * Implement this method to write to the underlying the response. + * @param body the publisher to write with + */ + protected abstract Mono writeWithInternal(Publisher body); + /** * Implement this method to write the status code to the underlying response. * This method is called once only. @@ -145,35 +176,10 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse { */ protected abstract void writeCookies(); - /** - * Implement this method to write to the underlying the response. - * @param body the publisher to write with - */ - protected abstract Mono writeWithInternal(Publisher body); - - @Override - public void beforeCommit(Supplier> action) { - Assert.notNull(action); - this.beforeCommitActions.add(action); - } @Override public Mono setComplete() { return applyBeforeCommit(); } - @Override - public boolean setStatusCode(HttpStatus statusCode) { - Assert.notNull(statusCode); - if (STATE_NEW == this.state.get()) { - this.statusCode = statusCode; - return true; - } - else if (logger.isDebugEnabled()) { - logger.debug("Can't set the status " + statusCode.toString() + - " because the HTTP response has already been committed"); - } - return false; - } - } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java index 5ca6ca3bc8..55d1a26265 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java @@ -48,6 +48,7 @@ public class ReactorServerHttpResponse extends AbstractServerHttpResponse private final HttpChannel channel; + public ReactorServerHttpResponse(HttpChannel response, DataBufferFactory dataBufferFactory) { super(dataBufferFactory); @@ -60,6 +61,15 @@ public class ReactorServerHttpResponse extends AbstractServerHttpResponse return this.channel; } + + @Override + protected void writeStatusCode() { + HttpStatus statusCode = this.getStatusCode(); + if (statusCode != null) { + getReactorChannel().status(HttpResponseStatus.valueOf(statusCode.value())); + } + } + @Override protected Mono writeWithInternal(Publisher publisher) { return Flux.from(publisher) @@ -70,14 +80,6 @@ public class ReactorServerHttpResponse extends AbstractServerHttpResponse .then(); } - @Override - protected void writeStatusCode() { - HttpStatus statusCode = this.getStatusCode(); - if (statusCode != null) { - getReactorChannel().status(HttpResponseStatus.valueOf(statusCode.value())); - } - } - @Override protected void writeHeaders() { for (String name : getHeaders().keySet()) { @@ -119,4 +121,5 @@ public class ReactorServerHttpResponse extends AbstractServerHttpResponse return this.channel.sendFile(file, position, count); }); } + } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java index 80aadad457..9d50f4d337 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java @@ -46,6 +46,7 @@ public class RxNettyServerHttpResponse extends AbstractServerHttpResponse { private final HttpServerResponse response; + public RxNettyServerHttpResponse(HttpServerResponse response, NettyDataBufferFactory dataBufferFactory) { super(dataBufferFactory); @@ -59,6 +60,15 @@ public class RxNettyServerHttpResponse extends AbstractServerHttpResponse { return this.response; } + + @Override + protected void writeStatusCode() { + HttpStatus statusCode = this.getStatusCode(); + if (statusCode != null) { + this.response.setStatus(HttpResponseStatus.valueOf(statusCode.value())); + } + } + @Override protected Mono writeWithInternal(Publisher body) { Observable content = RxJava1ObservableConverter.from(body).map(this::toByteBuf); @@ -70,14 +80,6 @@ public class RxNettyServerHttpResponse extends AbstractServerHttpResponse { return (buffer instanceof FlushingDataBuffer ? new FlushingByteBuf(byteBuf) : byteBuf); } - @Override - protected void writeStatusCode() { - HttpStatus statusCode = this.getStatusCode(); - if (statusCode != null) { - this.response.setStatus(HttpResponseStatus.valueOf(statusCode.value())); - } - } - @Override protected void writeHeaders() { for (String name : getHeaders().keySet()) { diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java index 60cca26f23..9b84f5082e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java @@ -23,8 +23,6 @@ import java.util.function.Function; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletResponse; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; @@ -42,12 +40,11 @@ import org.springframework.util.Assert; */ public class ServletServerHttpResponse extends AbstractServerHttpResponse { - private static final Log logger = LogFactory.getLog(ServletServerHttpResponse.class); - private final HttpServletResponse response; private final Function, Mono> responseBodyWriter; + public ServletServerHttpResponse(HttpServletResponse response, DataBufferFactory dataBufferFactory, Function, Mono> responseBodyWriter) { @@ -58,14 +55,11 @@ public class ServletServerHttpResponse extends AbstractServerHttpResponse { this.responseBodyWriter = responseBodyWriter; } + public HttpServletResponse getServletResponse() { return this.response; } - @Override - protected Mono writeWithInternal(Publisher publisher) { - return this.responseBodyWriter.apply(publisher); - } @Override protected void writeStatusCode() { @@ -75,6 +69,11 @@ public class ServletServerHttpResponse extends AbstractServerHttpResponse { } } + @Override + protected Mono writeWithInternal(Publisher publisher) { + return this.responseBodyWriter.apply(publisher); + } + @Override protected void writeHeaders() { for (Map.Entry> entry : getHeaders().entrySet()) { diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java index 73fba1d28a..3dc11cf395 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java @@ -54,6 +54,7 @@ public class UndertowServerHttpResponse extends AbstractServerHttpResponse private final HttpServerExchange exchange; + public UndertowServerHttpResponse(HttpServerExchange exchange, DataBufferFactory dataBufferFactory) { super(dataBufferFactory); @@ -66,6 +67,16 @@ public class UndertowServerHttpResponse extends AbstractServerHttpResponse return this.exchange; } + + @Override + protected void writeStatusCode() { + HttpStatus statusCode = this.getStatusCode(); + if (statusCode != null) { + getUndertowExchange().setStatusCode(statusCode.value()); + } + } + + @Override protected Mono writeWithInternal(Publisher publisher) { return Mono.from(s -> { @@ -101,14 +112,6 @@ public class UndertowServerHttpResponse extends AbstractServerHttpResponse } } - @Override - protected void writeStatusCode() { - HttpStatus statusCode = this.getStatusCode(); - if (statusCode != null) { - getUndertowExchange().setStatusCode(statusCode.value()); - } - } - @Override protected void writeHeaders() { for (Map.Entry> entry : getHeaders().entrySet()) { From 2068f667f38cd206e3e9116d0f82d3ffe4faca3b Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 5 Jul 2016 11:10:36 -0400 Subject: [PATCH 464/498] Expose getStatusCode from ServerHttpResponse --- .../http/server/reactive/AbstractServerHttpResponse.java | 3 ++- .../http/server/reactive/ServerHttpResponse.java | 7 ++++++- .../http/server/reactive/MockServerHttpResponse.java | 2 +- .../web/reactive/DispatcherHandlerErrorTests.java | 4 ++-- .../web/reactive/ResponseStatusExceptionHandlerTests.java | 2 +- .../annotation/ResponseEntityResultHandlerTests.java | 6 +++--- .../server/handler/ExceptionHandlingHttpHandlerTests.java | 8 ++++---- .../web/server/handler/FilteringWebHandlerTests.java | 2 +- 8 files changed, 20 insertions(+), 14 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java index 2aafef5d7a..a19b875c43 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java @@ -97,7 +97,8 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse { return false; } - protected HttpStatus getStatusCode() { + @Override + public HttpStatus getStatusCode() { return this.statusCode; } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java index c458cfcc7e..777b36defa 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java @@ -40,8 +40,13 @@ public interface ServerHttpResponse extends ReactiveHttpOutputMessage { boolean setStatusCode(HttpStatus status); /** - * Return a mutable map with the cookies to send to the server. + * Return the HTTP status code or {@code null} if not set. */ + HttpStatus getStatusCode(); + + /** + * Return a mutable map with the cookies to send to the server. + */ MultiValueMap getCookies(); /** diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java index c12ba69f94..3ffafd090c 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/MockServerHttpResponse.java @@ -54,7 +54,7 @@ public class MockServerHttpResponse implements ServerHttpResponse { return true; } - public HttpStatus getStatus() { + public HttpStatus getStatusCode() { return this.status; } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java index e6ba0b2163..3657fd2fc4 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java @@ -192,7 +192,7 @@ public class DispatcherHandlerErrorTests { Mono publisher = webHandler.handle(this.exchange); publisher.block(); - assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, this.response.getStatus()); + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, this.response.getStatusCode()); } @Test @@ -204,7 +204,7 @@ public class DispatcherHandlerErrorTests { Mono publisher = webHandler.handle(this.exchange); publisher.block(); - assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, this.response.getStatus()); + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, this.response.getStatusCode()); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java index d916a81c96..c44553b96f 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java @@ -67,7 +67,7 @@ public class ResponseStatusExceptionHandlerTests { Mono publisher = this.handler.handle(this.exchange, ex); publisher.block(); - assertEquals(HttpStatus.BAD_REQUEST, this.response.getStatus()); + assertEquals(HttpStatus.BAD_REQUEST, this.response.getStatusCode()); } @Test diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java index b54994f170..546c1bd0b4 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java @@ -145,7 +145,7 @@ public class ResponseEntityResultHandlerTests { HandlerResult result = handlerResult(value, type); this.resultHandler.handleResult(exchange, result).block(Duration.ofSeconds(5)); - assertEquals(HttpStatus.NO_CONTENT, this.response.getStatus()); + assertEquals(HttpStatus.NO_CONTENT, this.response.getStatusCode()); assertEquals(0, this.response.getHeaders().size()); assertNull(this.response.getBody()); } @@ -158,7 +158,7 @@ public class ResponseEntityResultHandlerTests { HandlerResult result = handlerResult(value, type); this.resultHandler.handleResult(this.exchange, result).block(Duration.ofSeconds(5)); - assertEquals(HttpStatus.CREATED, this.response.getStatus()); + assertEquals(HttpStatus.CREATED, this.response.getStatusCode()); assertEquals(1, this.response.getHeaders().size()); assertEquals(location, this.response.getHeaders().getLocation()); assertNull(this.response.getBody()); @@ -188,7 +188,7 @@ public class ResponseEntityResultHandlerTests { HandlerResult result = handlerResult(returnValue, type); this.resultHandler.handleResult(this.exchange, result).block(Duration.ofSeconds(5)); - assertEquals(HttpStatus.OK, this.response.getStatus()); + assertEquals(HttpStatus.OK, this.response.getStatusCode()); assertEquals("text/plain;charset=UTF-8", this.response.getHeaders().getFirst("Content-Type")); assertResponseBody("abc"); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/server/handler/ExceptionHandlingHttpHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/server/handler/ExceptionHandlingHttpHandlerTests.java index 6152a48e2f..c3b9aef394 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/server/handler/ExceptionHandlingHttpHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/server/handler/ExceptionHandlingHttpHandlerTests.java @@ -64,7 +64,7 @@ public class ExceptionHandlingHttpHandlerTests { WebExceptionHandler exceptionHandler = new BadRequestExceptionHandler(); createWebHandler(exceptionHandler).handle(this.exchange).block(); - assertEquals(HttpStatus.BAD_REQUEST, this.response.getStatus()); + assertEquals(HttpStatus.BAD_REQUEST, this.response.getStatusCode()); } @Test @@ -77,7 +77,7 @@ public class ExceptionHandlingHttpHandlerTests { }; createWebHandler(exceptionHandlers).handle(this.exchange).block(); - assertEquals(HttpStatus.BAD_REQUEST, this.response.getStatus()); + assertEquals(HttpStatus.BAD_REQUEST, this.response.getStatusCode()); } @Test @@ -85,7 +85,7 @@ public class ExceptionHandlingHttpHandlerTests { WebExceptionHandler exceptionHandler = new UnresolvedExceptionHandler(); createWebHandler(exceptionHandler).handle(this.exchange).block(); - assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, this.response.getStatus()); + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, this.response.getStatusCode()); } @Test @@ -93,7 +93,7 @@ public class ExceptionHandlingHttpHandlerTests { WebExceptionHandler exceptionHandler = new BadRequestExceptionHandler(); createWebHandler(exceptionHandler).handle(this.exchange).block(); - assertEquals(HttpStatus.BAD_REQUEST, this.response.getStatus()); + assertEquals(HttpStatus.BAD_REQUEST, this.response.getStatusCode()); } private WebHandler createWebHandler(WebExceptionHandler... handlers) { diff --git a/spring-web-reactive/src/test/java/org/springframework/web/server/handler/FilteringWebHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/server/handler/FilteringWebHandlerTests.java index 206c03e8b2..4f6d54b9f0 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/server/handler/FilteringWebHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/server/handler/FilteringWebHandlerTests.java @@ -117,7 +117,7 @@ public class FilteringWebHandlerTests { .filters(new ExceptionFilter()).exceptionHandlers(exceptionHandler).build(); handler.handle(this.request, this.response).block(); - assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, this.response.getStatus()); + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, this.response.getStatusCode()); Throwable savedException = exceptionHandler.ex; assertNotNull(savedException); From 7af7a55ede0cba513836032ff7f12be46b316633 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 5 Jul 2016 12:02:47 -0400 Subject: [PATCH 465/498] Polish and simplify JacksonJsonEncoder --- .../http/codec/json/JacksonJsonEncoder.java | 50 ++++++++----------- .../core/codec/ByteBufferEncoderTests.java | 2 +- .../core/codec/ResourceEncoderTests.java | 2 +- .../core/codec/StringEncoderTests.java | 2 +- .../AbstractDataBufferAllocatingTestCase.java | 6 +-- .../support/DataBufferTestUtilsTests.java | 4 +- .../buffer/support/DataBufferUtilsTests.java | 6 +-- .../http/codec/SseEventEncoderTests.java | 8 +-- .../codec/json/JacksonJsonEncoderTests.java | 17 +++---- .../http/codec/xml/Jaxb2EncoderTests.java | 2 +- 10 files changed, 44 insertions(+), 55 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/codec/json/JacksonJsonEncoder.java b/spring-web-reactive/src/main/java/org/springframework/http/codec/json/JacksonJsonEncoder.java index 7499826d7c..0d7e177d09 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/codec/json/JacksonJsonEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/codec/json/JacksonJsonEncoder.java @@ -46,14 +46,16 @@ import org.springframework.util.MimeType; */ public class JacksonJsonEncoder extends AbstractEncoder { - private final ObjectMapper mapper; - private static final ByteBuffer START_ARRAY_BUFFER = ByteBuffer.wrap(new byte[]{'['}); private static final ByteBuffer SEPARATOR_BUFFER = ByteBuffer.wrap(new byte[]{','}); private static final ByteBuffer END_ARRAY_BUFFER = ByteBuffer.wrap(new byte[]{']'}); + + private final ObjectMapper mapper; + + public JacksonJsonEncoder() { this(new ObjectMapper()); } @@ -66,54 +68,44 @@ public class JacksonJsonEncoder extends AbstractEncoder { this.mapper = mapper; } + @Override - public Flux encode(Publisher inputStream, - DataBufferFactory bufferFactory, ResolvableType elementType, MimeType mimeType, - Object... hints) { + public Flux encode(Publisher inputStream, DataBufferFactory bufferFactory, + ResolvableType elementType, MimeType mimeType, 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) { - // single object - return Flux.from(inputStream) - .map(value -> serialize(value, bufferFactory, elementType)); + return Flux.from(inputStream).map(value -> encodeValue(value, bufferFactory, elementType)); } - else { - // array - Mono startArray = - Mono.just(bufferFactory.wrap(START_ARRAY_BUFFER)); - Flux arraySeparators = - Mono.fromSupplier(() -> bufferFactory.wrap(SEPARATOR_BUFFER)).repeat(); - Mono endArray = - Mono.just(bufferFactory.wrap(END_ARRAY_BUFFER)); - Flux serializedObjects = Flux.from(inputStream) - .map(value -> serialize(value, bufferFactory, elementType)); + Flux array = Flux.from(inputStream) + .flatMap(value -> + Flux.just(encodeValue(value, bufferFactory, elementType), + bufferFactory.wrap(SEPARATOR_BUFFER))); - Flux array = Flux.zip(serializedObjects, arraySeparators) - .flatMap(tuple -> Flux.just(tuple.getT1(), tuple.getT2())); - - Flux arrayWithoutLastSeparator = array.skipLast(1); - - return Flux.concat(startArray, arrayWithoutLastSeparator, endArray); - } + return Flux.concat(Mono.just(bufferFactory.wrap(START_ARRAY_BUFFER)), array.skipLast(1), + Mono.just(bufferFactory.wrap(END_ARRAY_BUFFER))); } - private DataBuffer serialize(Object value, DataBufferFactory dataBufferFactory, - ResolvableType type) { + private DataBuffer encodeValue(Object value, DataBufferFactory bufferFactory, ResolvableType type) { TypeFactory typeFactory = this.mapper.getTypeFactory(); JavaType javaType = typeFactory.constructType(type.getType()); ObjectWriter writer = this.mapper.writerFor(javaType); - DataBuffer buffer = dataBufferFactory.allocateBuffer(); + + DataBuffer buffer = bufferFactory.allocateBuffer(); OutputStream outputStream = buffer.asOutputStream(); + try { writer.writeValue(outputStream, value); } catch (IOException e) { throw new CodecException("Error while writing the data", e); } + return buffer; } - } diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/ByteBufferEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/ByteBufferEncoderTests.java index dc44221899..157724d3b1 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/ByteBufferEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/ByteBufferEncoderTests.java @@ -61,7 +61,7 @@ public class ByteBufferEncoderTests extends AbstractDataBufferAllocatingTestCase Flux source = Flux.just(ByteBuffer.wrap(fooBytes), ByteBuffer.wrap(barBytes)); - Flux output = this.encoder.encode(source, this.dataBufferFactory, + Flux output = this.encoder.encode(source, this.bufferFactory, ResolvableType.forClassWithGenerics(Publisher.class, ByteBuffer.class), null); TestSubscriber diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/ResourceEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/ResourceEncoderTests.java index 75bb8a07c3..b27f987a2a 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/ResourceEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/ResourceEncoderTests.java @@ -62,7 +62,7 @@ public class ResourceEncoderTests extends AbstractDataBufferAllocatingTestCase { Mono source = Mono.just(resource); - Flux output = this.encoder.encode(source, this.dataBufferFactory, + Flux output = this.encoder.encode(source, this.bufferFactory, ResolvableType.forClass(Resource.class), null); diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/StringEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/StringEncoderTests.java index 6bcf51c6ca..2a4e60f18c 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/StringEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/StringEncoderTests.java @@ -59,7 +59,7 @@ public class StringEncoderTests extends AbstractDataBufferAllocatingTestCase { @Test public void write() throws InterruptedException { Flux output = Flux.from( - this.encoder.encode(Flux.just("foo"), this.dataBufferFactory, null, null)) + this.encoder.encode(Flux.just("foo"), this.bufferFactory, null, null)) .map(chunk -> { byte[] b = new byte[chunk.readableByteCount()]; chunk.read(b); diff --git a/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/AbstractDataBufferAllocatingTestCase.java b/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/AbstractDataBufferAllocatingTestCase.java index 8afe4d5abf..1ec7c9990a 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/AbstractDataBufferAllocatingTestCase.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/AbstractDataBufferAllocatingTestCase.java @@ -37,7 +37,7 @@ import static org.junit.Assert.assertEquals; public abstract class AbstractDataBufferAllocatingTestCase { @Parameterized.Parameter - public DataBufferFactory dataBufferFactory; + public DataBufferFactory bufferFactory; @Parameterized.Parameters(name = "{0}") public static Object[][] dataBufferFactories() { @@ -53,12 +53,12 @@ public abstract class AbstractDataBufferAllocatingTestCase { } protected DataBuffer createDataBuffer(int capacity) { - return this.dataBufferFactory.allocateBuffer(capacity); + return this.bufferFactory.allocateBuffer(capacity); } protected DataBuffer stringBuffer(String value) { byte[] bytes = value.getBytes(StandardCharsets.UTF_8); - DataBuffer buffer = this.dataBufferFactory.allocateBuffer(bytes.length); + DataBuffer buffer = this.bufferFactory.allocateBuffer(bytes.length); buffer.write(bytes); return buffer; } diff --git a/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/support/DataBufferTestUtilsTests.java b/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/support/DataBufferTestUtilsTests.java index 6c5012e5a5..1763f4bbce 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/support/DataBufferTestUtilsTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/support/DataBufferTestUtilsTests.java @@ -33,7 +33,7 @@ public class DataBufferTestUtilsTests extends AbstractDataBufferAllocatingTestCa @Test public void dumpBytes() { - DataBuffer buffer = this.dataBufferFactory.allocateBuffer(4); + DataBuffer buffer = this.bufferFactory.allocateBuffer(4); byte[] source = {'a', 'b', 'c', 'd'}; buffer.write(source); @@ -46,7 +46,7 @@ public class DataBufferTestUtilsTests extends AbstractDataBufferAllocatingTestCa @Test public void dumpString() { - DataBuffer buffer = this.dataBufferFactory.allocateBuffer(4); + DataBuffer buffer = this.bufferFactory.allocateBuffer(4); String source = "abcd"; buffer.write(source.getBytes(StandardCharsets.UTF_8)); diff --git a/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/support/DataBufferUtilsTests.java b/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/support/DataBufferUtilsTests.java index 63029c5882..51dc81833f 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/support/DataBufferUtilsTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/io/buffer/support/DataBufferUtilsTests.java @@ -42,7 +42,7 @@ public class DataBufferUtilsTests extends AbstractDataBufferAllocatingTestCase { .toURI(); FileChannel channel = FileChannel.open(Paths.get(uri), StandardOpenOption.READ); - Flux flux = DataBufferUtils.read(channel, this.dataBufferFactory, 4); + Flux flux = DataBufferUtils.read(channel, this.bufferFactory, 4); TestSubscriber .subscribe(flux) @@ -61,7 +61,7 @@ public class DataBufferUtilsTests extends AbstractDataBufferAllocatingTestCase { .toURI(); FileChannel channel = FileChannel.open(Paths.get(uri), StandardOpenOption.READ); - Flux flux = DataBufferUtils.read(channel, this.dataBufferFactory, 3); + Flux flux = DataBufferUtils.read(channel, this.bufferFactory, 3); TestSubscriber .subscribe(flux) @@ -80,7 +80,7 @@ public class DataBufferUtilsTests extends AbstractDataBufferAllocatingTestCase { InputStream is = DataBufferUtilsTests.class .getResourceAsStream("DataBufferUtilsTests.txt"); - Flux flux = DataBufferUtils.read(is, this.dataBufferFactory, 4); + Flux flux = DataBufferUtils.read(is, this.bufferFactory, 4); TestSubscriber .subscribe(flux) diff --git a/spring-web-reactive/src/test/java/org/springframework/http/codec/SseEventEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/http/codec/SseEventEncoderTests.java index 352f8d4799..bb5b0996a7 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/codec/SseEventEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/codec/SseEventEncoderTests.java @@ -64,7 +64,7 @@ public class SseEventEncoderTests extends AbstractDataBufferAllocatingTestCase { event.setComment("bla\nbla bla\nbla bla bla"); event.setReconnectTime(123L); Mono source = Mono.just(event); - Flux output = encoder.encode(source, this.dataBufferFactory, + Flux output = encoder.encode(source, this.bufferFactory, ResolvableType.forClass(SseEvent.class), new MimeType("text", "event-stream")); TestSubscriber .subscribe(output) @@ -84,7 +84,7 @@ public class SseEventEncoderTests extends AbstractDataBufferAllocatingTestCase { public void encodeString() { SseEventEncoder encoder = new SseEventEncoder(Arrays.asList(new JacksonJsonEncoder())); Flux source = Flux.just("foo", "bar"); - Flux output = encoder.encode(source, this.dataBufferFactory, + Flux output = encoder.encode(source, this.bufferFactory, ResolvableType.forClass(String.class), new MimeType("text", "event-stream")); TestSubscriber .subscribe(output) @@ -103,7 +103,7 @@ public class SseEventEncoderTests extends AbstractDataBufferAllocatingTestCase { public void encodeMultilineString() { SseEventEncoder encoder = new SseEventEncoder(Arrays.asList(new JacksonJsonEncoder())); Flux source = Flux.just("foo\nbar", "foo\nbaz"); - Flux output = encoder.encode(source, this.dataBufferFactory, + Flux output = encoder.encode(source, this.bufferFactory, ResolvableType.forClass(String.class), new MimeType("text", "event-stream")); TestSubscriber .subscribe(output) @@ -122,7 +122,7 @@ public class SseEventEncoderTests extends AbstractDataBufferAllocatingTestCase { public void encodePojo() { SseEventEncoder encoder = new SseEventEncoder(Arrays.asList(new JacksonJsonEncoder())); Flux source = Flux.just(new Pojo("foofoo", "barbar"), new Pojo("foofoofoo", "barbarbar")); - Flux output = encoder.encode(source, this.dataBufferFactory, + Flux output = encoder.encode(source, this.bufferFactory, ResolvableType.forClass(Pojo.class), new MimeType("text", "event-stream")); TestSubscriber .subscribe(output) diff --git a/spring-web-reactive/src/test/java/org/springframework/http/codec/json/JacksonJsonEncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/http/codec/json/JacksonJsonEncoderTests.java index ebcfe78b19..b7249a0331 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/codec/json/JacksonJsonEncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/codec/json/JacksonJsonEncoderTests.java @@ -39,11 +39,13 @@ public class JacksonJsonEncoderTests extends AbstractDataBufferAllocatingTestCas private JacksonJsonEncoder encoder; + @Before public void createEncoder() { this.encoder = new JacksonJsonEncoder(); } + @Test public void canEncode() { assertTrue(this.encoder.canEncode(null, MediaType.APPLICATION_JSON)); @@ -57,13 +59,10 @@ public class JacksonJsonEncoderTests extends AbstractDataBufferAllocatingTestCas new Pojo("foofoo", "barbar"), new Pojo("foofoofoo", "barbarbar") ); - ResolvableType type = ResolvableType.forClass(Pojo.class); - Flux output = - this.encoder.encode(source, this.dataBufferFactory, type, null); + Flux output = this.encoder.encode(source, this.bufferFactory, type, null); - TestSubscriber - .subscribe(output) + TestSubscriber.subscribe(output) .assertComplete() .assertNoError() .assertValuesWith( @@ -80,13 +79,10 @@ public class JacksonJsonEncoderTests extends AbstractDataBufferAllocatingTestCas @Test public void encodeWithType() { Flux source = Flux.just(new Foo(), new Bar()); - ResolvableType type = ResolvableType.forClass(ParentClass.class); - Flux output = - this.encoder.encode(source, this.dataBufferFactory, type, null); + Flux output = this.encoder.encode(source, this.bufferFactory, type, null); - TestSubscriber - .subscribe(output) + TestSubscriber.subscribe(output) .assertComplete() .assertNoError() .assertValuesWith(stringConsumer("["), @@ -96,6 +92,7 @@ public class JacksonJsonEncoderTests extends AbstractDataBufferAllocatingTestCas stringConsumer("]")); } + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type") private static class ParentClass { } diff --git a/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/Jaxb2EncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/Jaxb2EncoderTests.java index 4476cfcaf7..4c51b88268 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/Jaxb2EncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/codec/xml/Jaxb2EncoderTests.java @@ -71,7 +71,7 @@ public class Jaxb2EncoderTests extends AbstractDataBufferAllocatingTestCase { @Test public void encode() { Flux source = Flux.just(new Pojo("foofoo", "barbar"), new Pojo("foofoofoo", "barbarbar")); - Flux output = this.encoder.encode(source, this.dataBufferFactory, + Flux output = this.encoder.encode(source, this.bufferFactory, ResolvableType.forClass(Pojo.class), MediaType.APPLICATION_XML); TestSubscriber From 33b6dca2a6d56398626ecc663ed8f54b9415d66b Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 5 Jul 2016 18:14:47 -0400 Subject: [PATCH 466/498] Polish tests --- .../reactive/DispatcherHandlerErrorTests.java | 119 ++++++------------ .../ResponseStatusExceptionHandlerTests.java | 26 ++-- ...pleUrlHandlerMappingIntegrationTests.java} | 114 ++++++----------- 3 files changed, 85 insertions(+), 174 deletions(-) rename spring-web-reactive/src/test/java/org/springframework/web/reactive/result/{WebHandlerIntegrationTests.java => SimpleUrlHandlerMappingIntegrationTests.java} (57%) diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java index 3657fd2fc4..cbd70fab9c 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java @@ -23,14 +23,12 @@ import org.junit.Before; import org.junit.Test; import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; -import reactor.core.publisher.Signal; -import reactor.core.util.SignalKind; +import reactor.core.test.TestSubscriber; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.codec.StringEncoder; -import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DefaultDataBufferFactory; @@ -38,7 +36,6 @@ import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.converter.reactive.CodecHttpMessageConverter; -import org.springframework.http.converter.reactive.HttpMessageConverter; import org.springframework.http.server.reactive.MockServerHttpRequest; import org.springframework.http.server.reactive.MockServerHttpResponse; import org.springframework.stereotype.Controller; @@ -52,14 +49,10 @@ import org.springframework.web.server.NotAcceptableStatusException; import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebExceptionHandler; -import org.springframework.web.server.WebFilter; -import org.springframework.web.server.WebFilterChain; import org.springframework.web.server.WebHandler; import org.springframework.web.server.adapter.DefaultServerWebExchange; import org.springframework.web.server.handler.ExceptionHandlingWebHandler; -import org.springframework.web.server.handler.FilteringWebHandler; import org.springframework.web.server.session.MockWebSessionManager; -import org.springframework.web.server.session.WebSessionManager; import static org.hamcrest.CoreMatchers.startsWith; import static org.junit.Assert.assertEquals; @@ -75,15 +68,13 @@ import static org.junit.Assert.assertThat; @SuppressWarnings({"ThrowableResultOfMethodCallIgnored", "ThrowableInstanceNeverThrown"}) public class DispatcherHandlerErrorTests { - public static final IllegalStateException EXCEPTION = new IllegalStateException("boo"); + private static final IllegalStateException EXCEPTION = new IllegalStateException("boo"); private DispatcherHandler dispatcherHandler; private MockServerHttpRequest request; - private MockServerHttpResponse response; - private ServerWebExchange exchange; @@ -96,127 +87,101 @@ public class DispatcherHandlerErrorTests { this.dispatcherHandler = new DispatcherHandler(); this.dispatcherHandler.setApplicationContext(appContext); - WebSessionManager sessionManager = new MockWebSessionManager(); - this.request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); - this.response = new MockServerHttpResponse(); - this.exchange = new DefaultServerWebExchange(this.request, this.response, sessionManager); + MockServerHttpResponse response = new MockServerHttpResponse(); + MockWebSessionManager sessionManager = new MockWebSessionManager(); + this.exchange = new DefaultServerWebExchange(this.request, response, sessionManager); } @Test public void noHandler() throws Exception { this.request.setUri(new URI("/does-not-exist")); - Mono publisher = this.dispatcherHandler.handle(this.exchange); - Throwable ex = awaitErrorSignal(publisher); - assertEquals(ResponseStatusException.class, ex.getClass()); - assertEquals(HttpStatus.NOT_FOUND, ((ResponseStatusException) ex).getStatus()); + TestSubscriber.subscribe(publisher) + .assertError(ResponseStatusException.class) + .assertErrorMessage("Request failure [status: 404, reason: \"No matching handler\"]"); } @Test - public void noResolverForArgument() throws Exception { + public void unknownMethodArgumentType() throws Exception { this.request.setUri(new URI("/unknown-argument-type")); - Mono publisher = this.dispatcherHandler.handle(this.exchange); - Throwable ex = awaitErrorSignal(publisher); - assertEquals(IllegalStateException.class, ex.getClass()); - assertThat(ex.getMessage(), startsWith("No resolver for argument [0]")); + TestSubscriber.subscribe(publisher) + .assertError(IllegalStateException.class) + .assertErrorWith(ex -> assertThat(ex.getMessage(), startsWith("No resolver for argument [0]"))); } @Test - public void controllerMethodError() throws Exception { + public void controllerReturnsMonoError() throws Exception { this.request.setUri(new URI("/error-signal")); - Mono publisher = this.dispatcherHandler.handle(this.exchange); - Throwable ex = awaitErrorSignal(publisher); - assertSame(EXCEPTION, ex); + TestSubscriber.subscribe(publisher) + .assertErrorWith(ex -> assertSame(EXCEPTION, ex)); } @Test - public void controllerMethodWithThrownException() throws Exception { + public void controllerThrowsException() throws Exception { this.request.setUri(new URI("/raise-exception")); - Mono publisher = this.dispatcherHandler.handle(this.exchange); - Throwable ex = awaitErrorSignal(publisher); - assertSame(EXCEPTION, ex); + TestSubscriber.subscribe(publisher) + .assertErrorWith(ex -> assertSame(EXCEPTION, ex)); } @Test - public void noHandlerResultHandler() throws Exception { + public void unknownReturnType() throws Exception { this.request.setUri(new URI("/unknown-return-type")); - Mono publisher = this.dispatcherHandler.handle(this.exchange); - Throwable ex = awaitErrorSignal(publisher); - assertEquals(IllegalStateException.class, ex.getClass()); - assertThat(ex.getMessage(), startsWith("No HandlerResultHandler")); + TestSubscriber.subscribe(publisher) + .assertError(IllegalStateException.class) + .assertErrorWith(ex -> assertThat(ex.getMessage(), startsWith("No HandlerResultHandler"))); } @Test - public void notAcceptable() throws Exception { + public void responseBodyMessageConversionError() throws Exception { + DataBuffer dataBuffer = new DefaultDataBufferFactory().allocateBuffer(); this.request.setUri(new URI("/request-body")); - this.request.getHeaders().setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); - DataBuffer buffer = new DefaultDataBufferFactory().allocateBuffer() - .write("body".getBytes("UTF-8")); - this.request.writeWith(Mono.just(buffer)); + this.request.getHeaders().add("Accept", MediaType.APPLICATION_JSON_VALUE); + this.request.writeWith(Mono.just(dataBuffer.write("body".getBytes("UTF-8")))); Mono publisher = this.dispatcherHandler.handle(this.exchange); - Throwable ex = awaitErrorSignal(publisher); - assertEquals(NotAcceptableStatusException.class, ex.getClass()); + TestSubscriber.subscribe(publisher) + .assertError(NotAcceptableStatusException.class); } @Test public void requestBodyError() throws Exception { this.request.setUri(new URI("/request-body")); this.request.writeWith(Mono.error(EXCEPTION)); - Mono publisher = this.dispatcherHandler.handle(this.exchange); - Throwable ex = awaitErrorSignal(publisher); - ex.printStackTrace(); - assertSame(EXCEPTION, ex); + TestSubscriber.subscribe(publisher) + .assertErrorWith(ex -> assertSame(EXCEPTION, ex)); } @Test - public void dispatcherHandlerWithHttpExceptionHandler() throws Exception { + public void webExceptionHandler() throws Exception { this.request.setUri(new URI("/unknown-argument-type")); WebExceptionHandler exceptionHandler = new ServerError500ExceptionHandler(); WebHandler webHandler = new ExceptionHandlingWebHandler(this.dispatcherHandler, exceptionHandler); Mono publisher = webHandler.handle(this.exchange); - publisher.block(); - assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, this.response.getStatusCode()); - } + TestSubscriber.subscribe(publisher) + .assertErrorWith(ex -> assertEquals( + HttpStatus.INTERNAL_SERVER_ERROR, this.exchange.getResponse().getStatusCode())); - @Test - public void filterChainWithHttpExceptionHandler() throws Exception { - this.request.setUri(new URI("/unknown-argument-type")); - - WebHandler webHandler = new FilteringWebHandler(this.dispatcherHandler, new TestWebFilter()); - webHandler = new ExceptionHandlingWebHandler(webHandler, new ServerError500ExceptionHandler()); - Mono publisher = webHandler.handle(this.exchange); - - publisher.block(); - assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, this.response.getStatusCode()); - } - - - private Throwable awaitErrorSignal(Mono mono) throws Exception { - Signal signal = mono.materialize().block(); - assertEquals("Unexpected signal: " + signal, SignalKind.onError, signal.getType()); - return signal.getThrowable(); } @Configuration - @SuppressWarnings("unused") + @SuppressWarnings({"unused", "WeakerAccess"}) static class TestConfig { @Bean @@ -231,9 +196,9 @@ public class DispatcherHandlerErrorTests { @Bean public ResponseBodyResultHandler resultHandler() { - HttpMessageConverter converter = new CodecHttpMessageConverter<>(new StringEncoder()); - ConversionService conversionService = new DefaultConversionService(); - return new ResponseBodyResultHandler(Collections.singletonList(converter), conversionService); + return new ResponseBodyResultHandler( + Collections.singletonList(new CodecHttpMessageConverter<>(new StringEncoder())), + new DefaultConversionService()); } @Bean @@ -285,12 +250,4 @@ public class DispatcherHandlerErrorTests { } } - private static class TestWebFilter implements WebFilter { - - @Override - public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { - return chain.filter(exchange); - } - } - } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java index c44553b96f..704a713008 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/ResponseStatusExceptionHandlerTests.java @@ -16,11 +16,12 @@ package org.springframework.web.reactive; import java.net.URI; +import java.time.Duration; import org.junit.Before; import org.junit.Test; import reactor.core.publisher.Mono; -import reactor.core.publisher.Signal; +import reactor.core.test.TestSubscriber; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; @@ -30,12 +31,9 @@ import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange; import org.springframework.web.server.session.MockWebSessionManager; -import org.springframework.web.server.session.WebSessionManager; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; /** * Unit tests for {@link ResponseStatusExceptionHandler}. @@ -54,31 +52,29 @@ public class ResponseStatusExceptionHandlerTests { @Before public void setUp() throws Exception { this.handler = new ResponseStatusExceptionHandler(); - MockServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/path")); - WebSessionManager sessionManager = new MockWebSessionManager(); this.response = new MockServerHttpResponse(); - this.exchange = new DefaultServerWebExchange(request, this.response, sessionManager); + this.exchange = new DefaultServerWebExchange( + new MockServerHttpRequest(HttpMethod.GET, new URI("/path")), + this.response, + new MockWebSessionManager()); } @Test public void handleException() throws Exception { Throwable ex = new ResponseStatusException(HttpStatus.BAD_REQUEST, ""); - Mono publisher = this.handler.handle(this.exchange, ex); + this.handler.handle(this.exchange, ex).block(Duration.ofSeconds(5)); - publisher.block(); assertEquals(HttpStatus.BAD_REQUEST, this.response.getStatusCode()); } @Test public void unresolvedException() throws Exception { - Throwable ex = new IllegalStateException(); - Mono publisher = this.handler.handle(this.exchange, ex); + Throwable expected = new IllegalStateException(); + Mono mono = this.handler.handle(this.exchange, expected); - Signal signal = publisher.materialize().block(); - assertNotNull(signal); - assertTrue(signal.hasError()); - assertSame(ex, signal.getThrowable()); + TestSubscriber.subscribe(mono) + .assertErrorWith(actual -> assertSame(expected, actual)); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/WebHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/SimpleUrlHandlerMappingIntegrationTests.java similarity index 57% rename from spring-web-reactive/src/test/java/org/springframework/web/reactive/result/WebHandlerIntegrationTests.java rename to spring-web-reactive/src/test/java/org/springframework/web/reactive/result/SimpleUrlHandlerMappingIntegrationTests.java index 73c45012ae..5b1c273038 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/WebHandlerIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/SimpleUrlHandlerMappingIntegrationTests.java @@ -17,7 +17,6 @@ package org.springframework.web.reactive.result; import java.net.URI; -import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; @@ -31,6 +30,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DefaultDataBuffer; import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.HttpStatus; import org.springframework.http.RequestEntity; @@ -42,27 +42,22 @@ import org.springframework.web.client.RestTemplate; import org.springframework.web.reactive.DispatcherHandler; import org.springframework.web.reactive.ResponseStatusExceptionHandler; import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping; -import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebHandler; import org.springframework.web.server.adapter.WebHttpHandlerBuilder; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; - /** - * Integration tests with requests mapped to plain {@link WebHandler}s. + * Integration tests with requests mapped via + * {@link SimpleUrlHandlerMapping} to plain {@link WebHandler}s. * * @author Rossen Stoyanchev */ -public class WebHandlerIntegrationTests extends AbstractHttpHandlerIntegrationTests { - - private static final Charset UTF_8 = Charset.forName("UTF-8"); - +public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandlerIntegrationTests { @Override protected HttpHandler createHttpHandler() { - AnnotationConfigApplicationContext wac = new AnnotationConfigApplicationContext(); wac.register(WebConfig.class); wac.refresh(); @@ -76,109 +71,73 @@ public class WebHandlerIntegrationTests extends AbstractHttpHandlerIntegrationTe } @Test - public void testFooHandler() throws Exception { - - RestTemplate restTemplate = new RestTemplate(); - - URI url = new URI("http://localhost:" + port + "/foo"); + public void testRequestToFooHandler() throws Exception { + URI url = new URI("http://localhost:" + this.port + "/foo"); RequestEntity request = RequestEntity.get(url).build(); - ResponseEntity response = restTemplate.exchange(request, byte[].class); + ResponseEntity response = new RestTemplate().exchange(request, byte[].class); assertEquals(HttpStatus.OK, response.getStatusCode()); - assertArrayEquals("foo".getBytes(UTF_8), response.getBody()); + assertArrayEquals("foo".getBytes("UTF-8"), response.getBody()); } @Test - public void testBarHandler() throws Exception { - - RestTemplate restTemplate = new RestTemplate(); - - URI url = new URI("http://localhost:" + port + "/bar"); + public void testRequestToBarHandler() throws Exception { + URI url = new URI("http://localhost:" + this.port + "/bar"); RequestEntity request = RequestEntity.get(url).build(); - ResponseEntity response = restTemplate.exchange(request, byte[].class); + ResponseEntity response = new RestTemplate().exchange(request, byte[].class); assertEquals(HttpStatus.OK, response.getStatusCode()); - assertArrayEquals("bar".getBytes(UTF_8), response.getBody()); + assertArrayEquals("bar".getBytes("UTF-8"), response.getBody()); } @Test - public void testHeaderSettingHandler() throws Exception { - - RestTemplate restTemplate = new RestTemplate(); - - URI url = new URI("http://localhost:" + port + "/header"); + public void testRequestToHeaderSettingHandler() throws Exception { + URI url = new URI("http://localhost:" + this.port + "/header"); RequestEntity request = RequestEntity.get(url).build(); - ResponseEntity response = restTemplate.exchange(request, byte[].class); + ResponseEntity response = new RestTemplate().exchange(request, byte[].class); assertEquals(HttpStatus.OK, response.getStatusCode()); assertEquals("bar", response.getHeaders().getFirst("foo")); } @Test - public void testNotFound() throws Exception { - - RestTemplate restTemplate = new RestTemplate(); - - URI url = new URI("http://localhost:" + port + "/oops"); + public void testHandlerNotFound() throws Exception { + URI url = new URI("http://localhost:" + this.port + "/oops"); RequestEntity request = RequestEntity.get(url).build(); try { - restTemplate.exchange(request, byte[].class); + new RestTemplate().exchange(request, byte[].class); } catch (HttpClientErrorException ex) { assertEquals(HttpStatus.NOT_FOUND, ex.getStatusCode()); } } - - private static class TestSimpleUrlHandlerMapping extends SimpleUrlHandlerMapping { - - public TestSimpleUrlHandlerMapping() { - Map map = new HashMap<>(); - map.put("/foo", new FooHandler()); - map.put("/bar", new BarHandler()); - map.put("/header", new HeaderSettingHandler()); - setUrlMap(map); - } - } - private static DataBuffer asDataBuffer(String text) { - return new DefaultDataBufferFactory().allocateBuffer() - .write(text.getBytes(StandardCharsets.UTF_8)); + DefaultDataBuffer buffer = new DefaultDataBufferFactory().allocateBuffer(); + return buffer.write(text.getBytes(StandardCharsets.UTF_8)); } - private static class FooHandler implements WebHandler { - - @Override - public Mono handle(ServerWebExchange exchange) { - DataBuffer buffer = asDataBuffer("foo"); - return exchange.getResponse().writeWith(Flux.just(buffer)); - } - } - - private static class BarHandler implements WebHandler { - - @Override - public Mono handle(ServerWebExchange exchange) { - DataBuffer buffer = asDataBuffer("bar"); - return exchange.getResponse().writeWith(Flux.just(buffer)); - } - } - - private static class HeaderSettingHandler implements WebHandler { - - @Override - public Mono handle(ServerWebExchange exchange) { - exchange.getResponse().getHeaders().add("foo", "bar"); - return Mono.empty(); - } - } @Configuration + @SuppressWarnings({"unused", "WeakerAccess"}) static class WebConfig { @Bean - public TestSimpleUrlHandlerMapping handlerMapping() { - return new TestSimpleUrlHandlerMapping(); + public SimpleUrlHandlerMapping handlerMapping() { + return new SimpleUrlHandlerMapping() { + { + Map map = new HashMap<>(); + map.put("/foo", (WebHandler) exchange -> + exchange.getResponse().writeWith(Flux.just(asDataBuffer("foo")))); + map.put("/bar", (WebHandler) exchange -> + exchange.getResponse().writeWith(Flux.just(asDataBuffer("bar")))); + map.put("/header", (WebHandler) exchange -> { + exchange.getResponse().getHeaders().add("foo", "bar"); + return Mono.empty(); + }); + setUrlMap(map); + } + }; } @Bean @@ -190,7 +149,6 @@ public class WebHandlerIntegrationTests extends AbstractHttpHandlerIntegrationTe public SimpleResultHandler resultHandler() { return new SimpleResultHandler(new DefaultConversionService()); } - } } From 9c438a8f7831d4841b82149f317804a262946f94 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 5 Jul 2016 18:20:31 -0400 Subject: [PATCH 467/498] Fix failing test --- .../web/reactive/DispatcherHandlerErrorTests.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java index cbd70fab9c..affa9c3f75 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java @@ -17,6 +17,7 @@ package org.springframework.web.reactive; import java.net.URI; +import java.time.Duration; import java.util.Collections; import org.junit.Before; @@ -171,12 +172,9 @@ public class DispatcherHandlerErrorTests { WebExceptionHandler exceptionHandler = new ServerError500ExceptionHandler(); WebHandler webHandler = new ExceptionHandlingWebHandler(this.dispatcherHandler, exceptionHandler); - Mono publisher = webHandler.handle(this.exchange); - - TestSubscriber.subscribe(publisher) - .assertErrorWith(ex -> assertEquals( - HttpStatus.INTERNAL_SERVER_ERROR, this.exchange.getResponse().getStatusCode())); + webHandler.handle(this.exchange).block(Duration.ofSeconds(5)); + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, this.exchange.getResponse().getStatusCode()); } From 55193194c23cee602e0a98f750228b044c47fed8 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 5 Jul 2016 20:39:56 -0400 Subject: [PATCH 468/498] Polish tests --- ...tNegotiatingResultHandlerSupportTests.java | 28 ++- .../web/reactive/result/ResolvableMethod.java | 27 ++- .../method/HandlerMethodMappingTests.java | 12 +- .../method/InvocableHandlerMethodTests.java | 188 +++++++----------- 4 files changed, 115 insertions(+), 140 deletions(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/ContentNegotiatingResultHandlerSupportTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/ContentNegotiatingResultHandlerSupportTests.java index 211706577b..08bc361585 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/ContentNegotiatingResultHandlerSupportTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/ContentNegotiatingResultHandlerSupportTests.java @@ -52,7 +52,7 @@ import static org.springframework.web.reactive.HandlerMapping.PRODUCIBLE_MEDIA_T */ public class ContentNegotiatingResultHandlerSupportTests { - private TestHandlerSupport handlerSupport; + private TestResultHandler resultHandler; private MockServerHttpRequest request; @@ -61,7 +61,7 @@ public class ContentNegotiatingResultHandlerSupportTests { @Before public void setUp() throws Exception { - this.handlerSupport = new TestHandlerSupport(); + this.resultHandler = new TestResultHandler(); this.request = new MockServerHttpRequest(HttpMethod.GET, new URI("/path")); this.exchange = new DefaultServerWebExchange( this.request, new MockServerHttpResponse(), new MockWebSessionManager()); @@ -70,11 +70,9 @@ public class ContentNegotiatingResultHandlerSupportTests { @Test public void usesContentTypeResolver() throws Exception { - RequestedContentTypeResolver resolver = new FixedContentTypeResolver(IMAGE_GIF); - TestHandlerSupport handlerSupport = new TestHandlerSupport(resolver); - + TestResultHandler resultHandler = new TestResultHandler(new FixedContentTypeResolver(IMAGE_GIF)); List mediaTypes = Arrays.asList(IMAGE_JPEG, IMAGE_GIF, IMAGE_PNG); - MediaType actual = handlerSupport.selectMediaType(this.exchange, mediaTypes); + MediaType actual = resultHandler.selectMediaType(this.exchange, mediaTypes); assertEquals(IMAGE_GIF, actual); } @@ -85,7 +83,7 @@ public class ContentNegotiatingResultHandlerSupportTests { this.exchange.getAttributes().put(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, producible); List mediaTypes = Arrays.asList(IMAGE_JPEG, IMAGE_GIF, IMAGE_PNG); - MediaType actual = handlerSupport.selectMediaType(this.exchange, mediaTypes); + MediaType actual = resultHandler.selectMediaType(this.exchange, mediaTypes); assertEquals(IMAGE_GIF, actual); } @@ -95,7 +93,7 @@ public class ContentNegotiatingResultHandlerSupportTests { this.request.getHeaders().add("Accept", "text/plain; q=0.5, application/json"); List mediaTypes = Arrays.asList(TEXT_PLAIN, APPLICATION_JSON_UTF8); - MediaType actual = this.handlerSupport.selectMediaType(this.exchange, mediaTypes); + MediaType actual = this.resultHandler.selectMediaType(this.exchange, mediaTypes); assertEquals(APPLICATION_JSON_UTF8, actual); } @@ -104,9 +102,8 @@ public class ContentNegotiatingResultHandlerSupportTests { public void charsetFromAcceptHeader() throws Exception { MediaType text8859 = MediaType.parseMediaType("text/plain;charset=ISO-8859-1"); MediaType textUtf8 = MediaType.parseMediaType("text/plain;charset=UTF-8"); - this.request.getHeaders().setAccept(Collections.singletonList(text8859)); - MediaType actual = this.handlerSupport.selectMediaType(this.exchange, Collections.singletonList(textUtf8)); + MediaType actual = this.resultHandler.selectMediaType(this.exchange, Collections.singletonList(textUtf8)); assertEquals(text8859, actual); } @@ -114,21 +111,20 @@ public class ContentNegotiatingResultHandlerSupportTests { @Test // SPR-12894 public void noConcreteMediaType() throws Exception { List producible = Collections.singletonList(ALL); - MediaType actual = this.handlerSupport.selectMediaType(this.exchange, producible); + MediaType actual = this.resultHandler.selectMediaType(this.exchange, producible); assertEquals(APPLICATION_OCTET_STREAM, actual); } + @SuppressWarnings("WeakerAccess") + private static class TestResultHandler extends ContentNegotiatingResultHandlerSupport { - - private static class TestHandlerSupport extends ContentNegotiatingResultHandlerSupport { - - protected TestHandlerSupport() { + protected TestResultHandler() { this(new HeaderContentTypeResolver()); } - public TestHandlerSupport(RequestedContentTypeResolver contentTypeResolver) { + public TestResultHandler(RequestedContentTypeResolver contentTypeResolver) { super(new GenericConversionService(), contentTypeResolver); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/ResolvableMethod.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/ResolvableMethod.java index 2a83857cf1..f415b02079 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/ResolvableMethod.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/ResolvableMethod.java @@ -21,11 +21,14 @@ import java.util.ArrayList; import java.util.List; import java.util.Set; +import org.bouncycastle.util.Arrays; + import org.springframework.core.MethodIntrospector; import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; /** @@ -66,6 +69,8 @@ public class ResolvableMethod { private String methodName; + private Class[] argumentTypes; + private ResolvableType returnType; private final List> annotationTypes = new ArrayList<>(4); @@ -81,6 +86,11 @@ public class ResolvableMethod { return this; } + public ResolvableMethod argumentTypes(Class... argumentTypes) { + this.argumentTypes = argumentTypes; + return this; + } + public ResolvableMethod returning(ResolvableType resolvableType) { this.returnType = resolvableType; return this; @@ -94,16 +104,21 @@ public class ResolvableMethod { public Method resolve() { // String comparison (ResolvableType's with different providers) - String expected = this.returnType != null ? this.returnType.toString() : null; + String expectedReturnType = getReturnType(); Set methods = MethodIntrospector.selectMethods(this.targetClass, (ReflectionUtils.MethodFilter) method -> { - String actual = ResolvableType.forMethodReturnType(method).toString(); if (this.methodName != null && !this.methodName.equals(method.getName())) { return false; } - if (expected != null) { - if (!actual.equals(expected) && !Object.class.equals(method.getDeclaringClass())) { + if (getReturnType() != null) { + String actual = ResolvableType.forMethodReturnType(method).toString(); + if (!actual.equals(getReturnType()) && !Object.class.equals(method.getDeclaringClass())) { + return false; + } + } + if (!ObjectUtils.isEmpty(this.argumentTypes)) { + if (!Arrays.areEqual(this.argumentTypes, method.getParameterTypes())) { return false; } } @@ -121,6 +136,10 @@ public class ResolvableMethod { return methods.iterator().next(); } + private String getReturnType() { + return this.returnType != null ? this.returnType.toString() : null; + } + public MethodParameter resolveReturnType() { Method method = resolve(); return new MethodParameter(method, -1); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/HandlerMethodMappingTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/HandlerMethodMappingTests.java index 23608b0124..cb2165e400 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/HandlerMethodMappingTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/HandlerMethodMappingTests.java @@ -49,7 +49,6 @@ import static org.junit.Assert.assertNull; /** * Unit tests for {@link AbstractHandlerMethodMapping}. - * * @author Rossen Stoyanchev */ public class HandlerMethodMappingTests { @@ -82,8 +81,8 @@ public class HandlerMethodMappingTests { public void directMatch() throws Exception { String key = "foo"; this.mapping.registerMapping(key, this.handler, this.method1); - Mono result = this.mapping.getHandler(createExchange(HttpMethod.GET, key)); + assertEquals(this.method1, ((HandlerMethod) result.block()).getMethod()); } @@ -102,9 +101,7 @@ public class HandlerMethodMappingTests { this.mapping.registerMapping("/fo?", this.handler, this.method2); Mono result = this.mapping.getHandler(createExchange(HttpMethod.GET, "/foo")); - TestSubscriber - .subscribe(result) - .assertError(IllegalStateException.class); + TestSubscriber.subscribe(result).assertError(IllegalStateException.class); } @Test @@ -115,6 +112,7 @@ public class HandlerMethodMappingTests { this.mapping.registerMapping(key2, this.handler, this.method2); List directUrlMatches = this.mapping.getMappingRegistry().getMappingsByUrl(key1); + assertNotNull(directUrlMatches); assertEquals(1, directUrlMatches.size()); assertEquals(key1, directUrlMatches.get(0)); @@ -130,6 +128,7 @@ public class HandlerMethodMappingTests { this.mapping.registerMapping(key2, handler2, this.method1); List directUrlMatches = this.mapping.getMappingRegistry().getMappingsByUrl(key1); + assertNotNull(directUrlMatches); assertEquals(1, directUrlMatches.size()); assertEquals(key1, directUrlMatches.get(0)); @@ -140,14 +139,17 @@ public class HandlerMethodMappingTests { String key = "foo"; this.mapping.registerMapping(key, this.handler, this.method1); Mono result = this.mapping.getHandler(createExchange(HttpMethod.GET, key)); + assertNotNull(result.block()); this.mapping.unregisterMapping(key); result = this.mapping.getHandler(createExchange(HttpMethod.GET, key)); + assertNull(result.block()); assertNull(this.mapping.getMappingRegistry().getMappingsByUrl(key)); } + private ServerWebExchange createExchange(HttpMethod httpMethod, String path) throws URISyntaxException { ServerHttpRequest request = new MockServerHttpRequest(httpMethod, new URI(path)); WebSessionManager sessionManager = new MockWebSessionManager(); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/InvocableHandlerMethodTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/InvocableHandlerMethodTests.java index 9a5b5c26ed..c2944ea5de 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/InvocableHandlerMethodTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/InvocableHandlerMethodTests.java @@ -17,197 +17,156 @@ package org.springframework.web.reactive.result.method; import java.lang.reflect.Method; import java.net.URI; -import java.util.Arrays; import java.util.Collections; +import java.util.Optional; import org.junit.Before; import org.junit.Test; -import org.reactivestreams.Publisher; -import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import reactor.core.publisher.Signal; -import reactor.core.util.SignalKind; +import reactor.core.test.TestSubscriber; -import org.springframework.core.convert.support.GenericConversionService; -import org.springframework.http.server.reactive.ServerHttpRequest; -import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.http.HttpMethod; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; import org.springframework.ui.ExtendedModelMap; import org.springframework.ui.ModelMap; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerResult; -import org.springframework.web.reactive.result.method.annotation.RequestParamMethodArgumentResolver; +import org.springframework.web.reactive.result.ResolvableMethod; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange; import org.springframework.web.server.session.MockWebSessionManager; -import org.springframework.web.server.session.WebSessionManager; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; /** + * Unit tests for {@link InvocableHandlerMethod}. * @author Rossen Stoyanchev */ @SuppressWarnings("ThrowableResultOfMethodCallIgnored") public class InvocableHandlerMethodTests { - private ServerHttpRequest request; - private ServerWebExchange exchange; - private ModelMap model; + private ModelMap model = new ExtendedModelMap(); @Before public void setUp() throws Exception { - WebSessionManager sessionManager = new MockWebSessionManager(); - this.request = mock(ServerHttpRequest.class); - this.exchange = new DefaultServerWebExchange(request, mock(ServerHttpResponse.class), sessionManager); - this.model = new ExtendedModelMap(); + this.exchange = new DefaultServerWebExchange( + new MockServerHttpRequest(HttpMethod.GET, new URI("http://localhost:8080/path")), + new MockServerHttpResponse(), + new MockWebSessionManager()); } @Test - public void noArgsMethod() throws Exception { - InvocableHandlerMethod hm = createHandlerMethod("noArgs"); - + public void invokeMethodWithNoArguments() throws Exception { + InvocableHandlerMethod hm = handlerMethod("noArgs"); Mono mono = hm.invokeForRequest(this.exchange, this.model); - HandlerResult value = mono.block(); - assertNotNull(value); - assertEquals("success", value.getReturnValue().get()); + assertHandlerResultValue(mono, "success"); } @Test - public void resolveArgToZeroValues() throws Exception { - when(this.request.getURI()).thenReturn(new URI("http://localhost:8080/path")); - when(this.request.getQueryParams()).thenReturn(new LinkedMultiValueMap<>()); - InvocableHandlerMethod hm = createHandlerMethod("singleArg", String.class); - hm.setHandlerMethodArgumentResolvers(Collections.singletonList( - new RequestParamMethodArgumentResolver(new GenericConversionService(), null, false))); - + public void invokeMethodWithNoValue() throws Exception { + InvocableHandlerMethod hm = handlerMethod("singleArg"); + addResolver(hm, Mono.empty()); Mono mono = hm.invokeForRequest(this.exchange, this.model); - HandlerResult value = mono.block(); - assertNotNull(value); - assertEquals("success:null", value.getReturnValue().get()); + assertHandlerResultValue(mono, "success:null"); } @Test - public void resolveArgToOneValue() throws Exception { - InvocableHandlerMethod hm = createHandlerMethod("singleArg", String.class); + public void invokeMethodWithValue() throws Exception { + InvocableHandlerMethod hm = handlerMethod("singleArg", String.class); addResolver(hm, Mono.just("value1")); - Mono mono = hm.invokeForRequest(this.exchange, this.model); - HandlerResult value = mono.block(); - assertNotNull(value); - assertEquals("success:value1", value.getReturnValue().get()); + assertHandlerResultValue(mono, "success:value1"); } @Test - public void resolveArgToMultipleValues() throws Exception { - InvocableHandlerMethod hm = createHandlerMethod("singleArg", String.class); - addResolver(hm, Flux.fromIterable(Arrays.asList("value1", "value2", "value3"))); - + public void noMatchingResolver() throws Exception { + InvocableHandlerMethod hm = handlerMethod("singleArg", String.class); Mono mono = hm.invokeForRequest(this.exchange, this.model); - HandlerResult value = mono.block(); - assertNotNull(value); - assertEquals("success:value1", value.getReturnValue().get()); + TestSubscriber.subscribe(mono) + .assertError(IllegalStateException.class) + .assertErrorMessage("No resolver for argument [0] of type [java.lang.String] " + + "on method [" + hm.getMethod().toGenericString() + "]"); } @Test - public void noResolverForArg() throws Exception { - InvocableHandlerMethod hm = createHandlerMethod("singleArg", String.class); - - Publisher publisher = hm.invokeForRequest(this.exchange, this.model); - Throwable ex = awaitErrorSignal(publisher); - - assertEquals(IllegalStateException.class, ex.getClass()); - assertEquals("No resolver for argument [0] of type [java.lang.String] on method " + - "[" + hm.getMethod().toGenericString() + "]", ex.getMessage()); - } - - @Test - public void resolveArgumentWithThrownException() throws Exception { - HandlerMethodArgumentResolver resolver = mock(HandlerMethodArgumentResolver.class); - when(resolver.supportsParameter(any())).thenReturn(true); - when(resolver.resolveArgument(any(), any(), any())).thenThrow(new IllegalStateException("boo")); - - InvocableHandlerMethod hm = createHandlerMethod("singleArg", String.class); - hm.setHandlerMethodArgumentResolvers(Collections.singletonList(resolver)); - - Publisher publisher = hm.invokeForRequest(this.exchange, this.model); - Throwable ex = awaitErrorSignal(publisher); - - assertEquals(IllegalStateException.class, ex.getClass()); - assertEquals("Exception not wrapped with helpful argument details", - "Error resolving argument [0] of type [java.lang.String] on method " + - "[" + hm.getMethod().toGenericString() + "]", ex.getMessage()); - } - - @Test - public void resolveArgumentWithErrorSignal() throws Exception { - InvocableHandlerMethod hm = createHandlerMethod("singleArg", String.class); + public void resolverThrowsException() throws Exception { + InvocableHandlerMethod hm = handlerMethod("singleArg", String.class); addResolver(hm, Mono.error(new IllegalStateException("boo"))); + Mono mono = hm.invokeForRequest(this.exchange, this.model); - Publisher publisher = hm.invokeForRequest(this.exchange, this.model); - Throwable ex = awaitErrorSignal(publisher); - - assertEquals(IllegalStateException.class, ex.getClass()); - assertEquals("Exception not wrapped with helpful argument details", - "Error resolving argument [0] of type [java.lang.String] on method " + - "[" + hm.getMethod().toGenericString() + "]", ex.getMessage()); + TestSubscriber.subscribe(mono) + .assertError(IllegalStateException.class) + .assertErrorMessage("Error resolving argument [0] of type [java.lang.String] " + + "on method [" + hm.getMethod().toGenericString() + "]"); } @Test - public void illegalArgumentExceptionIsWrappedWithHelpfulDetails() throws Exception { - InvocableHandlerMethod hm = createHandlerMethod("singleArg", String.class); + public void resolverWithErrorSignal() throws Exception { + InvocableHandlerMethod hm = handlerMethod("singleArg", String.class); + addResolver(hm, Mono.error(new IllegalStateException("boo"))); + Mono mono = hm.invokeForRequest(this.exchange, this.model); + + TestSubscriber.subscribe(mono) + .assertError(IllegalStateException.class) + .assertErrorMessage("Error resolving argument [0] of type [java.lang.String] " + + "on method [" + hm.getMethod().toGenericString() + "]"); + } + + @Test + public void illegalArgumentExceptionIsWrappedWithInvocationDetails() throws Exception { + InvocableHandlerMethod hm = handlerMethod("singleArg", String.class); addResolver(hm, Mono.just(1)); + Mono mono = hm.invokeForRequest(this.exchange, this.model); - Publisher publisher = hm.invokeForRequest(this.exchange, this.model); - Throwable ex = awaitErrorSignal(publisher); - - assertEquals(IllegalStateException.class, ex.getClass()); - assertEquals("Failed to invoke controller with resolved arguments: " + - "[0][type=java.lang.Integer][value=1] " + - "on method [" + hm.getMethod().toGenericString() + "]", ex.getMessage()); + TestSubscriber.subscribe(mono) + .assertError(IllegalStateException.class) + .assertErrorMessage("Failed to invoke controller with resolved arguments: " + + "[0][type=java.lang.Integer][value=1] " + + "on method [" + hm.getMethod().toGenericString() + "]"); } @Test public void invocationTargetExceptionIsUnwrapped() throws Exception { - InvocableHandlerMethod hm = createHandlerMethod("exceptionMethod"); + InvocableHandlerMethod hm = handlerMethod("exceptionMethod"); + Mono mono = hm.invokeForRequest(this.exchange, this.model); - Publisher publisher = hm.invokeForRequest(this.exchange, this.model); - Throwable ex = awaitErrorSignal(publisher); - - assertEquals(IllegalStateException.class, ex.getClass()); - assertEquals("boo", ex.getMessage()); + TestSubscriber.subscribe(mono) + .assertError(IllegalStateException.class) + .assertErrorMessage("boo"); } - private InvocableHandlerMethod createHandlerMethod(String methodName, Class... argTypes) throws Exception { - Object controller = new TestController(); - Method method = controller.getClass().getMethod(methodName, argTypes); - return new InvocableHandlerMethod(new HandlerMethod(controller, method)); + private InvocableHandlerMethod handlerMethod(String name, Class... args) throws Exception { + Method method = ResolvableMethod.on(TestController.class).name(name).argumentTypes(args).resolve(); + return new InvocableHandlerMethod(new HandlerMethod(new TestController(), method)); } - private void addResolver(InvocableHandlerMethod handlerMethod, Publisher resolvedValue) { + private void addResolver(InvocableHandlerMethod handlerMethod, Mono resolvedValue) { HandlerMethodArgumentResolver resolver = mock(HandlerMethodArgumentResolver.class); when(resolver.supportsParameter(any())).thenReturn(true); - when(resolver.resolveArgument(any(), any(), any())).thenReturn(Mono.from(resolvedValue)); + when(resolver.resolveArgument(any(), any(), any())).thenReturn(resolvedValue); handlerMethod.setHandlerMethodArgumentResolvers(Collections.singletonList(resolver)); } - private Throwable awaitErrorSignal(Publisher publisher) throws Exception { - Signal signal = Flux.from(publisher).materialize().collectList().block().get(0); - assertEquals("Unexpected signal: " + signal, SignalKind.onError, signal.getType()); - return signal.getThrowable(); + private void assertHandlerResultValue(Mono mono, String expected) { + TestSubscriber.subscribe(mono).assertValuesWith(result -> { + Optional optional = result.getReturnValue(); + assertTrue(optional.isPresent()); + assertEquals(expected, optional.get()); + }); } @@ -218,7 +177,7 @@ public class InvocableHandlerMethodTests { return "success"; } - public String singleArg(@RequestParam(required=false) String q) { + public String singleArg(String q) { return "success:" + q; } @@ -227,5 +186,4 @@ public class InvocableHandlerMethodTests { } } - } From 7763391e8430e3091be560f490d20aeb22db21fb Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Wed, 6 Jul 2016 10:14:05 +0300 Subject: [PATCH 469/498] AbstractResponseBodySubscriber improvements When the thread is writing to the response in RECEIVED.onWritePossible(), the execution may stop because isReady() returned false. In this case the buffer is partially written. When there is partially written case: 1. The state will be changed from WRITING to RECEIVED 2. A check for "write possible" will be performed: - If onWritePossible event has been already called by the web container while in WRITING state then this check will trigger RECEIVED.onWritePossible() because isReady() will be true and the writing will continue. - Otherwise the writing will be resumed when the web container sends onWritePossible event. --- .../reactive/AbstractResponseBodySubscriber.java | 11 +++++++++++ .../server/reactive/ServletHttpHandlerAdapter.java | 4 +--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractResponseBodySubscriber.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractResponseBodySubscriber.java index 049d74b205..ea8e28db06 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractResponseBodySubscriber.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractResponseBodySubscriber.java @@ -101,6 +101,16 @@ abstract class AbstractResponseBodySubscriber implements Subscriber protected void receiveBuffer(DataBuffer dataBuffer) { Assert.state(this.currentBuffer == null); this.currentBuffer = dataBuffer; + + checkOnWritePossible(); + } + + /** + * Called when a {@link DataBuffer} is received via {@link Subscriber#onNext(Object)} + * or when only partial data from the {@link DataBuffer} was written. + */ + protected void checkOnWritePossible() { + // no-op } /** @@ -238,6 +248,7 @@ abstract class AbstractResponseBodySubscriber implements Subscriber } else { subscriber.changeState(WRITING, RECEIVED); + subscriber.checkOnWritePossible(); } } catch (IOException ex) { diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java index 4314ec221e..0416445ffa 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java @@ -233,9 +233,7 @@ public class ServletHttpHandlerAdapter extends HttpServlet { } @Override - protected void receiveBuffer(DataBuffer dataBuffer) { - super.receiveBuffer(dataBuffer); - + protected void checkOnWritePossible() { try { if (outputStream().isReady()) { onWritePossible(); From b0de99bc8cb0a2e20c49e7f627139204bfc13a13 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Tue, 5 Jul 2016 11:20:18 +0200 Subject: [PATCH 470/498] Refactor ResponseBodySubscriber to Processor This commit changes the AbstractResponseBodySubscriber into a AbstractResponseBodyProcessor, so that the processor can be used as a return value for writeWith. Additional, this commit no longer closes the response after an eror occurred. This fixes #59. --- .../AbstractRequestBodyPublisher.java | 27 +- .../AbstractResponseBodyProcessor.java | 486 ++++++++++++++++++ .../AbstractResponseBodySubscriber.java | 321 ------------ .../reactive/ServletHttpHandlerAdapter.java | 29 +- .../reactive/UndertowHttpHandlerAdapter.java | 7 +- .../reactive/UndertowServerHttpResponse.java | 27 +- .../ErrorHandlerIntegrationTests.java | 108 ++++ .../RandomHandlerIntegrationTests.java | 58 +-- 8 files changed, 638 insertions(+), 425 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractResponseBodyProcessor.java delete mode 100644 spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractResponseBodySubscriber.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ErrorHandlerIntegrationTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractRequestBodyPublisher.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractRequestBodyPublisher.java index 412ecdf8db..049b3c837b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractRequestBodyPublisher.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractRequestBodyPublisher.java @@ -52,6 +52,8 @@ abstract class AbstractRequestBodyPublisher implements Publisher { private Subscriber subscriber; + private volatile boolean dataAvailable; + @Override public void subscribe(Subscriber subscriber) { if (this.logger.isTraceEnabled()) { @@ -199,7 +201,9 @@ abstract class AbstractRequestBodyPublisher implements Publisher { void subscribe(AbstractRequestBodyPublisher publisher, Subscriber subscriber) { Objects.requireNonNull(subscriber); - if (publisher.changeState(this, DATA_UNAVAILABLE)) { + State newState = + publisher.dataAvailable ? DATA_AVAILABLE : DATA_UNAVAILABLE; + if (publisher.changeState(this, newState)) { Subscription subscription = new RequestBodySubscription( publisher); publisher.subscriber = subscriber; @@ -209,6 +213,11 @@ abstract class AbstractRequestBodyPublisher implements Publisher { throw new IllegalStateException(toString()); } } + + @Override + void onDataAvailable(AbstractRequestBodyPublisher publisher) { + publisher.dataAvailable = true; + } }, /** * State that gets entered when there is no data to be read. Responds to {@link @@ -252,20 +261,11 @@ abstract class AbstractRequestBodyPublisher implements Publisher { } } - @Override - void onDataAvailable(AbstractRequestBodyPublisher publisher) { - // ignore - } }, /** * The terminal completed state. Does not respond to any events. */ COMPLETED { - @Override - void subscribe(AbstractRequestBodyPublisher publisher, - Subscriber subscriber) { - // ignore - } @Override void request(AbstractRequestBodyPublisher publisher, long n) { @@ -277,11 +277,6 @@ abstract class AbstractRequestBodyPublisher implements Publisher { // ignore } - @Override - void onDataAvailable(AbstractRequestBodyPublisher publisher) { - // ignore - } - @Override void onAllDataRead(AbstractRequestBodyPublisher publisher) { // ignore @@ -309,7 +304,7 @@ abstract class AbstractRequestBodyPublisher implements Publisher { } void onDataAvailable(AbstractRequestBodyPublisher publisher) { - throw new IllegalStateException(toString()); + // ignore } void onAllDataRead(AbstractRequestBodyPublisher publisher) { diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractResponseBodyProcessor.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractResponseBodyProcessor.java new file mode 100644 index 0000000000..c187a5c49d --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractResponseBodyProcessor.java @@ -0,0 +1,486 @@ +/* + * 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.http.server.reactive; + +import java.io.IOException; +import java.nio.channels.Channel; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; +import javax.servlet.WriteListener; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.reactivestreams.Processor; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import reactor.core.util.BackpressureUtils; + +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.FlushingDataBuffer; +import org.springframework.core.io.buffer.support.DataBufferUtils; +import org.springframework.util.Assert; + +/** + * Abstract base class for {@code Subscriber} implementations that bridge between + * event-listener APIs and Reactive Streams. Specifically, base class for the Servlet 3.1 + * and Undertow support. + * @author Arjen Poutsma + * @see ServletServerHttpRequest + * @see UndertowHttpHandlerAdapter + */ +abstract class AbstractResponseBodyProcessor implements Processor { + + protected final Log logger = LogFactory.getLog(getClass()); + + private final AtomicReference subscriberState = + new AtomicReference<>(SubscriberState.UNSUBSCRIBED); + + private final AtomicReference publisherState = + new AtomicReference<>(PublisherState.UNSUBSCRIBED); + + private volatile DataBuffer currentBuffer; + + private volatile boolean subscriberCompleted; + + private volatile boolean publisherCompleted; + + private volatile Throwable publisherError; + + private Subscription subscription; + + private Subscriber subscriber; + + // Subscriber + + @Override + public final void onSubscribe(Subscription subscription) { + if (logger.isTraceEnabled()) { + logger.trace("SUB " + this.subscriberState + " onSubscribe: " + subscription); + } + this.subscriberState.get().onSubscribe(this, subscription); + } + + @Override + public final void onNext(DataBuffer dataBuffer) { + if (logger.isTraceEnabled()) { + logger.trace("SUB " + this.subscriberState + " onNext: " + dataBuffer); + } + this.subscriberState.get().onNext(this, dataBuffer); + } + + @Override + public final void onError(Throwable t) { + if (logger.isErrorEnabled()) { + logger.error("SUB " + this.subscriberState + " publishError: " + t, t); + } + this.subscriberState.get().onError(this, t); + } + + @Override + public final void onComplete() { + if (logger.isTraceEnabled()) { + logger.trace("SUB " + this.subscriberState + " onComplete"); + } + this.subscriberState.get().onComplete(this); + } + + // Publisher + + @Override + public final void subscribe(Subscriber subscriber) { + if (logger.isTraceEnabled()) { + logger.trace("PUB " + this.publisherState + " subscribe: " + subscriber); + } + this.publisherState.get().subscribe(this, subscriber); + } + + private void publishComplete() { + if (logger.isTraceEnabled()) { + logger.trace("PUB " + this.publisherState + " publishComplete"); + } + this.publisherState.get().publishComplete(this); + } + + private void publishError(Throwable t) { + if (logger.isTraceEnabled()) { + logger.trace("PUB " + this.publisherState + " publishError: " + t); + } + this.publisherState.get().publishError(this, t); + } + + // listener methods + + /** + * Called via a listener interface to indicate that writing is possible. + * @see WriteListener#onWritePossible() + * @see org.xnio.ChannelListener#handleEvent(Channel) + */ + protected final void onWritePossible() { + this.subscriberState.get().onWritePossible(this); + } + + /** + * Called when a {@link DataBuffer} is received via {@link Subscriber#onNext(Object)} + * @param dataBuffer the buffer that was received. + */ + protected void receiveBuffer(DataBuffer dataBuffer) { + Assert.state(this.currentBuffer == null); + this.currentBuffer = dataBuffer; + + checkOnWritePossible(); + } + + /** + * Called when a {@link DataBuffer} is received via {@link Subscriber#onNext(Object)} + * or when only partial data from the {@link DataBuffer} was written. + */ + protected void checkOnWritePossible() { + // no-op + } + + /** + * Called when the current buffer should be + * {@linkplain DataBufferUtils#release(DataBuffer) released}. + */ + protected void releaseBuffer() { + if (logger.isTraceEnabled()) { + logger.trace("releaseBuffer: " + this.currentBuffer); + } + DataBufferUtils.release(this.currentBuffer); + this.currentBuffer = null; + } + + /** + * Writes the given data buffer to the output, indicating if the entire buffer was + * written. + * @param dataBuffer the data buffer to write + * @return {@code true} if {@code dataBuffer} was fully written and a new buffer + * can be requested; {@code false} otherwise + */ + protected abstract boolean write(DataBuffer dataBuffer) throws IOException; + + /** + * Flushes the output. + */ + protected abstract void flush() throws IOException; + + /** + * Closes the output. + */ + protected abstract void close(); + + private boolean changeSubscriberState(SubscriberState oldState, + SubscriberState newState) { + return this.subscriberState.compareAndSet(oldState, newState); + } + + private boolean changePublisherState(PublisherState oldState, + PublisherState newState) { + return this.publisherState.compareAndSet(oldState, newState); + } + + private static final class ResponseBodySubscription implements Subscription { + + private final AbstractResponseBodyProcessor processor; + + public ResponseBodySubscription(AbstractResponseBodyProcessor processor) { + this.processor = processor; + } + + @Override + public final void request(long n) { + if (this.processor.logger.isTraceEnabled()) { + this.processor.logger.trace("PUB " + state() + " request: " + n); + } + state().request(this.processor, n); + } + + @Override + public final void cancel() { + if (this.processor.logger.isTraceEnabled()) { + this.processor.logger.trace("PUB " + state() + " cancel"); + } + state().cancel(this.processor); + } + + private PublisherState state() { + return this.processor.publisherState.get(); + } + + } + + /** + * Represents a state for the {@link Subscriber} to be in. The following figure + * indicate the four different states that exist, and the relationships between them. + * + *
    +	 *       UNSUBSCRIBED
    +	 *        |
    +	 *        v
    +	 * REQUESTED -------------------> RECEIVED
    +	 *         ^                      ^
    +	 *         |                      |
    +	 *         --------- WRITING <-----
    +	 *                      |
    +	 *                      v
    +	 *                  COMPLETED
    +	 * 
    + * Refer to the individual states for more information. + */ + private enum SubscriberState { + + /** + * The initial unsubscribed state. Will respond to {@code onSubscribe} by + * requesting 1 buffer from the subscription, and change state to {@link + * #REQUESTED}. + */ + UNSUBSCRIBED { + @Override + void onSubscribe(AbstractResponseBodyProcessor processor, + Subscription subscription) { + Objects.requireNonNull(subscription, "Subscription cannot be null"); + if (processor.changeSubscriberState(this, REQUESTED)) { + processor.subscription = subscription; + subscription.request(1); + } + else { + super.onSubscribe(processor, subscription); + } + } + }, + /** + * State that gets entered after a buffer has been + * {@linkplain Subscription#request(long) requested}. Responds to {@code onNext} + * by changing state to {@link #RECEIVED}, and responds to {@code onComplete} by + * changing state to {@link #COMPLETED}. + */ + REQUESTED { + @Override + void onNext(AbstractResponseBodyProcessor processor, DataBuffer dataBuffer) { + if (processor.changeSubscriberState(this, RECEIVED)) { + processor.receiveBuffer(dataBuffer); + } + } + + @Override + void onComplete(AbstractResponseBodyProcessor processor) { + if (processor.changeSubscriberState(this, COMPLETED)) { + processor.subscriberCompleted = true; + processor.close(); + processor.publishComplete(); + } + } + }, + /** + * State that gets entered after a buffer has been + * {@linkplain Subscriber#onNext(Object) received}. Responds to + * {@code onWritePossible} by writing the current buffer and changes + * the state to {@link #WRITING}. If it can be written completely, + * changes the state to either {@link #REQUESTED} if the subscription + * has not been completed; or {@link #COMPLETED} if it has. If it cannot + * be written completely the state will be changed to {@link #RECEIVED}. + */ + RECEIVED { + @Override + void onWritePossible(AbstractResponseBodyProcessor processor) { + if (processor.changeSubscriberState(this, WRITING)) { + DataBuffer dataBuffer = processor.currentBuffer; + try { + boolean writeCompleted = processor.write(dataBuffer); + if (writeCompleted) { + if (dataBuffer instanceof FlushingDataBuffer) { + processor.flush(); + } + processor.releaseBuffer(); + if (!processor.subscriberCompleted) { + processor.changeSubscriberState(WRITING, REQUESTED); + processor.subscription.request(1); + } + else { + processor.changeSubscriberState(WRITING, COMPLETED); + processor.close(); + processor.publishComplete(); + } + } + else { + processor.changeSubscriberState(WRITING, RECEIVED); + processor.checkOnWritePossible(); + } + } + catch (IOException ex) { + processor.onError(ex); + } + } + } + + @Override + void onComplete(AbstractResponseBodyProcessor processor) { + processor.subscriberCompleted = true; + } + }, + /** + * State that gets entered after a writing of the current buffer has been + * {@code onWritePossible started}. + */ + WRITING { + @Override + void onComplete(AbstractResponseBodyProcessor processor) { + processor.subscriberCompleted = true; + } + }, + /** + * The terminal completed state. Does not respond to any events. + */ + COMPLETED { + @Override + void onNext(AbstractResponseBodyProcessor processor, DataBuffer dataBuffer) { + // ignore + } + + @Override + void onError(AbstractResponseBodyProcessor processor, Throwable t) { + // ignore + } + + @Override + void onComplete(AbstractResponseBodyProcessor processor) { + // ignore + } + }; + + void onSubscribe(AbstractResponseBodyProcessor processor, Subscription s) { + s.cancel(); + } + + void onNext(AbstractResponseBodyProcessor processor, DataBuffer dataBuffer) { + throw new IllegalStateException(toString()); + } + + void onError(AbstractResponseBodyProcessor processor, Throwable t) { + if (processor.changeSubscriberState(this, COMPLETED)) { + processor.publishError(t); + } + } + + void onComplete(AbstractResponseBodyProcessor processor) { + throw new IllegalStateException(toString()); + } + + void onWritePossible(AbstractResponseBodyProcessor processor) { + // ignore + } + } + + private enum PublisherState { + UNSUBSCRIBED { + @Override + void subscribe(AbstractResponseBodyProcessor processor, + Subscriber subscriber) { + Objects.requireNonNull(subscriber); + if (processor.changePublisherState(this, SUBSCRIBED)) { + Subscription subscription = new ResponseBodySubscription(processor); + processor.subscriber = subscriber; + subscriber.onSubscribe(subscription); + if (processor.publisherCompleted) { + processor.publishComplete(); + } + else if (processor.publisherError != null) { + processor.publishError(processor.publisherError); + } + } + else { + throw new IllegalStateException(toString()); + } + } + + @Override + void publishComplete(AbstractResponseBodyProcessor processor) { + processor.publisherCompleted = true; + } + + @Override + void publishError(AbstractResponseBodyProcessor processor, Throwable t) { + processor.publisherError = t; + } + }, + SUBSCRIBED { + @Override + void request(AbstractResponseBodyProcessor processor, long n) { + BackpressureUtils.checkRequest(n, processor.subscriber); + } + + @Override + void publishComplete(AbstractResponseBodyProcessor processor) { + if (processor.changePublisherState(this, COMPLETED)) { + processor.subscriber.onComplete(); + } + } + + @Override + void publishError(AbstractResponseBodyProcessor processor, Throwable t) { + if (processor.changePublisherState(this, COMPLETED)) { + processor.subscriber.onError(t); + } + } + + }, + COMPLETED { + @Override + void request(AbstractResponseBodyProcessor processor, long n) { + // ignore + } + + @Override + void cancel(AbstractResponseBodyProcessor processor) { + // ignore + } + + @Override + void publishComplete(AbstractResponseBodyProcessor processor) { + // ignore + } + + @Override + void publishError(AbstractResponseBodyProcessor processor, Throwable t) { + // ignore + } + }; + + void subscribe(AbstractResponseBodyProcessor processor, + Subscriber subscriber) { + throw new IllegalStateException(toString()); + } + + void request(AbstractResponseBodyProcessor processor, long n) { + throw new IllegalStateException(toString()); + } + + void cancel(AbstractResponseBodyProcessor processor) { + processor.changePublisherState(this, COMPLETED); + } + + void publishComplete(AbstractResponseBodyProcessor processor) { + throw new IllegalStateException(toString()); + } + + void publishError(AbstractResponseBodyProcessor processor, Throwable t) { + throw new IllegalStateException(toString()); + } + + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractResponseBodySubscriber.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractResponseBodySubscriber.java deleted file mode 100644 index ea8e28db06..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractResponseBodySubscriber.java +++ /dev/null @@ -1,321 +0,0 @@ -/* - * 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.http.server.reactive; - -import java.io.IOException; -import java.nio.channels.Channel; -import java.util.Objects; -import java.util.concurrent.atomic.AtomicReference; -import javax.servlet.WriteListener; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; - -import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.FlushingDataBuffer; -import org.springframework.core.io.buffer.support.DataBufferUtils; -import org.springframework.util.Assert; - -/** - * Abstract base class for {@code Subscriber} implementations that bridge between - * event-listener APIs and Reactive Streams. Specifically, base class for the Servlet 3.1 - * and Undertow support. - * @author Arjen Poutsma - * @see ServletServerHttpRequest - * @see UndertowHttpHandlerAdapter - */ -abstract class AbstractResponseBodySubscriber implements Subscriber { - - protected final Log logger = LogFactory.getLog(getClass()); - - private final AtomicReference state = - new AtomicReference<>(State.UNSUBSCRIBED); - - private volatile DataBuffer currentBuffer; - - private volatile boolean subscriptionCompleted; - - private Subscription subscription; - - @Override - public final void onSubscribe(Subscription subscription) { - if (logger.isTraceEnabled()) { - logger.trace(this.state + " onSubscribe: " + subscription); - } - this.state.get().onSubscribe(this, subscription); - } - - @Override - public final void onNext(DataBuffer dataBuffer) { - if (logger.isTraceEnabled()) { - logger.trace(this.state + " onNext: " + dataBuffer); - } - this.state.get().onNext(this, dataBuffer); - } - - @Override - public final void onError(Throwable t) { - if (logger.isErrorEnabled()) { - logger.error(this.state + " onError: " + t, t); - } - this.state.get().onError(this, t); - } - - @Override - public final void onComplete() { - if (logger.isTraceEnabled()) { - logger.trace(this.state + " onComplete"); - } - this.state.get().onComplete(this); - } - - /** - * Called via a listener interface to indicate that writing is possible. - * @see WriteListener#onWritePossible() - * @see org.xnio.ChannelListener#handleEvent(Channel) - */ - protected final void onWritePossible() { - this.state.get().onWritePossible(this); - } - - /** - * Called when a {@link DataBuffer} is received via {@link Subscriber#onNext(Object)} - * @param dataBuffer the buffer that was received. - */ - protected void receiveBuffer(DataBuffer dataBuffer) { - Assert.state(this.currentBuffer == null); - this.currentBuffer = dataBuffer; - - checkOnWritePossible(); - } - - /** - * Called when a {@link DataBuffer} is received via {@link Subscriber#onNext(Object)} - * or when only partial data from the {@link DataBuffer} was written. - */ - protected void checkOnWritePossible() { - // no-op - } - - /** - * Called when the current buffer should be - * {@linkplain DataBufferUtils#release(DataBuffer) released}. - */ - protected void releaseBuffer() { - if (logger.isTraceEnabled()) { - logger.trace("releaseBuffer: " + this.currentBuffer); - } - DataBufferUtils.release(this.currentBuffer); - this.currentBuffer = null; - } - - /** - * Writes the given data buffer to the output, indicating if the entire buffer was - * written. - * @param dataBuffer the data buffer to write - * @return {@code true} if {@code dataBuffer} was fully written and a new buffer - * can be requested; {@code false} otherwise - */ - protected abstract boolean write(DataBuffer dataBuffer) throws IOException; - - /** - * Writes the given exception to the output. - */ - protected abstract void writeError(Throwable t); - - /** - * Flushes the output. - */ - protected abstract void flush() throws IOException; - - /** - * Closes the output. - */ - protected abstract void close(); - - private boolean changeState(State oldState, State newState) { - return this.state.compareAndSet(oldState, newState); - } - - /** - * Represents a state for the {@link Subscriber} to be in. The following figure - * indicate the four different states that exist, and the relationships between them. - * - *
    -	 *       UNSUBSCRIBED
    -	 *        |
    -	 *        v
    -	 * REQUESTED -------------------> RECEIVED
    -	 *         ^                      ^
    -	 *         |                      |
    -	 *         --------- WRITING <-----
    -	 *                      |
    -	 *                      v
    -	 *                  COMPLETED
    -	 * 
    - * Refer to the individual states for more information. - */ - private enum State { - - /** - * The initial unsubscribed state. Will respond to {@code onSubscribe} by - * requesting 1 buffer from the subscription, and change state to {@link - * #REQUESTED}. - */ - UNSUBSCRIBED { - @Override - void onSubscribe(AbstractResponseBodySubscriber subscriber, - Subscription subscription) { - Objects.requireNonNull(subscription, "Subscription cannot be null"); - if (subscriber.changeState(this, REQUESTED)) { - subscriber.subscription = subscription; - subscription.request(1); - } - else { - super.onSubscribe(subscriber, subscription); - } - } - }, - /** - * State that gets entered after a buffer has been - * {@linkplain Subscription#request(long) requested}. Responds to {@code onNext} - * by changing state to {@link #RECEIVED}, and responds to {@code onComplete} by - * changing state to {@link #COMPLETED}. - */ - REQUESTED { - @Override - void onNext(AbstractResponseBodySubscriber subscriber, - DataBuffer dataBuffer) { - if (subscriber.changeState(this, RECEIVED)) { - subscriber.receiveBuffer(dataBuffer); - } - } - - @Override - void onComplete(AbstractResponseBodySubscriber subscriber) { - if (subscriber.changeState(this, COMPLETED)) { - subscriber.subscriptionCompleted = true; - subscriber.close(); - } - } - }, - /** - * State that gets entered after a buffer has been - * {@linkplain Subscriber#onNext(Object) received}. Responds to - * {@code onWritePossible} by writing the current buffer and changes - * the state to {@link #WRITING}. If it can be written completely, - * changes the state to either {@link #REQUESTED} if the subscription - * has not been completed; or {@link #COMPLETED} if it has. If it cannot - * be written completely the state will be changed to {@link #RECEIVED}. - */ - RECEIVED { - @Override - void onWritePossible(AbstractResponseBodySubscriber subscriber) { - if (subscriber.changeState(this, WRITING)) { - DataBuffer dataBuffer = subscriber.currentBuffer; - try { - boolean writeCompleted = subscriber.write(dataBuffer); - if (writeCompleted) { - if (dataBuffer instanceof FlushingDataBuffer) { - subscriber.flush(); - } - subscriber.releaseBuffer(); - boolean subscriptionCompleted = subscriber.subscriptionCompleted; - if (!subscriptionCompleted) { - subscriber.changeState(WRITING, REQUESTED); - subscriber.subscription.request(1); - } - else { - subscriber.changeState(WRITING, COMPLETED); - subscriber.close(); - } - } - else { - subscriber.changeState(WRITING, RECEIVED); - subscriber.checkOnWritePossible(); - } - } - catch (IOException ex) { - subscriber.onError(ex); - } - } - } - - @Override - void onComplete(AbstractResponseBodySubscriber subscriber) { - subscriber.subscriptionCompleted = true; - } - }, - /** - * State that gets entered after a writing of the current buffer has been - * {@code onWritePossible started}. - */ - WRITING { - @Override - void onComplete(AbstractResponseBodySubscriber subscriber) { - subscriber.subscriptionCompleted = true; - } - }, - /** - * The terminal completed state. Does not respond to any events. - */ - COMPLETED { - @Override - void onNext(AbstractResponseBodySubscriber subscriber, - DataBuffer dataBuffer) { - // ignore - } - - @Override - void onError(AbstractResponseBodySubscriber subscriber, Throwable t) { - // ignore - } - - @Override - void onComplete(AbstractResponseBodySubscriber subscriber) { - // ignore - } - }; - - void onSubscribe(AbstractResponseBodySubscriber subscriber, Subscription s) { - s.cancel(); - } - - void onNext(AbstractResponseBodySubscriber subscriber, DataBuffer dataBuffer) { - throw new IllegalStateException(toString()); - } - - void onError(AbstractResponseBodySubscriber subscriber, Throwable t) { - if (subscriber.changeState(this, COMPLETED)) { - subscriber.writeError(t); - subscriber.close(); - } - } - - void onComplete(AbstractResponseBodySubscriber subscriber) { - throw new IllegalStateException(toString()); - } - - void onWritePossible(AbstractResponseBodySubscriber subscriber) { - // ignore - } - - } - -} diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java index 0416445ffa..61ff7409e3 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java @@ -38,7 +38,6 @@ import reactor.core.publisher.Mono; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DefaultDataBufferFactory; -import org.springframework.http.HttpStatus; import org.springframework.util.Assert; /** @@ -91,13 +90,15 @@ public class ServletHttpHandlerAdapter extends HttpServlet { ServletServerHttpRequest request = new ServletServerHttpRequest(servletRequest, requestBody); - ResponseBodySubscriber responseBody = - new ResponseBodySubscriber(synchronizer, this.bufferSize); + ResponseBodyProcessor responseBody = + new ResponseBodyProcessor(synchronizer, this.bufferSize); responseBody.registerListener(); ServletServerHttpResponse response = new ServletServerHttpResponse(servletResponse, this.dataBufferFactory, - publisher -> Mono - .from(subscriber -> publisher.subscribe(responseBody))); + publisher -> Mono.from(subscriber -> { + publisher.subscribe(responseBody); + responseBody.subscribe(subscriber); + })); HandlerResultSubscriber resultSubscriber = new HandlerResultSubscriber(synchronizer); @@ -129,7 +130,7 @@ public class ServletHttpHandlerAdapter extends HttpServlet { logger.error("Error from request handling. Completing the request.", ex); HttpServletResponse response = (HttpServletResponse) this.synchronizer.getResponse(); - response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); this.synchronizer.complete(); } @@ -206,8 +207,7 @@ public class ServletHttpHandlerAdapter extends HttpServlet { } } - - private static class ResponseBodySubscriber extends AbstractResponseBodySubscriber { + private static class ResponseBodyProcessor extends AbstractResponseBodyProcessor { private final ResponseBodyWriteListener writeListener = new ResponseBodyWriteListener(); @@ -218,7 +218,7 @@ public class ServletHttpHandlerAdapter extends HttpServlet { private volatile boolean flushOnNext; - public ResponseBodySubscriber(ServletAsyncContextSynchronizer synchronizer, + public ResponseBodyProcessor(ServletAsyncContextSynchronizer synchronizer, int bufferSize) { this.synchronizer = synchronizer; this.bufferSize = bufferSize; @@ -272,13 +272,6 @@ public class ServletHttpHandlerAdapter extends HttpServlet { } } - @Override - protected void writeError(Throwable t) { - HttpServletResponse response = - (HttpServletResponse) this.synchronizer.getResponse(); - response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); - } - @Override protected void flush() throws IOException { ServletOutputStream output = outputStream(); @@ -324,14 +317,14 @@ public class ServletHttpHandlerAdapter extends HttpServlet { @Override public void onWritePossible() throws IOException { - ResponseBodySubscriber.this.onWritePossible(); + ResponseBodyProcessor.this.onWritePossible(); } @Override public void onError(Throwable ex) { // Error on writing to the HTTP stream, so any further writes will probably // fail. Let's log instead of calling {@link #writeError}. - ResponseBodySubscriber.this.logger + ResponseBodyProcessor.this.logger .error("ResponseBodyWriteListener error", ex); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java index a856a7a101..7380725e55 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowHttpHandlerAdapter.java @@ -70,11 +70,8 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle @Override public void onError(Throwable ex) { - if (exchange.isResponseStarted() || exchange.getStatusCode() > 500) { - logger.error("Error from request handling. Completing the request.", - ex); - } - else { + logger.error("Error from request handling. Completing the request.", ex); + if (!exchange.isResponseStarted() && exchange.getStatusCode() <= 500) { exchange.setStatusCode(500); } exchange.endExchange(); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java index 3dc11cf395..0910723ea8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java @@ -79,13 +79,14 @@ public class UndertowServerHttpResponse extends AbstractServerHttpResponse @Override protected Mono writeWithInternal(Publisher publisher) { - return Mono.from(s -> { - // lazily create Subscriber, since calling - // {@link HttpServerExchange#getResponseChannel} as done in the - // ResponseBodySubscriber constructor commits the response status and headers - ResponseBodySubscriber subscriber = new ResponseBodySubscriber(this.exchange); - subscriber.registerListener(); - publisher.subscribe(subscriber); + // lazily create Subscriber, since calling + // {@link HttpServerExchange#getResponseChannel} as done in the + // ResponseBodyProcessor constructor commits the response status and headers + return Mono.from(subscriber -> { + ResponseBodyProcessor processor = new ResponseBodyProcessor(this.exchange); + processor.registerListener(); + publisher.subscribe(processor); + processor.subscribe(subscriber); }); } @@ -137,7 +138,7 @@ public class UndertowServerHttpResponse extends AbstractServerHttpResponse } } - private static class ResponseBodySubscriber extends AbstractResponseBodySubscriber { + private static class ResponseBodyProcessor extends AbstractResponseBodyProcessor { private final ChannelListener listener = new WriteListener(); @@ -147,7 +148,7 @@ public class UndertowServerHttpResponse extends AbstractServerHttpResponse private volatile ByteBuffer byteBuffer; - public ResponseBodySubscriber(HttpServerExchange exchange) { + public ResponseBodyProcessor(HttpServerExchange exchange) { this.exchange = exchange; this.responseChannel = exchange.getResponseChannel(); } @@ -157,14 +158,6 @@ public class UndertowServerHttpResponse extends AbstractServerHttpResponse this.responseChannel.resumeWrites(); } - @Override - protected void writeError(Throwable t) { - if (!this.exchange.isResponseStarted() && - this.exchange.getStatusCode() < 500) { - this.exchange.setStatusCode(500); - } - } - @Override protected void flush() throws IOException { if (logger.isTraceEnabled()) { diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ErrorHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ErrorHandlerIntegrationTests.java new file mode 100644 index 0000000000..c7dfa83f73 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ErrorHandlerIntegrationTests.java @@ -0,0 +1,108 @@ +/* + * 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.http.server.reactive; + +import java.io.IOException; +import java.net.URI; + +import org.junit.Test; +import reactor.core.publisher.Mono; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.http.server.reactive.boot.ReactorHttpServer; +import org.springframework.web.client.ResponseErrorHandler; +import org.springframework.web.client.RestTemplate; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assume.assumeFalse; + +/** + * @author Arjen Poutsma + */ +public class ErrorHandlerIntegrationTests extends AbstractHttpHandlerIntegrationTests { + + private ErrorHandler handler = new ErrorHandler(); + + @Override + protected HttpHandler createHttpHandler() { + return handler; + } + + @Test + public void response() throws Exception { + // TODO: fix Reactor + assumeFalse(server instanceof ReactorHttpServer); + + RestTemplate restTemplate = new RestTemplate(); + restTemplate.setErrorHandler(NO_OP_ERROR_HANDLER); + + ResponseEntity response = restTemplate + .getForEntity(new URI("http://localhost:" + port + "/response"), + String.class); + + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode()); + } + + @Test + public void returnValue() throws Exception { + // TODO: fix Reactor + assumeFalse(server instanceof ReactorHttpServer); + + RestTemplate restTemplate = new RestTemplate(); + restTemplate.setErrorHandler(NO_OP_ERROR_HANDLER); + + ResponseEntity response = restTemplate + .getForEntity(new URI("http://localhost:" + port + "/returnValue"), + String.class); + + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode()); + } + + private static class ErrorHandler implements HttpHandler { + + @Override + public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { + Exception error = new UnsupportedOperationException(); + String path = request.getURI().getPath(); + if (path.endsWith("response")) { + return response.writeWith(Mono.error(error)); + } + else if (path.endsWith("returnValue")) { + return Mono.error(error); + } + else { + return Mono.empty(); + } + } + } + + private static final ResponseErrorHandler NO_OP_ERROR_HANDLER = + new ResponseErrorHandler() { + + @Override + public boolean hasError(ClientHttpResponse response) throws IOException { + return false; + } + + @Override + public void handleError(ClientHttpResponse response) throws IOException { + } + }; + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/RandomHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/RandomHandlerIntegrationTests.java index 095c8f86e0..18062e4360 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/RandomHandlerIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/RandomHandlerIntegrationTests.java @@ -21,8 +21,6 @@ import java.util.Random; import org.junit.Test; import org.reactivestreams.Publisher; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -31,12 +29,9 @@ import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; -import org.springframework.http.server.reactive.boot.ReactorHttpServer; import org.springframework.web.client.RestTemplate; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assume.assumeFalse; +import static org.junit.Assert.*; public class RandomHandlerIntegrationTests extends AbstractHttpHandlerIntegrationTests { @@ -60,7 +55,6 @@ public class RandomHandlerIntegrationTests extends AbstractHttpHandlerIntegratio @Test public void random() throws Throwable { // TODO: fix Reactor support - assumeFalse(server instanceof ReactorHttpServer); RestTemplate restTemplate = new RestTemplate(); @@ -72,14 +66,6 @@ public class RandomHandlerIntegrationTests extends AbstractHttpHandlerIntegratio assertEquals(RESPONSE_SIZE, response.getHeaders().getContentLength()); assertEquals(RESPONSE_SIZE, response.getBody().length); - - while (!handler.requestComplete) { - Thread.sleep(100); - } - if (handler.requestError != null) { - throw handler.requestError; - } - assertEquals(REQUEST_SIZE, handler.requestSize); } @@ -93,45 +79,21 @@ public class RandomHandlerIntegrationTests extends AbstractHttpHandlerIntegratio public static final int CHUNKS = 16; - private volatile boolean requestComplete; - - private int requestSize; - - private Throwable requestError; - @Override public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { - requestError = null; + Mono requestSizeMono = request.getBody(). + reduce(0, (integer, dataBuffer) -> integer + + dataBuffer.readableByteCount()). + doAfterTerminate((size, throwable) -> { + assertNull(throwable); + assertEquals(REQUEST_SIZE, (long) size); + }); - request.getBody().subscribe(new Subscriber() { - @Override - public void onSubscribe(Subscription s) { - requestComplete = false; - requestSize = 0; - requestError = null; - s.request(Long.MAX_VALUE); - } - - @Override - public void onNext(DataBuffer bytes) { - requestSize += bytes.readableByteCount(); - } - - @Override - public void onError(Throwable t) { - requestComplete = true; - requestError = t; - } - - @Override - public void onComplete() { - requestComplete = true; - } - }); response.getHeaders().setContentLength(RESPONSE_SIZE); - return response.writeWith(multipleChunks()); + + return requestSizeMono.then(response.writeWith(multipleChunks())); } private Publisher singleChunk() { From 9ee4f6ee305b82b68b61576bfd60bbc178657917 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 6 Jul 2016 08:59:37 -0400 Subject: [PATCH 471/498] Polish tests --- .../web/reactive/result/ResolvableMethod.java | 46 ++++++- .../method/InvocableHandlerMethodTests.java | 14 +- .../RequestBodyArgumentResolverTests.java | 129 +++++++++++------- .../ViewResolutionResultHandlerTests.java | 60 ++++---- 4 files changed, 152 insertions(+), 97 deletions(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/ResolvableMethod.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/ResolvableMethod.java index f415b02079..bd2ede3151 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/ResolvableMethod.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/ResolvableMethod.java @@ -18,10 +18,10 @@ package org.springframework.web.reactive.result; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Set; - -import org.bouncycastle.util.Arrays; +import java.util.function.Predicate; import org.springframework.core.MethodIntrospector; import org.springframework.core.MethodParameter; @@ -101,24 +101,23 @@ public class ResolvableMethod { return this; } + // Resolve methods public Method resolve() { - // String comparison (ResolvableType's with different providers) - String expectedReturnType = getReturnType(); - Set methods = MethodIntrospector.selectMethods(this.targetClass, (ReflectionUtils.MethodFilter) method -> { if (this.methodName != null && !this.methodName.equals(method.getName())) { return false; } if (getReturnType() != null) { + // String comparison (ResolvableType's with different providers) String actual = ResolvableType.forMethodReturnType(method).toString(); if (!actual.equals(getReturnType()) && !Object.class.equals(method.getDeclaringClass())) { return false; } } if (!ObjectUtils.isEmpty(this.argumentTypes)) { - if (!Arrays.areEqual(this.argumentTypes, method.getParameterTypes())) { + if (!Arrays.equals(this.argumentTypes, method.getParameterTypes())) { return false; } } @@ -145,6 +144,41 @@ public class ResolvableMethod { return new MethodParameter(method, -1); } + @SafeVarargs + public final MethodParameter resolveParam(Predicate... predicates) { + return resolveParam(null, predicates); + } + + @SafeVarargs + public final MethodParameter resolveParam(ResolvableType type, + Predicate... predicates) { + + List matches = new ArrayList<>(); + + Method method = resolve(); + for (int i = 0; i < method.getParameterCount(); i++) { + MethodParameter param = new MethodParameter(method, i); + if (type != null) { + if (!ResolvableType.forMethodParameter(param).toString().equals(type.toString())) { + continue; + } + } + if (!ObjectUtils.isEmpty(predicates)) { + if (Arrays.stream(predicates).filter(p -> !p.test(param)).findFirst().isPresent()) { + continue; + } + } + matches.add(param); + } + + Assert.isTrue(!matches.isEmpty(), "No matching method argument: " + this); + Assert.isTrue(matches.size() == 1, "Multiple matching method arguments: " + matches); + + return matches.get(0); + } + + + @Override public String toString() { diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/InvocableHandlerMethodTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/InvocableHandlerMethodTests.java index c2944ea5de..4984f9e056 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/InvocableHandlerMethodTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/InvocableHandlerMethodTests.java @@ -83,7 +83,7 @@ public class InvocableHandlerMethodTests { @Test public void invokeMethodWithValue() throws Exception { - InvocableHandlerMethod hm = handlerMethod("singleArg", String.class); + InvocableHandlerMethod hm = handlerMethod("singleArg"); addResolver(hm, Mono.just("value1")); Mono mono = hm.invokeForRequest(this.exchange, this.model); @@ -92,7 +92,7 @@ public class InvocableHandlerMethodTests { @Test public void noMatchingResolver() throws Exception { - InvocableHandlerMethod hm = handlerMethod("singleArg", String.class); + InvocableHandlerMethod hm = handlerMethod("singleArg"); Mono mono = hm.invokeForRequest(this.exchange, this.model); TestSubscriber.subscribe(mono) @@ -103,7 +103,7 @@ public class InvocableHandlerMethodTests { @Test public void resolverThrowsException() throws Exception { - InvocableHandlerMethod hm = handlerMethod("singleArg", String.class); + InvocableHandlerMethod hm = handlerMethod("singleArg"); addResolver(hm, Mono.error(new IllegalStateException("boo"))); Mono mono = hm.invokeForRequest(this.exchange, this.model); @@ -115,7 +115,7 @@ public class InvocableHandlerMethodTests { @Test public void resolverWithErrorSignal() throws Exception { - InvocableHandlerMethod hm = handlerMethod("singleArg", String.class); + InvocableHandlerMethod hm = handlerMethod("singleArg"); addResolver(hm, Mono.error(new IllegalStateException("boo"))); Mono mono = hm.invokeForRequest(this.exchange, this.model); @@ -127,7 +127,7 @@ public class InvocableHandlerMethodTests { @Test public void illegalArgumentExceptionIsWrappedWithInvocationDetails() throws Exception { - InvocableHandlerMethod hm = handlerMethod("singleArg", String.class); + InvocableHandlerMethod hm = handlerMethod("singleArg"); addResolver(hm, Mono.just(1)); Mono mono = hm.invokeForRequest(this.exchange, this.model); @@ -149,8 +149,8 @@ public class InvocableHandlerMethodTests { } - private InvocableHandlerMethod handlerMethod(String name, Class... args) throws Exception { - Method method = ResolvableMethod.on(TestController.class).name(name).argumentTypes(args).resolve(); + private InvocableHandlerMethod handlerMethod(String name) throws Exception { + Method method = ResolvableMethod.on(TestController.class).name(name).resolve(); return new InvocableHandlerMethod(new HandlerMethod(new TestController(), method)); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java index d0171d69da..0c8e36d6f8 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java @@ -15,7 +15,6 @@ */ package org.springframework.web.reactive.result.method.annotation; -import java.lang.reflect.Method; import java.net.URI; import java.nio.ByteBuffer; import java.nio.charset.Charset; @@ -37,10 +36,8 @@ import reactor.core.test.TestSubscriber; import rx.Observable; import rx.Single; -import org.springframework.core.LocalVariableTableParameterNameDiscoverer; import org.springframework.core.MethodParameter; -import org.springframework.core.ParameterNameDiscoverer; -import org.springframework.core.annotation.SynthesizingMethodParameter; +import org.springframework.core.ResolvableType; import org.springframework.core.codec.Decoder; import org.springframework.core.codec.StringDecoder; import org.springframework.core.convert.support.MonoToCompletableFutureConverter; @@ -57,23 +54,24 @@ import org.springframework.http.converter.reactive.HttpMessageConverter; import org.springframework.http.server.reactive.MockServerHttpRequest; import org.springframework.http.server.reactive.MockServerHttpResponse; import org.springframework.ui.ExtendedModelMap; -import org.springframework.ui.ModelMap; -import org.springframework.util.ReflectionUtils; import org.springframework.validation.Errors; import org.springframework.validation.Validator; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.reactive.result.ResolvableMethod; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebInputException; import org.springframework.web.server.UnsupportedMediaTypeStatusException; import org.springframework.web.server.adapter.DefaultServerWebExchange; -import org.springframework.web.server.session.DefaultWebSessionManager; +import org.springframework.web.server.session.MockWebSessionManager; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import static org.springframework.core.ResolvableType.forClass; +import static org.springframework.core.ResolvableType.forClassWithGenerics; /** * Unit tests for {@link RequestBodyArgumentResolver}. @@ -81,23 +79,20 @@ import static org.junit.Assert.assertTrue; */ public class RequestBodyArgumentResolverTests { - private RequestBodyArgumentResolver resolver; + private RequestBodyArgumentResolver resolver = resolver(new JacksonJsonDecoder()); private ServerWebExchange exchange; private MockServerHttpRequest request; - private ModelMap model; + private ResolvableMethod testMethod = ResolvableMethod.on(this.getClass()).name("handle"); @Before public void setUp() throws Exception { - this.resolver = resolver(new JacksonJsonDecoder()); this.request = new MockServerHttpRequest(HttpMethod.GET, new URI("/path")); MockServerHttpResponse response = new MockServerHttpResponse(); - DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(); - this.exchange = new DefaultServerWebExchange(this.request, response, sessionManager); - this.model = new ExtendedModelMap(); + this.exchange = new DefaultServerWebExchange(this.request, response, new MockWebSessionManager()); } @@ -105,15 +100,21 @@ public class RequestBodyArgumentResolverTests { public void supports() throws Exception { RequestBodyArgumentResolver resolver = resolver(new StringDecoder()); - assertTrue(resolver.supportsParameter(parameter("monoTestBean"))); - assertFalse(resolver.supportsParameter(parameter("paramWithoutAnnotation"))); + ResolvableType type = forClassWithGenerics(Mono.class, TestBean.class); + MethodParameter param = this.testMethod.resolveParam(type); + assertTrue(resolver.supportsParameter(param)); + + MethodParameter parameter = this.testMethod.resolveParam(p -> !p.hasParameterAnnotations()); + assertFalse(resolver.supportsParameter(parameter)); } @Test public void missingContentType() throws Exception { String body = "{\"bar\":\"BARBAR\",\"foo\":\"FOOFOO\"}"; this.request.writeWith(Flux.just(dataBuffer(body))); - Mono result = this.resolver.resolveArgument(parameter("monoTestBean"), this.model, this.exchange); + ResolvableType type = forClassWithGenerics(Mono.class, TestBean.class); + MethodParameter param = this.testMethod.resolveParam(type); + Mono result = this.resolver.resolveArgument(param, new ExtendedModelMap(), this.exchange); TestSubscriber.subscribe(result) .assertError(UnsupportedMediaTypeStatusException.class); @@ -122,28 +123,41 @@ public class RequestBodyArgumentResolverTests { @Test @SuppressWarnings("unchecked") public void monoTestBean() throws Exception { String body = "{\"bar\":\"b1\",\"foo\":\"f1\"}"; - Mono mono = (Mono) resolveValue("monoTestBean", Mono.class, body); + ResolvableType type = forClassWithGenerics(Mono.class, TestBean.class); + MethodParameter param = this.testMethod.resolveParam(type); + Mono mono = (Mono) resolveValue(param, Mono.class, body); + assertEquals(new TestBean("f1", "b1"), mono.block()); } @Test @SuppressWarnings("unchecked") public void fluxTestBean() throws Exception { String body = "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]"; - Flux flux = (Flux) resolveValue("fluxTestBean", Flux.class, body); - assertEquals(Arrays.asList(new TestBean("f1", "b1"), new TestBean("f2", "b2")), flux.collectList().block()); + ResolvableType type = forClassWithGenerics(Flux.class, TestBean.class); + MethodParameter param = this.testMethod.resolveParam(type); + Flux flux = (Flux) resolveValue(param, Flux.class, body); + + assertEquals(Arrays.asList(new TestBean("f1", "b1"), new TestBean("f2", "b2")), + flux.collectList().block()); } @Test @SuppressWarnings("unchecked") public void singleTestBean() throws Exception { String body = "{\"bar\":\"b1\",\"foo\":\"f1\"}"; - Single single = (Single) resolveValue("singleTestBean", Single.class, body); + ResolvableType type = forClassWithGenerics(Single.class, TestBean.class); + MethodParameter param = this.testMethod.resolveParam(type); + Single single = (Single) resolveValue(param, Single.class, body); + assertEquals(new TestBean("f1", "b1"), single.toBlocking().value()); } @Test @SuppressWarnings("unchecked") public void observableTestBean() throws Exception { String body = "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]"; - Observable observable = (Observable) resolveValue("observableTestBean", Observable.class, body); + ResolvableType type = forClassWithGenerics(Observable.class, TestBean.class); + MethodParameter param = this.testMethod.resolveParam(type); + Observable observable = (Observable) resolveValue(param, Observable.class, body); + assertEquals(Arrays.asList(new TestBean("f1", "b1"), new TestBean("f2", "b2")), observable.toList().toBlocking().first()); } @@ -151,13 +165,21 @@ public class RequestBodyArgumentResolverTests { @Test @SuppressWarnings("unchecked") public void futureTestBean() throws Exception { String body = "{\"bar\":\"b1\",\"foo\":\"f1\"}"; - assertEquals(new TestBean("f1", "b1"), resolveValue("futureTestBean", CompletableFuture.class, body).get()); + ResolvableType type = forClassWithGenerics(CompletableFuture.class, TestBean.class); + MethodParameter param = this.testMethod.resolveParam(type); + CompletableFuture future = resolveValue(param, CompletableFuture.class, body); + + assertEquals(new TestBean("f1", "b1"), future.get()); } @Test public void testBean() throws Exception { String body = "{\"bar\":\"b1\",\"foo\":\"f1\"}"; - assertEquals(new TestBean("f1", "b1"), resolveValue("testBean", TestBean.class, body)); + MethodParameter param = this.testMethod.resolveParam( + forClass(TestBean.class), p -> p.hasParameterAnnotation(RequestBody.class)); + TestBean value = resolveValue(param, TestBean.class, body); + + assertEquals(new TestBean("f1", "b1"), value); } @Test @@ -166,47 +188,65 @@ public class RequestBodyArgumentResolverTests { Map map = new HashMap<>(); map.put("foo", "f1"); map.put("bar", "b1"); - assertEquals(map, resolveValue("map", Map.class, body)); + ResolvableType type = forClassWithGenerics(Map.class, String.class, String.class); + MethodParameter param = this.testMethod.resolveParam(type); + Map actual = resolveValue(param, Map.class, body); + + assertEquals(map, actual); } @Test public void list() throws Exception { String body = "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]"; - assertEquals(Arrays.asList(new TestBean("f1", "b1"), new TestBean("f2", "b2")), - resolveValue("list", List.class, body)); + ResolvableType type = forClassWithGenerics(List.class, TestBean.class); + MethodParameter param = this.testMethod.resolveParam(type); + List list = resolveValue(param, List.class, body); + + assertEquals(Arrays.asList(new TestBean("f1", "b1"), new TestBean("f2", "b2")), list); } @Test public void array() throws Exception { String body = "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]"; - assertArrayEquals(new TestBean[] {new TestBean("f1", "b1"), new TestBean("f2", "b2")}, - resolveValue("array", TestBean[].class, body)); + ResolvableType type = forClass(TestBean[].class); + MethodParameter param = this.testMethod.resolveParam(type); + TestBean[] value = resolveValue(param, TestBean[].class, body); + + assertArrayEquals(new TestBean[] {new TestBean("f1", "b1"), new TestBean("f2", "b2")}, value); } @Test @SuppressWarnings("unchecked") public void validateMonoTestBean() throws Exception { String body = "{\"bar\":\"b1\"}"; - Mono mono = (Mono) resolveValue("monoTestBean", Mono.class, body); - TestSubscriber.subscribe(mono).assertNoValues().assertError(ServerWebInputException.class); + ResolvableType type = forClassWithGenerics(Mono.class, TestBean.class); + MethodParameter param = this.testMethod.resolveParam(type); + Mono mono = resolveValue(param, Mono.class, body); + + TestSubscriber.subscribe(mono) + .assertNoValues() + .assertError(ServerWebInputException.class); } @Test @SuppressWarnings("unchecked") public void validateFluxTestBean() throws Exception { String body = "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\"}]"; - Flux flux = (Flux) resolveValue("fluxTestBean", Flux.class, body); + ResolvableType type = forClassWithGenerics(Flux.class, TestBean.class); + MethodParameter param = this.testMethod.resolveParam(type); + Flux flux = resolveValue(param, Flux.class, body); - TestSubscriber.subscribe(flux).assertValues(new TestBean("f1", "b1")) + TestSubscriber.subscribe(flux) + .assertValues(new TestBean("f1", "b1")) .assertError(ServerWebInputException.class); } @SuppressWarnings("unchecked") - private T resolveValue(String paramName, Class valueType, String body) { + private T resolveValue(MethodParameter param, Class valueType, String body) { this.request.getHeaders().setContentType(MediaType.APPLICATION_JSON); this.request.writeWith(Flux.just(dataBuffer(body))); - Mono result = this.resolver.resolveArgument(parameter(paramName), this.model, this.exchange); + Mono result = this.resolver.resolveArgument(param, new ExtendedModelMap(), this.exchange); Object value = result.block(Duration.ofSeconds(5)); assertNotNull(value); @@ -228,20 +268,6 @@ public class RequestBodyArgumentResolverTests { return new RequestBodyArgumentResolver(converters, service, new TestBeanValidator()); } - @SuppressWarnings("ConfusingArgumentToVarargsMethod") - private MethodParameter parameter(String name) { - ParameterNameDiscoverer nameDiscoverer = new LocalVariableTableParameterNameDiscoverer(); - Method method = ReflectionUtils.findMethod(getClass(), "handle", (Class[]) null); - String[] names = nameDiscoverer.getParameterNames(method); - for (int i=0; i < names.length; i++) { - if (name.equals(names[i])) { - return new SynthesizingMethodParameter(method, i); - } - } - throw new IllegalArgumentException("Invalid parameter name '" + name + "'. Actual parameters: " + - Arrays.toString(names)); - } - private DataBuffer dataBuffer(String body) { byte[] bytes = body.getBytes(Charset.forName("UTF-8")); ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); @@ -266,16 +292,17 @@ public class RequestBodyArgumentResolverTests { @XmlRootElement - static class TestBean { + private static class TestBean { private String foo; private String bar; + @SuppressWarnings("unused") public TestBean() { } - public TestBean(String foo, String bar) { + TestBean(String foo, String bar) { this.foo = foo; this.bar = bar; } @@ -319,7 +346,7 @@ public class RequestBodyArgumentResolverTests { } } - static class TestBeanValidator implements Validator { + private static class TestBeanValidator implements Validator { @Override public boolean supports(Class clazz) { diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java index 03b6581a24..a06c70cd23 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java @@ -79,6 +79,8 @@ public class ViewResolutionResultHandlerTests { private ServerWebExchange exchange; + private ModelMap model = new ExtendedModelMap(); + @Before public void setUp() throws Exception { @@ -102,7 +104,7 @@ public class ViewResolutionResultHandlerTests { testSupports(ResolvableType.forClass(TestBean.class), true); testSupports(ResolvableType.forClass(Integer.class), false); - testSupports(resolvableMethod().annotated(ModelAttribute.class), true); + testSupports(ResolvableMethod.on(TestController.class).annotated(ModelAttribute.class), true); } @Test @@ -151,7 +153,7 @@ public class ViewResolutionResultHandlerTests { String responseBody = "account: {id=123, testBean=TestBean[name=Joe]}"; testHandle("/account", returnType, returnValue, responseBody, resolver); - testHandle("/account", resolvableMethod().annotated(ModelAttribute.class), + testHandle("/account", ResolvableMethod.on(TestController.class).annotated(ModelAttribute.class), 99L, "account: {id=123, num=99}", resolver); } @@ -194,8 +196,7 @@ public class ViewResolutionResultHandlerTests { public void unresolvedViewName() throws Exception { String returnValue = "account"; ResolvableType type = ResolvableType.forClass(String.class); - ExtendedModelMap model = new ExtendedModelMap(); - HandlerResult handlerResult = new HandlerResult(new Object(), returnValue, returnType(type), model); + HandlerResult handlerResult = new HandlerResult(new Object(), returnValue, returnType(type), this.model); this.request.setUri(new URI("/path")); Mono mono = createResultHandler().handleResult(this.exchange, handlerResult); @@ -207,8 +208,7 @@ public class ViewResolutionResultHandlerTests { public void contentNegotiation() throws Exception { TestBean value = new TestBean("Joe"); ResolvableType type = ResolvableType.forClass(TestBean.class); - ExtendedModelMap model = new ExtendedModelMap(); - HandlerResult handlerResult = new HandlerResult(new Object(), value, returnType(type), model); + HandlerResult handlerResult = new HandlerResult(new Object(), value, returnType(type), this.model); this.request.getHeaders().setAccept(Collections.singletonList(APPLICATION_JSON)); this.request.setUri(new URI("/account")); @@ -227,8 +227,7 @@ public class ViewResolutionResultHandlerTests { public void contentNegotiationWith406() throws Exception { TestBean value = new TestBean("Joe"); ResolvableType type = ResolvableType.forClass(TestBean.class); - ExtendedModelMap model = new ExtendedModelMap(); - HandlerResult handlerResult = new HandlerResult(new Object(), value, returnType(type), model); + HandlerResult handlerResult = new HandlerResult(new Object(), value, returnType(type), this.model); this.request.getHeaders().setAccept(Collections.singletonList(APPLICATION_JSON)); this.request.setUri(new URI("/account")); @@ -240,23 +239,7 @@ public class ViewResolutionResultHandlerTests { private MethodParameter returnType(ResolvableType type) { - return resolvableMethod().returning(type).resolveReturnType(); - } - - private ResolvableMethod resolvableMethod() { - return ResolvableMethod.on(TestController.class); - } - - private void testSupports(ResolvableType type, boolean result) { - testSupports(resolvableMethod().returning(type), result); - } - - private void testSupports(ResolvableMethod resolvableMethod, boolean result) { - ViewResolutionResultHandler resultHandler = createResultHandler(mock(ViewResolver.class)); - MethodParameter returnType = resolvableMethod.resolveReturnType(); - ExtendedModelMap model = new ExtendedModelMap(); - HandlerResult handlerResult = new HandlerResult(new Object(), null, returnType, model); - assertEquals(result, resultHandler.supports(handlerResult)); + return ResolvableMethod.on(TestController.class).returning(type).resolveReturnType(); } private ViewResolutionResultHandler createResultHandler(ViewResolver... resolvers) { @@ -264,7 +247,6 @@ public class ViewResolutionResultHandlerTests { } private ViewResolutionResultHandler createResultHandler(List defaultViews, ViewResolver... resolvers) { - FormattingConversionService service = new DefaultFormattingConversionService(); service.addConverter(new MonoToCompletableFutureConverter()); service.addConverter(new ReactorToRxJava1Converter()); @@ -275,10 +257,22 @@ public class ViewResolutionResultHandlerTests { return handler; } + private void testSupports(ResolvableType type, boolean result) { + testSupports(ResolvableMethod.on(TestController.class).returning(type), result); + } + + private void testSupports(ResolvableMethod resolvableMethod, boolean result) { + ViewResolutionResultHandler resultHandler = createResultHandler(mock(ViewResolver.class)); + MethodParameter returnType = resolvableMethod.resolveReturnType(); + HandlerResult handlerResult = new HandlerResult(new Object(), null, returnType, this.model); + assertEquals(result, resultHandler.supports(handlerResult)); + } + private void testHandle(String path, ResolvableType returnType, Object returnValue, String responseBody, ViewResolver... resolvers) throws URISyntaxException { - testHandle(path, resolvableMethod().returning(returnType), returnValue, responseBody, resolvers); + testHandle(path, ResolvableMethod.on(TestController.class).returning(returnType), + returnValue, responseBody, resolvers); } private void testHandle(String path, ResolvableMethod resolvableMethod, Object returnValue, @@ -306,11 +300,11 @@ public class ViewResolutionResultHandlerTests { private int order = Ordered.LOWEST_PRECEDENCE; - public TestViewResolver(String... viewNames) { + TestViewResolver(String... viewNames) { Arrays.stream(viewNames).forEach(name -> this.views.put(name, new TestView(name))); } - public void setOrder(int order) { + void setOrder(int order) { this.order = order; } @@ -327,19 +321,19 @@ public class ViewResolutionResultHandlerTests { } - public static final class TestView implements View { + private static final class TestView implements View { private final String name; private final List mediaTypes; - public TestView(String name) { + TestView(String name) { this.name = name; this.mediaTypes = Collections.singletonList(MediaType.TEXT_HTML); } - public TestView(String name, MediaType... mediaTypes) { + TestView(String name, MediaType... mediaTypes) { this.name = name; this.mediaTypes = Arrays.asList(mediaTypes); } @@ -371,7 +365,7 @@ public class ViewResolutionResultHandlerTests { private final String name; - public TestBean(String name) { + TestBean(String name) { this.name = name; } From 6525504e17430ac6c32b1a7f7a169cbdcfb9ca01 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 6 Jul 2016 09:35:25 -0400 Subject: [PATCH 472/498] Restore local vars in JacksonJsonEncoder --- .../http/codec/json/JacksonJsonEncoder.java | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/codec/json/JacksonJsonEncoder.java b/spring-web-reactive/src/main/java/org/springframework/http/codec/json/JacksonJsonEncoder.java index 0d7e177d09..53792aaf4a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/codec/json/JacksonJsonEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/codec/json/JacksonJsonEncoder.java @@ -81,13 +81,16 @@ public class JacksonJsonEncoder extends AbstractEncoder { return Flux.from(inputStream).map(value -> encodeValue(value, bufferFactory, elementType)); } - Flux array = Flux.from(inputStream) - .flatMap(value -> - Flux.just(encodeValue(value, bufferFactory, elementType), - bufferFactory.wrap(SEPARATOR_BUFFER))); + Mono startArray = Mono.just(bufferFactory.wrap(START_ARRAY_BUFFER)); + Mono endArray = Mono.just(bufferFactory.wrap(END_ARRAY_BUFFER)); - return Flux.concat(Mono.just(bufferFactory.wrap(START_ARRAY_BUFFER)), array.skipLast(1), - Mono.just(bufferFactory.wrap(END_ARRAY_BUFFER))); + Flux array = Flux.from(inputStream) + .flatMap(value -> { + DataBuffer arraySeparator = bufferFactory.wrap(SEPARATOR_BUFFER); + return Flux.just(encodeValue(value, bufferFactory, elementType), arraySeparator); + }); + + return Flux.concat(startArray, array.skipLast(1), endArray); } private DataBuffer encodeValue(Object value, DataBufferFactory bufferFactory, ResolvableType type) { From 54bfbbc6078f1ae9d56dd7c2371e8161e797d498 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 6 Jul 2016 12:38:05 -0400 Subject: [PATCH 473/498] Rename decodeOne->decodeToMono and readOne->readMono --- .../core/codec/AbstractDecoder.java | 2 +- .../springframework/core/codec/Decoder.java | 2 +- .../core/codec/StringDecoder.java | 2 +- .../http/codec/json/JacksonJsonDecoder.java | 2 +- .../reactive/CodecHttpMessageConverter.java | 4 +- .../reactive/HttpMessageConverter.java | 2 +- .../RequestBodyArgumentResolver.java | 10 ++-- .../core/codec/StringDecoderTests.java | 60 +++++++------------ .../codec/json/JacksonJsonDecoderTests.java | 2 +- 9 files changed, 35 insertions(+), 51 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/AbstractDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/AbstractDecoder.java index 1b7af181f0..7f8fc803a4 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/AbstractDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/AbstractDecoder.java @@ -55,7 +55,7 @@ public abstract class AbstractDecoder implements Decoder { } @Override - public Mono decodeOne(Publisher inputStream, ResolvableType elementType, MimeType mimeType, Object... hints) { + public Mono decodeToMono(Publisher inputStream, ResolvableType elementType, MimeType mimeType, Object... hints) { throw new UnsupportedOperationException(); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/Decoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/Decoder.java index bd5e611d9a..1b029b4c79 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/Decoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/Decoder.java @@ -72,7 +72,7 @@ public interface Decoder { * @param hints additional information about how to do decode, optional * @return the output stream with the decoded element */ - Mono decodeOne(Publisher inputStream, ResolvableType elementType, + Mono decodeToMono(Publisher inputStream, ResolvableType elementType, MimeType mimeType, Object... hints); /** diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/StringDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/StringDecoder.java index 3c293029bd..51dc6a5eb5 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/StringDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/StringDecoder.java @@ -95,7 +95,7 @@ public class StringDecoder extends AbstractDecoder { } @Override - public Mono decodeOne(Publisher inputStream, ResolvableType elementType, + public Mono decodeToMono(Publisher inputStream, ResolvableType elementType, MimeType mimeType, Object... hints) { return decodeInternal(Flux.from(inputStream), mimeType). diff --git a/spring-web-reactive/src/main/java/org/springframework/http/codec/json/JacksonJsonDecoder.java b/spring-web-reactive/src/main/java/org/springframework/http/codec/json/JacksonJsonDecoder.java index 36b84d338b..037e46299e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/codec/json/JacksonJsonDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/codec/json/JacksonJsonDecoder.java @@ -78,7 +78,7 @@ public class JacksonJsonDecoder extends AbstractDecoder { } @Override - public Mono decodeOne(Publisher inputStream, ResolvableType elementType, + public Mono decodeToMono(Publisher inputStream, ResolvableType elementType, MimeType mimeType, Object... hints) { JsonObjectDecoder objectDecoder = this.monoObjectDecoder; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/CodecHttpMessageConverter.java b/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/CodecHttpMessageConverter.java index 60e4b3e3ea..b88156eaec 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/CodecHttpMessageConverter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/CodecHttpMessageConverter.java @@ -123,12 +123,12 @@ public class CodecHttpMessageConverter implements HttpMessageConverter { } @Override - public Mono readOne(ResolvableType type, ReactiveHttpInputMessage inputMessage) { + public Mono readMono(ResolvableType type, ReactiveHttpInputMessage inputMessage) { if (this.decoder == null) { return Mono.error(new IllegalStateException("No decoder set")); } MediaType contentType = getContentType(inputMessage); - return this.decoder.decodeOne(inputMessage.getBody(), type, contentType); + return this.decoder.decodeToMono(inputMessage.getBody(), type, contentType); } private MediaType getContentType(ReactiveHttpInputMessage inputMessage) { diff --git a/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/HttpMessageConverter.java b/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/HttpMessageConverter.java index 0f663457fa..d79009faef 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/HttpMessageConverter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/converter/reactive/HttpMessageConverter.java @@ -69,7 +69,7 @@ public interface HttpMessageConverter { * @param inputMessage the HTTP input message to read from * @return the converted {@link Mono} of object */ - Mono readOne(ResolvableType type, ReactiveHttpInputMessage inputMessage); + Mono readMono(ResolvableType type, ReactiveHttpInputMessage inputMessage); /** * Indicates whether the given class can be written by this converter. diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java index 52ece42bbc..9113e3f24e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java @@ -151,14 +151,16 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve return Mono.just(getConversionService().convert(flux, FLUX_TYPE, typeDescriptor)); } else { - Mono mono = converter.readOne(elementType, request); + Mono mono = converter.readMono(elementType, request); if (this.validator != null) { mono = mono.map(applyValidationIfApplicable(parameter)); } - if (!convertFromMono) { - return mono.map(value-> value); // TODO: MonoToObjectConverter + if (convertFromMono) { + return Mono.just(getConversionService().convert(mono, MONO_TYPE, typeDescriptor)); + } + else { + return Mono.from(mono); } - return Mono.just(getConversionService().convert(mono, MONO_TYPE, typeDescriptor)); } } } diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/StringDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/StringDecoderTests.java index c6624efdeb..f3d4bb43b0 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/StringDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/StringDecoderTests.java @@ -16,7 +16,6 @@ package org.springframework.core.codec; -import org.junit.Before; import org.junit.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -37,37 +36,25 @@ import static org.junit.Assert.assertTrue; */ public class StringDecoderTests extends AbstractDataBufferAllocatingTestCase { - private StringDecoder decoder; - - @Before - public void createEncoder() { - this.decoder = new StringDecoder(); - } + private StringDecoder decoder = new StringDecoder(); @Test public void canDecode() { - assertTrue(this.decoder - .canDecode(ResolvableType.forClass(String.class), MediaType.TEXT_PLAIN)); - assertTrue(this.decoder - .canDecode(ResolvableType.forClass(String.class), MediaType.TEXT_HTML)); - assertTrue(this.decoder.canDecode(ResolvableType.forClass(String.class), - MediaType.APPLICATION_JSON)); - assertFalse(this.decoder - .canDecode(ResolvableType.forClass(Integer.class), MediaType.TEXT_PLAIN)); - assertFalse(this.decoder.canDecode(ResolvableType.forClass(Object.class), - MediaType.APPLICATION_JSON)); + assertTrue(this.decoder.canDecode(ResolvableType.forClass(String.class), MediaType.TEXT_PLAIN)); + assertTrue(this.decoder.canDecode(ResolvableType.forClass(String.class), MediaType.TEXT_HTML)); + assertTrue(this.decoder.canDecode(ResolvableType.forClass(String.class), MediaType.APPLICATION_JSON)); + assertFalse(this.decoder.canDecode(ResolvableType.forClass(Integer.class), MediaType.TEXT_PLAIN)); + assertFalse(this.decoder.canDecode(ResolvableType.forClass(Object.class), MediaType.APPLICATION_JSON)); } @Test public void decode() throws InterruptedException { this.decoder = new StringDecoder(false); - Flux source = - Flux.just(stringBuffer("foo"), stringBuffer("bar"), stringBuffer("baz")); - Flux output = - this.decoder.decode(source, ResolvableType.forClass(String.class), null); - TestSubscriber - .subscribe(output) + Flux source = Flux.just(stringBuffer("foo"), stringBuffer("bar"), stringBuffer("baz")); + Flux output = this.decoder.decode(source, ResolvableType.forClass(String.class), null); + + TestSubscriber.subscribe(output) .assertNoError() .assertComplete() .assertValues("foo", "bar", "baz"); @@ -78,10 +65,9 @@ public class StringDecoderTests extends AbstractDataBufferAllocatingTestCase { DataBuffer fooBar = stringBuffer("\nfoo\r\nbar\r"); DataBuffer baz = stringBuffer("\nbaz"); Flux source = Flux.just(fooBar, baz); - Flux output = - decoder.decode(source, ResolvableType.forClass(String.class), null); - TestSubscriber - .subscribe(output) + Flux output = decoder.decode(source, ResolvableType.forClass(String.class), null); + + TestSubscriber.subscribe(output) .assertNoError() .assertComplete().assertValues("\n", "foo\r", "\n", "bar\r", "\n", "baz"); } @@ -89,22 +75,18 @@ public class StringDecoderTests extends AbstractDataBufferAllocatingTestCase { @Test public void decodeEmpty() throws InterruptedException { Flux source = Flux.just(stringBuffer("")); - Flux output = - this.decoder.decode(source, ResolvableType.forClass(String.class), null); - TestSubscriber - .subscribe(output) - .assertValues(""); + Flux output = this.decoder.decode(source, ResolvableType.forClass(String.class), null); + + TestSubscriber.subscribe(output).assertValues(""); } @Test - public void decodeOne() throws InterruptedException { + public void decodeToMono() throws InterruptedException { this.decoder = new StringDecoder(false); - Flux source = - Flux.just(stringBuffer("foo"), stringBuffer("bar"), stringBuffer("baz")); - Mono output = - this.decoder.decodeOne(source, ResolvableType.forClass(String.class), null); - TestSubscriber - .subscribe(output) + Flux source = Flux.just(stringBuffer("foo"), stringBuffer("bar"), stringBuffer("baz")); + Mono output = this.decoder.decodeToMono(source, ResolvableType.forClass(String.class), null); + + TestSubscriber.subscribe(output) .assertNoError() .assertComplete() .assertValues("foobarbaz"); diff --git a/spring-web-reactive/src/test/java/org/springframework/http/codec/json/JacksonJsonDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/http/codec/json/JacksonJsonDecoderTests.java index 0321bd2347..99aacd522c 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/codec/json/JacksonJsonDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/codec/json/JacksonJsonDecoderTests.java @@ -66,7 +66,7 @@ public class JacksonJsonDecoderTests extends AbstractDataBufferAllocatingTestCas Method method = getClass().getDeclaredMethod("handle", List.class); ResolvableType elementType = ResolvableType.forMethodParameter(method, 0); - Mono mono = new JacksonJsonDecoder().decodeOne(source, elementType, null); + Mono mono = new JacksonJsonDecoder().decodeToMono(source, elementType, null); TestSubscriber.subscribe(mono).assertNoError().assertComplete(). assertValues(Arrays.asList(new Pojo("f1", "b1"), new Pojo("f2", "b2"))); From 9cc01fc185a70f56acd5adb5355ae09a44d614a7 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 6 Jul 2016 14:10:51 -0400 Subject: [PATCH 474/498] Add @RequestBody tests --- .../RequestBodyArgumentResolver.java | 6 +- .../RequestBodyArgumentResolverTests.java | 82 ++++++++++++++++++- 2 files changed, 83 insertions(+), 5 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java index 9113e3f24e..d99240b436 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java @@ -124,15 +124,13 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve } @Override - public Mono resolveArgument(MethodParameter parameter, ModelMap model, - ServerWebExchange exchange) { - - ResolvableType type = ResolvableType.forMethodParameter(parameter); + public Mono resolveArgument(MethodParameter parameter, ModelMap model, ServerWebExchange exchange) { TypeDescriptor typeDescriptor = new TypeDescriptor(parameter); boolean convertFromMono = getConversionService().canConvert(MONO_TYPE, typeDescriptor); boolean convertFromFlux = getConversionService().canConvert(FLUX_TYPE, typeDescriptor); + ResolvableType type = ResolvableType.forMethodParameter(parameter); ResolvableType elementType = convertFromMono || convertFromFlux ? type.getGeneric(0) : type; ServerHttpRequest request = exchange.getRequest(); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java index 0c8e36d6f8..471f11dbd8 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java @@ -15,6 +15,8 @@ */ package org.springframework.web.reactive.result.method.annotation; +import java.io.Serializable; +import java.lang.reflect.Method; import java.net.URI; import java.nio.ByteBuffer; import java.nio.charset.Charset; @@ -29,6 +31,7 @@ import java.util.concurrent.CompletableFuture; import javax.xml.bind.annotation.XmlRootElement; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -120,6 +123,17 @@ public class RequestBodyArgumentResolverTests { .assertError(UnsupportedMediaTypeStatusException.class); } + @Test // SPR-9942 + public void missingContent() throws Exception { + this.request.writeWith(Flux.empty()); + ResolvableType type = forClassWithGenerics(Mono.class, TestBean.class); + MethodParameter param = this.testMethod.resolveParam(type); + Mono result = this.resolver.resolveArgument(param, new ExtendedModelMap(), this.exchange); + + TestSubscriber.subscribe(result) + .assertError(UnsupportedMediaTypeStatusException.class); + } + @Test @SuppressWarnings("unchecked") public void monoTestBean() throws Exception { String body = "{\"bar\":\"b1\",\"foo\":\"f1\"}"; @@ -205,6 +219,17 @@ public class RequestBodyArgumentResolverTests { assertEquals(Arrays.asList(new TestBean("f1", "b1"), new TestBean("f2", "b2")), list); } + @Test + public void monoList() throws Exception { + String body = "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]"; + ResolvableType type = forClassWithGenerics(Mono.class, forClassWithGenerics(List.class, TestBean.class)); + MethodParameter param = this.testMethod.resolveParam(type); + Mono mono = resolveValue(param, Mono.class, body); + + List list = (List) mono.block(Duration.ofSeconds(5)); + assertEquals(Arrays.asList(new TestBean("f1", "b1"), new TestBean("f2", "b2")), list); + } + @Test public void array() throws Exception { String body = "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]"; @@ -239,6 +264,17 @@ public class RequestBodyArgumentResolverTests { .assertError(ServerWebInputException.class); } + @Test // SPR-9964 + @Ignore + public void parameterizedMethodArgument() throws Exception { + Class clazz = ConcreteParameterizedController.class; + MethodParameter param = ResolvableMethod.on(clazz).name("handleDto").resolveParam(); + SimpleBean simpleBean = resolveValue(param, SimpleBean.class, "{\"name\" : \"Jad\"}"); + + assertEquals("Jad", simpleBean.getName()); + } + + @SuppressWarnings("unchecked") private T resolveValue(MethodParameter param, Class valueType, String body) { @@ -250,7 +286,7 @@ public class RequestBodyArgumentResolverTests { Object value = result.block(Duration.ofSeconds(5)); assertNotNull(value); - assertTrue("Actual type: " + value.getClass(), valueType.isAssignableFrom(value.getClass())); + assertTrue("Unexpected return value type: " + value, valueType.isAssignableFrom(value.getClass())); return (T) value; } @@ -285,6 +321,7 @@ public class RequestBodyArgumentResolverTests { @RequestBody TestBean testBean, @RequestBody Map map, @RequestBody List list, + @RequestBody Mono> monoList, @RequestBody Set set, @RequestBody TestBean[] array, TestBean paramWithoutAnnotation) { @@ -361,4 +398,47 @@ public class RequestBodyArgumentResolverTests { } } } + + private static abstract class AbstractParameterizedController { + + @SuppressWarnings("unused") + public void handleDto(@RequestBody DTO dto) {} + } + + private static class ConcreteParameterizedController extends AbstractParameterizedController { + } + + private interface Identifiable extends Serializable { + + Long getId(); + + void setId(Long id); + } + + @SuppressWarnings({ "serial" }) + private static class SimpleBean implements Identifiable { + + private Long id; + + private String name; + + @Override + public Long getId() { + return id; + } + + @Override + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + } From 4c3c54463c3ce17f153e2fdce7c2cff79b7bdf2f Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Thu, 7 Jul 2016 18:11:04 +0200 Subject: [PATCH 475/498] Upgrade build to Spring 5.0.0-BUILD-SNAPSHOT * update bean validation API version * MethodParameter now validates indices, see 39e3f2ebf669 * `getCharSet()` is now `getCharset()` --- spring-web-reactive/build.gradle | 4 ++-- .../http/server/reactive/ServletServerHttpRequest.java | 2 +- .../http/server/reactive/ServletServerHttpResponse.java | 2 +- .../RequestAttributeMethodArgumentResolverTests.java | 3 ++- .../SessionAttributeMethodArgumentResolverTests.java | 3 ++- 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index 1ddf309863..32571e2e98 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -27,7 +27,7 @@ repositories { } ext { - springVersion = '4.3.0.RELEASE' + springVersion = '5.0.0.BUILD-SNAPSHOT' reactorVersion = '2.5.0.BUILD-SNAPSHOT' reactorNettyVersion = '2.5.0.BUILD-SNAPSHOT' rxJavaVersion = '1.1.6' @@ -112,7 +112,7 @@ dependencies { optional "org.eclipse.jetty:jetty-servlet:${jettyVersion}" optional("org.freemarker:freemarker:2.3.23") optional("com.fasterxml:aalto-xml:1.0.0") - optional("javax.validation:validation-api:1.0.0.GA") + optional("javax.validation:validation-api:1.1.0.Final") provided "javax.servlet:javax.servlet-api:3.1.0" diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java index 9cd8586e7d..568942bde9 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java @@ -94,7 +94,7 @@ public class ServletServerHttpRequest extends AbstractServerHttpRequest { headers.setContentType(contentType); } } - if (contentType != null && contentType.getCharSet() == null) { + if (contentType != null && contentType.getCharset() == null) { String encoding = getServletRequest().getCharacterEncoding(); if (StringUtils.hasLength(encoding)) { Charset charset = Charset.forName(encoding); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java index 9b84f5082e..7cdad1512a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java @@ -86,7 +86,7 @@ public class ServletServerHttpResponse extends AbstractServerHttpResponse { if (this.response.getContentType() == null && contentType != null) { this.response.setContentType(contentType.toString()); } - Charset charset = (contentType != null ? contentType.getCharSet() : null); + Charset charset = (contentType != null ? contentType.getCharset() : null); if (this.response.getCharacterEncoding() == null && charset != null) { this.response.setCharacterEncoding(charset.name()); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestAttributeMethodArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestAttributeMethodArgumentResolverTests.java index 2fd3e49103..2a88c4fbe8 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestAttributeMethodArgumentResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestAttributeMethodArgumentResolverTests.java @@ -154,7 +154,8 @@ public class RequestAttributeMethodArgumentResolverTests { @RequestAttribute Foo foo, @RequestAttribute("specialFoo") Foo namedFoo, @RequestAttribute(name="foo", required = false) Foo notRequiredFoo, - @RequestAttribute(name="foo") Optional optionalFoo) { + @RequestAttribute(name="foo") Optional optionalFoo, + String notSupported) { } private static class Foo { diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SessionAttributeMethodArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SessionAttributeMethodArgumentResolverTests.java index 008352b278..fe3e3f1b7a 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SessionAttributeMethodArgumentResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SessionAttributeMethodArgumentResolverTests.java @@ -161,7 +161,8 @@ public class SessionAttributeMethodArgumentResolverTests { @SessionAttribute Foo foo, @SessionAttribute("specialFoo") Foo namedFoo, @SessionAttribute(name="foo", required = false) Foo notRequiredFoo, - @SessionAttribute(name="foo") Optional optionalFoo) { + @SessionAttribute(name="foo") Optional optionalFoo, + String notSupported) { } private static class Foo { From 1e1e2f8b1ba4c8c9e734b2000788eb964c1c8ae7 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 7 Jul 2016 04:16:04 -0400 Subject: [PATCH 476/498] Support HttpEntity method arguments The RequestBodyArgumentResolver has been refactored to have a shared base class and tests with the new HttpEntityMethodArgumentResolver. An HttpEntity argument is not expected to have an async wrapper because the request headers are available immediately. The body however can be asynchronous, e.g. HttpEntity>. --- ...tractMessageConverterArgumentResolver.java | 185 ++++++++ .../HttpEntityArgumentResolver.java | 107 +++++ .../RequestBodyArgumentResolver.java | 141 +----- .../HttpEntityArgumentResolverTests.java | 233 ++++++++++ ...MessageConverterArgumentResolverTests.java | 423 ++++++++++++++++++ .../RequestBodyArgumentResolverTests.java | 393 +--------------- .../ResponseBodyResultHandlerTests.java | 4 +- .../ResponseEntityResultHandlerTests.java | 12 +- 8 files changed, 980 insertions(+), 518 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageConverterArgumentResolver.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/HttpEntityArgumentResolver.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/HttpEntityArgumentResolverTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterArgumentResolverTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageConverterArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageConverterArgumentResolver.java new file mode 100644 index 0000000000..44f462d658 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageConverterArgumentResolver.java @@ -0,0 +1,185 @@ +/* + * 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.web.reactive.result.method.annotation; + +import java.lang.annotation.Annotation; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.core.Conventions; +import org.springframework.core.MethodParameter; +import org.springframework.core.ResolvableType; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.http.MediaType; +import org.springframework.http.converter.reactive.HttpMessageConverter; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.validation.BeanPropertyBindingResult; +import org.springframework.validation.Errors; +import org.springframework.validation.SmartValidator; +import org.springframework.validation.Validator; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.ServerWebInputException; +import org.springframework.web.server.UnsupportedMediaTypeStatusException; + +/** + * Abstract base class for argument resolvers that resolve method arguments + * by reading the request body with an {@link HttpMessageConverter}. + * + *

    Applies validation if the method argument is annotated with + * {@code @javax.validation.Valid} or + * {@link org.springframework.validation.annotation.Validated}. Validation + * failure results in an {@link ServerWebInputException}. + * + * @author Rossen Stoyanchev + */ +public abstract class AbstractMessageConverterArgumentResolver { + + private static final TypeDescriptor MONO_TYPE = TypeDescriptor.valueOf(Mono.class); + + private static final TypeDescriptor FLUX_TYPE = TypeDescriptor.valueOf(Flux.class); + + + private final List> messageConverters; + + private final ConversionService conversionService; + + private final Validator validator; + + private final List supportedMediaTypes; + + + /** + * Constructor with message converters and a ConversionService. + * @param converters converters for reading the request body with + * @param service for converting to other reactive types from Flux and Mono + * @param validator validator to validate decoded objects with + */ + protected AbstractMessageConverterArgumentResolver(List> converters, + ConversionService service, Validator validator) { + + Assert.notEmpty(converters, "At least one message converter is required."); + Assert.notNull(service, "'conversionService' is required."); + this.messageConverters = converters; + this.conversionService = service; + this.validator = validator; + this.supportedMediaTypes = converters.stream() + .flatMap(converter -> converter.getReadableMediaTypes().stream()) + .collect(Collectors.toList()); + } + + + /** + * Return the configured message converters. + */ + public List> getMessageConverters() { + return this.messageConverters; + } + + /** + * Return the configured {@link ConversionService}. + */ + public ConversionService getConversionService() { + return this.conversionService; + } + + + protected Mono readBody(MethodParameter bodyParameter, ServerWebExchange exchange) { + + TypeDescriptor typeDescriptor = new TypeDescriptor(bodyParameter); + boolean convertFromMono = getConversionService().canConvert(MONO_TYPE, typeDescriptor); + boolean convertFromFlux = getConversionService().canConvert(FLUX_TYPE, typeDescriptor); + + ResolvableType elementType = ResolvableType.forMethodParameter(bodyParameter); + if (convertFromMono || convertFromFlux) { + elementType = elementType.getGeneric(0); + } + + ServerHttpRequest request = exchange.getRequest(); + MediaType mediaType = request.getHeaders().getContentType(); + if (mediaType == null) { + mediaType = MediaType.APPLICATION_OCTET_STREAM; + } + + for (HttpMessageConverter converter : getMessageConverters()) { + if (converter.canRead(elementType, mediaType)) { + if (convertFromFlux) { + Flux flux = converter.read(elementType, request); + if (this.validator != null) { + flux = flux.map(applyValidationIfApplicable(bodyParameter)); + } + return Mono.just(getConversionService().convert(flux, FLUX_TYPE, typeDescriptor)); + } + else { + Mono mono = converter.readMono(elementType, request); + if (this.validator != null) { + mono = mono.map(applyValidationIfApplicable(bodyParameter)); + } + if (convertFromMono) { + return Mono.just(getConversionService().convert(mono, MONO_TYPE, typeDescriptor)); + } + else { + return Mono.from(mono); + } + } + } + } + + return Mono.error(new UnsupportedMediaTypeStatusException(mediaType, this.supportedMediaTypes)); + } + + protected Function applyValidationIfApplicable(MethodParameter methodParam) { + Annotation[] annotations = methodParam.getParameterAnnotations(); + for (Annotation ann : annotations) { + Validated validAnnot = AnnotationUtils.getAnnotation(ann, Validated.class); + if (validAnnot != null || ann.annotationType().getSimpleName().startsWith("Valid")) { + Object hints = (validAnnot != null ? validAnnot.value() : AnnotationUtils.getValue(ann)); + Object[] validHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); + return element -> { + doValidate(element, validHints, methodParam); + return element; + }; + } + } + return element -> element; + } + + /** + * TODO: replace with use of DataBinder + */ + private void doValidate(Object target, Object[] validationHints, MethodParameter methodParam) { + String name = Conventions.getVariableNameForParameter(methodParam); + Errors errors = new BeanPropertyBindingResult(target, name); + if (!ObjectUtils.isEmpty(validationHints) && this.validator instanceof SmartValidator) { + ((SmartValidator) this.validator).validate(target, errors, validationHints); + } + else if (this.validator != null) { + this.validator.validate(target, errors); + } + if (errors.hasErrors()) { + throw new ServerWebInputException("Validation failed", methodParam); + } + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/HttpEntityArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/HttpEntityArgumentResolver.java new file mode 100644 index 0000000000..b2bb395e1d --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/HttpEntityArgumentResolver.java @@ -0,0 +1,107 @@ +/* + * 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.web.reactive.result.method.annotation; + +import java.util.List; + +import reactor.core.publisher.Mono; + +import org.springframework.core.MethodParameter; +import org.springframework.core.ResolvableType; +import org.springframework.core.convert.ConversionService; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.RequestEntity; +import org.springframework.http.converter.reactive.HttpMessageConverter; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.ui.ModelMap; +import org.springframework.validation.Validator; +import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver; +import org.springframework.web.server.ServerWebExchange; + +/** + * Resolves method arguments of type {@link HttpEntity} or {@link RequestEntity} + * by reading the body of the request through a compatible + * {@code HttpMessageConverter}. + * + * @author Rossen Stoyanchev + */ +public class HttpEntityArgumentResolver extends AbstractMessageConverterArgumentResolver + implements HandlerMethodArgumentResolver { + + + /** + * Constructor with message converters and a ConversionService. + * @param converters converters for reading the request body with + * @param service for converting to other reactive types from Flux and Mono + */ + public HttpEntityArgumentResolver(List> converters, + ConversionService service) { + + this(converters, service, null); + } + + /** + * Constructor with message converters and a ConversionService. + * @param converters converters for reading the request body with + * @param service for converting to other reactive types from Flux and Mono + * @param validator validator to validate decoded objects with + */ + public HttpEntityArgumentResolver(List> converters, + ConversionService service, Validator validator) { + + super(converters, service, validator); + } + + + @Override + public boolean supportsParameter(MethodParameter parameter) { + Class clazz = parameter.getParameterType(); + return (HttpEntity.class.equals(clazz) || RequestEntity.class.equals(clazz)); + } + + @Override + public Mono resolveArgument(MethodParameter param, ModelMap model, ServerWebExchange exchange) { + + ResolvableType entityType; + MethodParameter bodyParameter; + + if (getConversionService().canConvert(Mono.class, param.getParameterType())) { + entityType = ResolvableType.forMethodParameter(param).getGeneric(0); + bodyParameter = new MethodParameter(param); + bodyParameter.increaseNestingLevel(); + bodyParameter.increaseNestingLevel(); + } + else { + entityType = ResolvableType.forMethodParameter(param); + bodyParameter = new MethodParameter(param); + bodyParameter.increaseNestingLevel(); + } + + return readBody(bodyParameter, exchange) + .map(body -> { + ServerHttpRequest request = exchange.getRequest(); + HttpHeaders headers = request.getHeaders(); + if (RequestEntity.class == entityType.getRawClass()) { + return new RequestEntity<>(body, headers, request.getMethod(), request.getURI()); + } + else { + return new HttpEntity<>(body, headers); + } + }); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java index d99240b436..8d8e7f01ac 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java @@ -16,60 +16,36 @@ package org.springframework.web.reactive.result.method.annotation; -import java.lang.annotation.Annotation; import java.util.List; -import java.util.function.Function; -import java.util.stream.Collectors; -import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import org.springframework.core.Conventions; import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.convert.ConversionService; -import org.springframework.core.convert.TypeDescriptor; -import org.springframework.http.MediaType; import org.springframework.http.converter.reactive.HttpMessageConverter; -import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.ui.ModelMap; -import org.springframework.util.Assert; -import org.springframework.util.ObjectUtils; -import org.springframework.validation.BeanPropertyBindingResult; -import org.springframework.validation.Errors; -import org.springframework.validation.SmartValidator; import org.springframework.validation.Validator; -import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebInputException; -import org.springframework.web.server.UnsupportedMediaTypeStatusException; /** - * Resolves method arguments annotated with {@code @RequestBody} by reading and - * decoding the body of the request through a compatible - * {@code HttpMessageConverter}. + * Resolves method arguments annotated with {@code @RequestBody} by reading the + * body of the request through a compatible {@code HttpMessageConverter}. + * + *

    An {@code @RequestBody} method argument is also validated if it is + * annotated with {@code @javax.validation.Valid} or + * {@link org.springframework.validation.annotation.Validated}. Validation + * failure results in an {@link ServerWebInputException}. * * @author Sebastien Deleuze * @author Stephane Maldini * @author Rossen Stoyanchev */ -public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolver { - - private static final TypeDescriptor MONO_TYPE = TypeDescriptor.valueOf(Mono.class); - - private static final TypeDescriptor FLUX_TYPE = TypeDescriptor.valueOf(Flux.class); - - - private final List> messageConverters; - - private final ConversionService conversionService; - - private final Validator validator; - - private final List supportedMediaTypes; +public class RequestBodyArgumentResolver extends AbstractMessageConverterArgumentResolver + implements HandlerMethodArgumentResolver { /** @@ -92,29 +68,7 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve public RequestBodyArgumentResolver(List> converters, ConversionService service, Validator validator) { - Assert.notEmpty(converters, "At least one message converter is required."); - Assert.notNull(service, "'conversionService' is required."); - this.messageConverters = converters; - this.conversionService = service; - this.validator = validator; - this.supportedMediaTypes = converters.stream() - .flatMap(converter -> converter.getReadableMediaTypes().stream()) - .collect(Collectors.toList()); - } - - - /** - * Return the configured message converters. - */ - public List> getMessageConverters() { - return this.messageConverters; - } - - /** - * Return the configured {@link ConversionService}. - */ - public ConversionService getConversionService() { - return this.conversionService; + super(converters, service, validator); } @@ -124,79 +78,8 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve } @Override - public Mono resolveArgument(MethodParameter parameter, ModelMap model, ServerWebExchange exchange) { - - TypeDescriptor typeDescriptor = new TypeDescriptor(parameter); - boolean convertFromMono = getConversionService().canConvert(MONO_TYPE, typeDescriptor); - boolean convertFromFlux = getConversionService().canConvert(FLUX_TYPE, typeDescriptor); - - ResolvableType type = ResolvableType.forMethodParameter(parameter); - ResolvableType elementType = convertFromMono || convertFromFlux ? type.getGeneric(0) : type; - - ServerHttpRequest request = exchange.getRequest(); - MediaType mediaType = request.getHeaders().getContentType(); - if (mediaType == null) { - mediaType = MediaType.APPLICATION_OCTET_STREAM; - } - - for (HttpMessageConverter converter : getMessageConverters()) { - if (converter.canRead(elementType, mediaType)) { - if (convertFromFlux) { - Flux flux = converter.read(elementType, request); - if (this.validator != null) { - flux = flux.map(applyValidationIfApplicable(parameter)); - } - return Mono.just(getConversionService().convert(flux, FLUX_TYPE, typeDescriptor)); - } - else { - Mono mono = converter.readMono(elementType, request); - if (this.validator != null) { - mono = mono.map(applyValidationIfApplicable(parameter)); - } - if (convertFromMono) { - return Mono.just(getConversionService().convert(mono, MONO_TYPE, typeDescriptor)); - } - else { - return Mono.from(mono); - } - } - } - } - - return Mono.error(new UnsupportedMediaTypeStatusException(mediaType, this.supportedMediaTypes)); - } - - protected Function applyValidationIfApplicable(MethodParameter methodParam) { - Annotation[] annotations = methodParam.getParameterAnnotations(); - for (Annotation ann : annotations) { - Validated validAnnot = AnnotationUtils.getAnnotation(ann, Validated.class); - if (validAnnot != null || ann.annotationType().getSimpleName().startsWith("Valid")) { - Object hints = (validAnnot != null ? validAnnot.value() : AnnotationUtils.getValue(ann)); - Object[] validHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); - return element -> { - doValidate(element, validHints, methodParam); - return element; - }; - } - } - return element -> element; - } - - /** - * TODO: replace with use of DataBinder - */ - private void doValidate(Object target, Object[] validationHints, MethodParameter methodParam) { - String name = Conventions.getVariableNameForParameter(methodParam); - Errors errors = new BeanPropertyBindingResult(target, name); - if (!ObjectUtils.isEmpty(validationHints) && this.validator instanceof SmartValidator) { - ((SmartValidator) this.validator).validate(target, errors, validationHints); - } - else if (this.validator != null) { - this.validator.validate(target, errors); - } - if (errors.hasErrors()) { - throw new ServerWebInputException("Validation failed", methodParam); - } + public Mono resolveArgument(MethodParameter param, ModelMap model, ServerWebExchange exchange) { + return readBody(param, exchange); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/HttpEntityArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/HttpEntityArgumentResolverTests.java new file mode 100644 index 0000000000..f42dce42ee --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/HttpEntityArgumentResolverTests.java @@ -0,0 +1,233 @@ +/* + * 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.web.reactive.result.method.annotation; + +import java.net.URI; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import org.junit.Before; +import org.junit.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.test.TestSubscriber; +import rx.Observable; +import rx.Single; + +import org.springframework.core.MethodParameter; +import org.springframework.core.ResolvableType; +import org.springframework.core.codec.StringDecoder; +import org.springframework.core.convert.support.MonoToCompletableFutureConverter; +import org.springframework.core.convert.support.ReactorToRxJava1Converter; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; +import org.springframework.format.support.DefaultFormattingConversionService; +import org.springframework.format.support.FormattingConversionService; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.RequestEntity; +import org.springframework.http.converter.reactive.CodecHttpMessageConverter; +import org.springframework.http.converter.reactive.HttpMessageConverter; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.ui.ExtendedModelMap; +import org.springframework.web.reactive.result.ResolvableMethod; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.MockWebSessionManager; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.springframework.core.ResolvableType.forClassWithGenerics; + +/** + * Unit tests for {@link HttpEntityArgumentResolver}.When adding a test also + * consider whether the logic under test is in a parent class, then see: + * {@link MessageConverterArgumentResolverTests}. + * + * @author Rossen Stoyanchev + */ +public class HttpEntityArgumentResolverTests { + + private HttpEntityArgumentResolver resolver = resolver(); + + private ServerWebExchange exchange; + + private MockServerHttpRequest request; + + private ResolvableMethod testMethod = ResolvableMethod.on(getClass()).name("handle"); + + + @Before + public void setUp() throws Exception { + this.request = new MockServerHttpRequest(HttpMethod.POST, new URI("/path")); + MockServerHttpResponse response = new MockServerHttpResponse(); + this.exchange = new DefaultServerWebExchange(this.request, response, new MockWebSessionManager()); + } + + + @Test + public void supports() throws Exception { + testSupports(httpEntity(String.class)); + testSupports(httpEntity(forClassWithGenerics(Mono.class, String.class))); + testSupports(httpEntity(forClassWithGenerics(Single.class, String.class))); + testSupports(httpEntity(forClassWithGenerics(CompletableFuture.class, String.class))); + testSupports(httpEntity(forClassWithGenerics(Flux.class, String.class))); + testSupports(httpEntity(forClassWithGenerics(Observable.class, String.class))); + testSupports(forClassWithGenerics(RequestEntity.class, String.class)); + } + + @Test + public void doesNotSupport() throws Exception { + ResolvableType type = ResolvableType.forClassWithGenerics(Mono.class, String.class); + assertFalse(this.resolver.supportsParameter(this.testMethod.resolveParam(type))); + + type = ResolvableType.forClass(String.class); + assertFalse(this.resolver.supportsParameter(this.testMethod.resolveParam(type))); + } + + @Test + public void httpEntityWithStringBody() throws Exception { + String body = "line1"; + ResolvableType type = httpEntity(String.class); + HttpEntity httpEntity = resolveValue(type, body); + + assertEquals(this.request.getHeaders(), httpEntity.getHeaders()); + assertEquals("line1", httpEntity.getBody()); + } + + @Test + public void httpEntityWithMonoBody() throws Exception { + String body = "line1"; + ResolvableType type = httpEntity(forClassWithGenerics(Mono.class, String.class)); + HttpEntity> httpEntity = resolveValue(type, body); + + assertEquals(this.request.getHeaders(), httpEntity.getHeaders()); + assertEquals("line1", httpEntity.getBody().block()); + } + + @Test + public void httpEntityWithSingleBody() throws Exception { + String body = "line1"; + ResolvableType type = httpEntity(forClassWithGenerics(Single.class, String.class)); + HttpEntity> httpEntity = resolveValue(type, body); + + assertEquals(this.request.getHeaders(), httpEntity.getHeaders()); + assertEquals("line1", httpEntity.getBody().toBlocking().value()); + } + + @Test + public void httpEntityWithCompletableFutureBody() throws Exception { + String body = "line1"; + ResolvableType type = httpEntity(forClassWithGenerics(CompletableFuture.class, String.class)); + HttpEntity> httpEntity = resolveValue(type, body); + + assertEquals(this.request.getHeaders(), httpEntity.getHeaders()); + assertEquals("line1", httpEntity.getBody().get()); + } + + @Test + public void httpEntityWithFluxBody() throws Exception { + String body = "line1\nline2\nline3\n"; + ResolvableType type = httpEntity(forClassWithGenerics(Flux.class, String.class)); + HttpEntity> httpEntity = resolveValue(type, body); + + assertEquals(this.request.getHeaders(), httpEntity.getHeaders()); + TestSubscriber.subscribe(httpEntity.getBody()).assertValues("line1\n", "line2\n", "line3\n"); + } + + @Test + public void requestEntity() throws Exception { + String body = "line1"; + ResolvableType type = forClassWithGenerics(RequestEntity.class, String.class); + RequestEntity requestEntity = resolveValue(type, body); + + assertEquals(this.request.getMethod(), requestEntity.getMethod()); + assertEquals(this.request.getURI(), requestEntity.getUrl()); + assertEquals(this.request.getHeaders(), requestEntity.getHeaders()); + assertEquals("line1", requestEntity.getBody()); + } + + + private ResolvableType httpEntity(Class bodyType) { + return httpEntity(ResolvableType.forClass(bodyType)); + } + + private ResolvableType httpEntity(ResolvableType type) { + return forClassWithGenerics(HttpEntity.class, type); + } + + private HttpEntityArgumentResolver resolver() { + List> converters = new ArrayList<>(); + converters.add(new CodecHttpMessageConverter<>(new StringDecoder())); + + FormattingConversionService service = new DefaultFormattingConversionService(); + service.addConverter(new MonoToCompletableFutureConverter()); + service.addConverter(new ReactorToRxJava1Converter()); + + return new HttpEntityArgumentResolver(converters, service); + } + + private void testSupports(ResolvableType type) { + MethodParameter parameter = this.testMethod.resolveParam(type); + assertTrue(this.resolver.supportsParameter(parameter)); + } + + @SuppressWarnings("unchecked") + private T resolveValue(ResolvableType type, String body) { + + this.request.getHeaders().add("foo", "bar"); + this.request.getHeaders().setContentType(MediaType.TEXT_PLAIN); + this.request.writeWith(Flux.just(dataBuffer(body))); + + MethodParameter param = this.testMethod.resolveParam(type); + Mono result = this.resolver.resolveArgument(param, new ExtendedModelMap(), this.exchange); + Object value = result.block(Duration.ofSeconds(5)); + + assertNotNull(value); + assertTrue("Unexpected return value type: " + value.getClass(), + param.getParameterType().isAssignableFrom(value.getClass())); + + return (T) value; + } + + private DataBuffer dataBuffer(String body) { + byte[] bytes = body.getBytes(Charset.forName("UTF-8")); + ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); + return new DefaultDataBufferFactory().wrap(byteBuffer); + } + + + @SuppressWarnings("unused") + void handle( + String string, + Mono monoString, + HttpEntity httpEntity, + HttpEntity> monoBody, + HttpEntity> singleBody, + HttpEntity> completableFutureBody, + HttpEntity> fluxBody, + HttpEntity> observableBody, + RequestEntity requestEntity) {} + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterArgumentResolverTests.java new file mode 100644 index 0000000000..ea6d7cd81c --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterArgumentResolverTests.java @@ -0,0 +1,423 @@ +/* + * 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.web.reactive.result.method.annotation; + +import java.io.Serializable; +import java.net.URI; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import javax.xml.bind.annotation.XmlRootElement; + +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.test.TestSubscriber; +import rx.Observable; +import rx.Single; + +import org.springframework.core.MethodParameter; +import org.springframework.core.ResolvableType; +import org.springframework.core.codec.Decoder; +import org.springframework.core.convert.support.MonoToCompletableFutureConverter; +import org.springframework.core.convert.support.ReactorToRxJava1Converter; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; +import org.springframework.format.support.DefaultFormattingConversionService; +import org.springframework.format.support.FormattingConversionService; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.codec.json.JacksonJsonDecoder; +import org.springframework.http.converter.reactive.CodecHttpMessageConverter; +import org.springframework.http.converter.reactive.HttpMessageConverter; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.validation.Errors; +import org.springframework.validation.Validator; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.reactive.result.ResolvableMethod; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.ServerWebInputException; +import org.springframework.web.server.UnsupportedMediaTypeStatusException; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.MockWebSessionManager; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.springframework.core.ResolvableType.forClass; +import static org.springframework.core.ResolvableType.forClassWithGenerics; + +/** + * Unit tests for {@link AbstractMessageConverterArgumentResolver}. + * @author Rossen Stoyanchev + */ +public class MessageConverterArgumentResolverTests { + + private AbstractMessageConverterArgumentResolver resolver = resolver(new JacksonJsonDecoder()); + + private ServerWebExchange exchange; + + private MockServerHttpRequest request; + + private ResolvableMethod testMethod = ResolvableMethod.on(this.getClass()).name("handle"); + + + @Before + public void setUp() throws Exception { + this.request = new MockServerHttpRequest(HttpMethod.GET, new URI("/path")); + MockServerHttpResponse response = new MockServerHttpResponse(); + this.exchange = new DefaultServerWebExchange(this.request, response, new MockWebSessionManager()); + } + + + @Test + public void missingContentType() throws Exception { + String body = "{\"bar\":\"BARBAR\",\"foo\":\"FOOFOO\"}"; + this.request.writeWith(Flux.just(dataBuffer(body))); + ResolvableType type = forClassWithGenerics(Mono.class, TestBean.class); + MethodParameter param = this.testMethod.resolveParam(type); + Mono result = this.resolver.readBody(param, this.exchange); + + TestSubscriber.subscribe(result) + .assertError(UnsupportedMediaTypeStatusException.class); + } + + @Test // SPR-9942 + public void noContent() throws Exception { + this.request.writeWith(Flux.empty()); + ResolvableType type = forClassWithGenerics(Mono.class, TestBean.class); + MethodParameter param = this.testMethod.resolveParam(type); + Mono result = this.resolver.readBody(param, this.exchange); + + TestSubscriber.subscribe(result).assertError(UnsupportedMediaTypeStatusException.class); + } + + @Test + public void monoTestBean() throws Exception { + String body = "{\"bar\":\"BARBAR\",\"foo\":\"FOOFOO\"}"; + ResolvableType type = forClassWithGenerics(Mono.class, TestBean.class); + MethodParameter param = this.testMethod.resolveParam(type); + Mono mono = resolveValue(param, body); + + assertEquals(new TestBean("FOOFOO", "BARBAR"), mono.block()); + } + + @Test + public void fluxTestBean() throws Exception { + String body = "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]"; + ResolvableType type = forClassWithGenerics(Flux.class, TestBean.class); + MethodParameter param = this.testMethod.resolveParam(type); + Flux flux = resolveValue(param, body); + + assertEquals(Arrays.asList(new TestBean("f1", "b1"), new TestBean("f2", "b2")), + flux.collectList().block()); + } + + @Test + public void singleTestBean() throws Exception { + String body = "{\"bar\":\"b1\",\"foo\":\"f1\"}"; + ResolvableType type = forClassWithGenerics(Single.class, TestBean.class); + MethodParameter param = this.testMethod.resolveParam(type); + Single single = resolveValue(param, body); + + assertEquals(new TestBean("f1", "b1"), single.toBlocking().value()); + } + + @Test + public void observableTestBean() throws Exception { + String body = "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]"; + ResolvableType type = forClassWithGenerics(Observable.class, TestBean.class); + MethodParameter param = this.testMethod.resolveParam(type); + Observable observable = resolveValue(param, body); + + assertEquals(Arrays.asList(new TestBean("f1", "b1"), new TestBean("f2", "b2")), + observable.toList().toBlocking().first()); + } + + @Test + public void futureTestBean() throws Exception { + String body = "{\"bar\":\"b1\",\"foo\":\"f1\"}"; + ResolvableType type = forClassWithGenerics(CompletableFuture.class, TestBean.class); + MethodParameter param = this.testMethod.resolveParam(type); + CompletableFuture future = resolveValue(param, body); + + assertEquals(new TestBean("f1", "b1"), future.get()); + } + + @Test + public void testBean() throws Exception { + String body = "{\"bar\":\"b1\",\"foo\":\"f1\"}"; + MethodParameter param = this.testMethod.resolveParam(forClass(TestBean.class)); + TestBean value = resolveValue(param, body); + + assertEquals(new TestBean("f1", "b1"), value); + } + + @Test + public void map() throws Exception { + String body = "{\"bar\":\"b1\",\"foo\":\"f1\"}"; + Map map = new HashMap<>(); + map.put("foo", "f1"); + map.put("bar", "b1"); + ResolvableType type = forClassWithGenerics(Map.class, String.class, String.class); + MethodParameter param = this.testMethod.resolveParam(type); + Map actual = resolveValue(param, body); + + assertEquals(map, actual); + } + + @Test + public void list() throws Exception { + String body = "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]"; + ResolvableType type = forClassWithGenerics(List.class, TestBean.class); + MethodParameter param = this.testMethod.resolveParam(type); + List list = resolveValue(param, body); + + assertEquals(Arrays.asList(new TestBean("f1", "b1"), new TestBean("f2", "b2")), list); + } + + @Test + public void monoList() throws Exception { + String body = "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]"; + ResolvableType type = forClassWithGenerics(Mono.class, forClassWithGenerics(List.class, TestBean.class)); + MethodParameter param = this.testMethod.resolveParam(type); + Mono mono = resolveValue(param, body); + + List list = (List) mono.block(Duration.ofSeconds(5)); + assertEquals(Arrays.asList(new TestBean("f1", "b1"), new TestBean("f2", "b2")), list); + } + + @Test + public void array() throws Exception { + String body = "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]"; + ResolvableType type = forClass(TestBean[].class); + MethodParameter param = this.testMethod.resolveParam(type); + TestBean[] value = resolveValue(param, body); + + assertArrayEquals(new TestBean[] {new TestBean("f1", "b1"), new TestBean("f2", "b2")}, value); + } + + @Test @SuppressWarnings("unchecked") + public void validateMonoTestBean() throws Exception { + String body = "{\"bar\":\"b1\"}"; + ResolvableType type = forClassWithGenerics(Mono.class, TestBean.class); + MethodParameter param = this.testMethod.resolveParam(type); + Mono mono = resolveValue(param, body); + + TestSubscriber.subscribe(mono) + .assertNoValues() + .assertError(ServerWebInputException.class); + } + + @Test @SuppressWarnings("unchecked") + public void validateFluxTestBean() throws Exception { + String body = "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\"}]"; + ResolvableType type = forClassWithGenerics(Flux.class, TestBean.class); + MethodParameter param = this.testMethod.resolveParam(type); + Flux flux = resolveValue(param, body); + + TestSubscriber.subscribe(flux) + .assertValues(new TestBean("f1", "b1")) + .assertError(ServerWebInputException.class); + } + + @Test // SPR-9964 + @Ignore + public void parameterizedMethodArgument() throws Exception { + Class clazz = ConcreteParameterizedController.class; + MethodParameter param = ResolvableMethod.on(clazz).name("handleDto").resolveParam(); + SimpleBean simpleBean = resolveValue(param, "{\"name\" : \"Jad\"}"); + + assertEquals("Jad", simpleBean.getName()); + } + + + @SuppressWarnings("unchecked") + private T resolveValue(MethodParameter param, String body) { + + this.request.getHeaders().setContentType(MediaType.APPLICATION_JSON); + this.request.writeWith(Flux.just(dataBuffer(body))); + + Mono result = this.resolver.readBody(param, this.exchange); + Object value = result.block(Duration.ofSeconds(5)); + + assertNotNull(value); + assertTrue("Unexpected return value type: " + value, + param.getParameterType().isAssignableFrom(value.getClass())); + + return (T) value; + } + + @SuppressWarnings("Convert2MethodRef") + private AbstractMessageConverterArgumentResolver resolver(Decoder... decoders) { + + List> converters = new ArrayList<>(); + Arrays.asList(decoders).forEach(decoder -> converters.add(new CodecHttpMessageConverter<>(decoder))); + + FormattingConversionService service = new DefaultFormattingConversionService(); + service.addConverter(new MonoToCompletableFutureConverter()); + service.addConverter(new ReactorToRxJava1Converter()); + + return new AbstractMessageConverterArgumentResolver(converters, service, new TestBeanValidator()) {}; + } + + private DataBuffer dataBuffer(String body) { + byte[] bytes = body.getBytes(Charset.forName("UTF-8")); + ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); + return new DefaultDataBufferFactory().wrap(byteBuffer); + } + + + @SuppressWarnings("unused") + private void handle( + @Validated Mono monoTestBean, + @Validated Flux fluxTestBean, + Single singleTestBean, + Observable observableTestBean, + CompletableFuture futureTestBean, + TestBean testBean, + Map map, + List list, + Mono> monoList, + Set set, + TestBean[] array) {} + + + @XmlRootElement + private static class TestBean { + + private String foo; + + private String bar; + + @SuppressWarnings("unused") + public TestBean() { + } + + TestBean(String foo, String bar) { + this.foo = foo; + this.bar = bar; + } + + public String getFoo() { + return this.foo; + } + + public void setFoo(String foo) { + this.foo = foo; + } + + public String getBar() { + return this.bar; + } + + public void setBar(String bar) { + this.bar = bar; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o instanceof TestBean) { + TestBean other = (TestBean) o; + return this.foo.equals(other.foo) && this.bar.equals(other.bar); + } + return false; + } + + @Override + public int hashCode() { + return 31 * foo.hashCode() + bar.hashCode(); + } + + @Override + public String toString() { + return "TestBean[foo='" + this.foo + "\'" + ", bar='" + this.bar + "\']"; + } + } + + private static class TestBeanValidator implements Validator { + + @Override + public boolean supports(Class clazz) { + return clazz.equals(TestBean.class); + } + + @Override + public void validate(Object target, Errors errors) { + TestBean testBean = (TestBean) target; + if (testBean.getFoo() == null) { + errors.rejectValue("foo", "nullValue"); + } + } + } + + private static abstract class AbstractParameterizedController { + + @SuppressWarnings("unused") + public void handleDto(DTO dto) {} + } + + private static class ConcreteParameterizedController extends AbstractParameterizedController { + } + + private interface Identifiable extends Serializable { + + Long getId(); + + void setId(Long id); + } + + @SuppressWarnings({ "serial" }) + private static class SimpleBean implements Identifiable { + + private Long id; + + private String name; + + @Override + public Long getId() { + return id; + } + + @Override + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java index 471f11dbd8..a694d9c98e 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java @@ -15,430 +15,65 @@ */ package org.springframework.web.reactive.result.method.annotation; -import java.io.Serializable; -import java.lang.reflect.Method; -import java.net.URI; -import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.time.Duration; import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.CompletableFuture; -import javax.xml.bind.annotation.XmlRootElement; -import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; -import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import reactor.core.test.TestSubscriber; -import rx.Observable; -import rx.Single; import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; -import org.springframework.core.codec.Decoder; import org.springframework.core.codec.StringDecoder; import org.springframework.core.convert.support.MonoToCompletableFutureConverter; import org.springframework.core.convert.support.ReactorToRxJava1Converter; -import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.format.support.FormattingConversionService; -import org.springframework.http.HttpMethod; -import org.springframework.http.MediaType; -import org.springframework.http.codec.json.JacksonJsonDecoder; import org.springframework.http.converter.reactive.CodecHttpMessageConverter; import org.springframework.http.converter.reactive.HttpMessageConverter; -import org.springframework.http.server.reactive.MockServerHttpRequest; -import org.springframework.http.server.reactive.MockServerHttpResponse; -import org.springframework.ui.ExtendedModelMap; -import org.springframework.validation.Errors; -import org.springframework.validation.Validator; -import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.reactive.result.ResolvableMethod; -import org.springframework.web.server.ServerWebExchange; -import org.springframework.web.server.ServerWebInputException; -import org.springframework.web.server.UnsupportedMediaTypeStatusException; -import org.springframework.web.server.adapter.DefaultServerWebExchange; -import org.springframework.web.server.session.MockWebSessionManager; -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; -import static org.springframework.core.ResolvableType.forClass; import static org.springframework.core.ResolvableType.forClassWithGenerics; /** - * Unit tests for {@link RequestBodyArgumentResolver}. + * Unit tests for {@link RequestBodyArgumentResolver}.When adding a test also + * consider whether the logic under test is in a parent class, then see: + * {@link MessageConverterArgumentResolverTests}. + * * @author Rossen Stoyanchev */ public class RequestBodyArgumentResolverTests { - private RequestBodyArgumentResolver resolver = resolver(new JacksonJsonDecoder()); - - private ServerWebExchange exchange; - - private MockServerHttpRequest request; - - private ResolvableMethod testMethod = ResolvableMethod.on(this.getClass()).name("handle"); - - - @Before - public void setUp() throws Exception { - this.request = new MockServerHttpRequest(HttpMethod.GET, new URI("/path")); - MockServerHttpResponse response = new MockServerHttpResponse(); - this.exchange = new DefaultServerWebExchange(this.request, response, new MockWebSessionManager()); - } - @Test public void supports() throws Exception { - RequestBodyArgumentResolver resolver = resolver(new StringDecoder()); - ResolvableType type = forClassWithGenerics(Mono.class, TestBean.class); - MethodParameter param = this.testMethod.resolveParam(type); + ResolvableMethod testMethod = ResolvableMethod.on(getClass()).name("handle"); + RequestBodyArgumentResolver resolver = resolver(); + + ResolvableType type = forClassWithGenerics(Mono.class, String.class); + MethodParameter param = testMethod.resolveParam(type); assertTrue(resolver.supportsParameter(param)); - MethodParameter parameter = this.testMethod.resolveParam(p -> !p.hasParameterAnnotations()); + MethodParameter parameter = testMethod.resolveParam(p -> !p.hasParameterAnnotations()); assertFalse(resolver.supportsParameter(parameter)); } - @Test - public void missingContentType() throws Exception { - String body = "{\"bar\":\"BARBAR\",\"foo\":\"FOOFOO\"}"; - this.request.writeWith(Flux.just(dataBuffer(body))); - ResolvableType type = forClassWithGenerics(Mono.class, TestBean.class); - MethodParameter param = this.testMethod.resolveParam(type); - Mono result = this.resolver.resolveArgument(param, new ExtendedModelMap(), this.exchange); - - TestSubscriber.subscribe(result) - .assertError(UnsupportedMediaTypeStatusException.class); - } - - @Test // SPR-9942 - public void missingContent() throws Exception { - this.request.writeWith(Flux.empty()); - ResolvableType type = forClassWithGenerics(Mono.class, TestBean.class); - MethodParameter param = this.testMethod.resolveParam(type); - Mono result = this.resolver.resolveArgument(param, new ExtendedModelMap(), this.exchange); - - TestSubscriber.subscribe(result) - .assertError(UnsupportedMediaTypeStatusException.class); - } - - @Test @SuppressWarnings("unchecked") - public void monoTestBean() throws Exception { - String body = "{\"bar\":\"b1\",\"foo\":\"f1\"}"; - ResolvableType type = forClassWithGenerics(Mono.class, TestBean.class); - MethodParameter param = this.testMethod.resolveParam(type); - Mono mono = (Mono) resolveValue(param, Mono.class, body); - - assertEquals(new TestBean("f1", "b1"), mono.block()); - } - - @Test @SuppressWarnings("unchecked") - public void fluxTestBean() throws Exception { - String body = "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]"; - ResolvableType type = forClassWithGenerics(Flux.class, TestBean.class); - MethodParameter param = this.testMethod.resolveParam(type); - Flux flux = (Flux) resolveValue(param, Flux.class, body); - - assertEquals(Arrays.asList(new TestBean("f1", "b1"), new TestBean("f2", "b2")), - flux.collectList().block()); - } - - @Test @SuppressWarnings("unchecked") - public void singleTestBean() throws Exception { - String body = "{\"bar\":\"b1\",\"foo\":\"f1\"}"; - ResolvableType type = forClassWithGenerics(Single.class, TestBean.class); - MethodParameter param = this.testMethod.resolveParam(type); - Single single = (Single) resolveValue(param, Single.class, body); - - assertEquals(new TestBean("f1", "b1"), single.toBlocking().value()); - } - - @Test @SuppressWarnings("unchecked") - public void observableTestBean() throws Exception { - String body = "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]"; - ResolvableType type = forClassWithGenerics(Observable.class, TestBean.class); - MethodParameter param = this.testMethod.resolveParam(type); - Observable observable = (Observable) resolveValue(param, Observable.class, body); - - assertEquals(Arrays.asList(new TestBean("f1", "b1"), new TestBean("f2", "b2")), - observable.toList().toBlocking().first()); - } - - @Test @SuppressWarnings("unchecked") - public void futureTestBean() throws Exception { - String body = "{\"bar\":\"b1\",\"foo\":\"f1\"}"; - ResolvableType type = forClassWithGenerics(CompletableFuture.class, TestBean.class); - MethodParameter param = this.testMethod.resolveParam(type); - CompletableFuture future = resolveValue(param, CompletableFuture.class, body); - - assertEquals(new TestBean("f1", "b1"), future.get()); - } - - @Test - public void testBean() throws Exception { - String body = "{\"bar\":\"b1\",\"foo\":\"f1\"}"; - MethodParameter param = this.testMethod.resolveParam( - forClass(TestBean.class), p -> p.hasParameterAnnotation(RequestBody.class)); - TestBean value = resolveValue(param, TestBean.class, body); - - assertEquals(new TestBean("f1", "b1"), value); - } - - @Test - public void map() throws Exception { - String body = "{\"bar\":\"b1\",\"foo\":\"f1\"}"; - Map map = new HashMap<>(); - map.put("foo", "f1"); - map.put("bar", "b1"); - ResolvableType type = forClassWithGenerics(Map.class, String.class, String.class); - MethodParameter param = this.testMethod.resolveParam(type); - Map actual = resolveValue(param, Map.class, body); - - assertEquals(map, actual); - } - - @Test - public void list() throws Exception { - String body = "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]"; - ResolvableType type = forClassWithGenerics(List.class, TestBean.class); - MethodParameter param = this.testMethod.resolveParam(type); - List list = resolveValue(param, List.class, body); - - assertEquals(Arrays.asList(new TestBean("f1", "b1"), new TestBean("f2", "b2")), list); - } - - @Test - public void monoList() throws Exception { - String body = "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]"; - ResolvableType type = forClassWithGenerics(Mono.class, forClassWithGenerics(List.class, TestBean.class)); - MethodParameter param = this.testMethod.resolveParam(type); - Mono mono = resolveValue(param, Mono.class, body); - - List list = (List) mono.block(Duration.ofSeconds(5)); - assertEquals(Arrays.asList(new TestBean("f1", "b1"), new TestBean("f2", "b2")), list); - } - - @Test - public void array() throws Exception { - String body = "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]"; - ResolvableType type = forClass(TestBean[].class); - MethodParameter param = this.testMethod.resolveParam(type); - TestBean[] value = resolveValue(param, TestBean[].class, body); - - assertArrayEquals(new TestBean[] {new TestBean("f1", "b1"), new TestBean("f2", "b2")}, value); - } - - @Test @SuppressWarnings("unchecked") - public void validateMonoTestBean() throws Exception { - String body = "{\"bar\":\"b1\"}"; - ResolvableType type = forClassWithGenerics(Mono.class, TestBean.class); - MethodParameter param = this.testMethod.resolveParam(type); - Mono mono = resolveValue(param, Mono.class, body); - - TestSubscriber.subscribe(mono) - .assertNoValues() - .assertError(ServerWebInputException.class); - } - - @Test @SuppressWarnings("unchecked") - public void validateFluxTestBean() throws Exception { - String body = "[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\"}]"; - ResolvableType type = forClassWithGenerics(Flux.class, TestBean.class); - MethodParameter param = this.testMethod.resolveParam(type); - Flux flux = resolveValue(param, Flux.class, body); - - TestSubscriber.subscribe(flux) - .assertValues(new TestBean("f1", "b1")) - .assertError(ServerWebInputException.class); - } - - @Test // SPR-9964 - @Ignore - public void parameterizedMethodArgument() throws Exception { - Class clazz = ConcreteParameterizedController.class; - MethodParameter param = ResolvableMethod.on(clazz).name("handleDto").resolveParam(); - SimpleBean simpleBean = resolveValue(param, SimpleBean.class, "{\"name\" : \"Jad\"}"); - - assertEquals("Jad", simpleBean.getName()); - } - - - - @SuppressWarnings("unchecked") - private T resolveValue(MethodParameter param, Class valueType, String body) { - - this.request.getHeaders().setContentType(MediaType.APPLICATION_JSON); - this.request.writeWith(Flux.just(dataBuffer(body))); - - Mono result = this.resolver.resolveArgument(param, new ExtendedModelMap(), this.exchange); - Object value = result.block(Duration.ofSeconds(5)); - - assertNotNull(value); - assertTrue("Unexpected return value type: " + value, valueType.isAssignableFrom(value.getClass())); - - return (T) value; - } - - @SuppressWarnings("Convert2MethodRef") - private RequestBodyArgumentResolver resolver(Decoder... decoders) { - + private RequestBodyArgumentResolver resolver() { List> converters = new ArrayList<>(); - Arrays.asList(decoders).forEach(decoder -> converters.add(new CodecHttpMessageConverter<>(decoder))); + converters.add(new CodecHttpMessageConverter<>(new StringDecoder())); FormattingConversionService service = new DefaultFormattingConversionService(); service.addConverter(new MonoToCompletableFutureConverter()); service.addConverter(new ReactorToRxJava1Converter()); - return new RequestBodyArgumentResolver(converters, service, new TestBeanValidator()); - } - - private DataBuffer dataBuffer(String body) { - byte[] bytes = body.getBytes(Charset.forName("UTF-8")); - ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); - return new DefaultDataBufferFactory().wrap(byteBuffer); + return new RequestBodyArgumentResolver(converters, service); } @SuppressWarnings("unused") - void handle( - @Validated @RequestBody Mono monoTestBean, - @Validated @RequestBody Flux fluxTestBean, - @RequestBody Single singleTestBean, - @RequestBody Observable observableTestBean, - @RequestBody CompletableFuture futureTestBean, - @RequestBody TestBean testBean, - @RequestBody Map map, - @RequestBody List list, - @RequestBody Mono> monoList, - @RequestBody Set set, - @RequestBody TestBean[] array, - TestBean paramWithoutAnnotation) { - } - - - @XmlRootElement - private static class TestBean { - - private String foo; - - private String bar; - - @SuppressWarnings("unused") - public TestBean() { - } - - TestBean(String foo, String bar) { - this.foo = foo; - this.bar = bar; - } - - public String getFoo() { - return this.foo; - } - - public void setFoo(String foo) { - this.foo = foo; - } - - public String getBar() { - return this.bar; - } - - public void setBar(String bar) { - this.bar = bar; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o instanceof TestBean) { - TestBean other = (TestBean) o; - return this.foo.equals(other.foo) && this.bar.equals(other.bar); - } - return false; - } - - @Override - public int hashCode() { - return 31 * foo.hashCode() + bar.hashCode(); - } - - @Override - public String toString() { - return "TestBean[foo='" + this.foo + "\'" + ", bar='" + this.bar + "\']"; - } - } - - private static class TestBeanValidator implements Validator { - - @Override - public boolean supports(Class clazz) { - return clazz.equals(TestBean.class); - } - - @Override - public void validate(Object target, Errors errors) { - TestBean testBean = (TestBean) target; - if (testBean.getFoo() == null) { - errors.rejectValue("foo", "nullValue"); - } - } - } - - private static abstract class AbstractParameterizedController { - - @SuppressWarnings("unused") - public void handleDto(@RequestBody DTO dto) {} - } - - private static class ConcreteParameterizedController extends AbstractParameterizedController { - } - - private interface Identifiable extends Serializable { - - Long getId(); - - void setId(Long id); - } - - @SuppressWarnings({ "serial" }) - private static class SimpleBean implements Identifiable { - - private Long id; - - private String name; - - @Override - public Long getId() { - return id; - } - - @Override - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - } + void handle(@RequestBody Mono monoString, String paramWithoutAnnotation) {} } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java index 06efa14061..cfd747d5a3 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java @@ -59,13 +59,13 @@ import static org.junit.Assert.assertEquals; /** - * Unit tests for {@link ResponseBodyResultHandler}. - * + * Unit tests for {@link ResponseBodyResultHandler}.When adding a test also * consider whether the logic under test is in a parent class, then see: *
      *
    • {@code MessageConverterResultHandlerTests}, *
    • {@code ContentNegotiatingResultHandlerSupportTests} *
    + * * @author Sebastien Deleuze * @author Rossen Stoyanchev */ diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java index 546c1bd0b4..93860f2a16 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java @@ -120,13 +120,13 @@ public class ResponseEntityResultHandlerTests { ResolvableType type = responseEntity(String.class); assertTrue(this.resultHandler.supports(handlerResult(value, type))); - type = classWithGenerics(Mono.class, responseEntity(String.class)); + type = forClassWithGenerics(Mono.class, responseEntity(String.class)); assertTrue(this.resultHandler.supports(handlerResult(value, type))); - type = classWithGenerics(Single.class, responseEntity(String.class)); + type = forClassWithGenerics(Single.class, responseEntity(String.class)); assertTrue(this.resultHandler.supports(handlerResult(value, type))); - type = classWithGenerics(CompletableFuture.class, responseEntity(String.class)); + type = forClassWithGenerics(CompletableFuture.class, responseEntity(String.class)); assertTrue(this.resultHandler.supports(handlerResult(value, type))); type = ResolvableType.forClass(String.class); @@ -195,11 +195,7 @@ public class ResponseEntityResultHandlerTests { private ResolvableType responseEntity(Class bodyType) { - return classWithGenerics(ResponseEntity.class, ResolvableType.forClass(bodyType)); - } - - private ResolvableType classWithGenerics(Class sourceType, ResolvableType genericType) { - return ResolvableType.forClassWithGenerics(sourceType, genericType); + return forClassWithGenerics(ResponseEntity.class, ResolvableType.forClass(bodyType)); } private HandlerResult handlerResult(Object returnValue, ResolvableType type) { From a584311178fc3873cd085b16c571873da148b7c8 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 7 Jul 2016 05:36:19 -0400 Subject: [PATCH 477/498] Fix StringDecoder#decodeToMono issue with empty input Before this change decodeToMono always created a StringBuilder to aggregate resulting in an "" (empty string) rather than an empty Mono for an empty input stream. Now we aggregate in the DataBuffer instead and then decode to String. --- .../core/codec/StringDecoder.java | 18 +++++++------- .../core/codec/StringDecoderTests.java | 24 ++++++++++++++++++- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/StringDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/StringDecoder.java index 51dc6a5eb5..a96e14f536 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/StringDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/StringDecoder.java @@ -91,16 +91,16 @@ public class StringDecoder extends AbstractDecoder { if (this.splitOnNewline) { inputFlux = Flux.from(inputStream).flatMap(StringDecoder::splitOnNewline); } - return decodeInternal(inputFlux, mimeType); + return inputFlux.map(buffer -> decodeDataBuffer(buffer, mimeType)); } @Override public Mono decodeToMono(Publisher inputStream, ResolvableType elementType, MimeType mimeType, Object... hints) { - return decodeInternal(Flux.from(inputStream), mimeType). - collect(StringBuilder::new, StringBuilder::append). - map(StringBuilder::toString); + return Flux.from(inputStream) + .reduce(DataBuffer::write) + .map(buffer -> decodeDataBuffer(buffer, mimeType)); } private static Flux splitOnNewline(DataBuffer dataBuffer) { @@ -120,13 +120,11 @@ public class StringDecoder extends AbstractDecoder { return Flux.fromIterable(results); } - private Flux decodeInternal(Flux inputFlux, MimeType mimeType) { + private String decodeDataBuffer(DataBuffer dataBuffer, MimeType mimeType) { Charset charset = getCharset(mimeType); - return inputFlux.map(dataBuffer -> { - CharBuffer charBuffer = charset.decode(dataBuffer.asByteBuffer()); - DataBufferUtils.release(dataBuffer); - return charBuffer.toString(); - }); + CharBuffer charBuffer = charset.decode(dataBuffer.asByteBuffer()); + DataBufferUtils.release(dataBuffer); + return charBuffer.toString(); } private Charset getCharset(MimeType mimeType) { diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/StringDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/StringDecoderTests.java index f3d4bb43b0..43492ae6a9 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/StringDecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/StringDecoderTests.java @@ -73,7 +73,18 @@ public class StringDecoderTests extends AbstractDataBufferAllocatingTestCase { } @Test - public void decodeEmpty() throws InterruptedException { + public void decodeEmptyFlux() throws InterruptedException { + Flux source = Flux.empty(); + Flux output = this.decoder.decode(source, ResolvableType.forClass(String.class), null); + + TestSubscriber.subscribe(output) + .assertNoError() + .assertComplete() + .assertNoValues(); + } + + @Test + public void decodeEmptyString() throws InterruptedException { Flux source = Flux.just(stringBuffer("")); Flux output = this.decoder.decode(source, ResolvableType.forClass(String.class), null); @@ -92,4 +103,15 @@ public class StringDecoderTests extends AbstractDataBufferAllocatingTestCase { .assertValues("foobarbaz"); } + @Test + public void decodeToMonoWithEmptyFlux() throws InterruptedException { + Flux source = Flux.empty(); + Mono output = this.decoder.decodeToMono(source, ResolvableType.forClass(String.class), null); + + TestSubscriber.subscribe(output) + .assertNoError() + .assertComplete() + .assertNoValues(); + } + } From 7534092ef3acf2a5c5f41d6b468915bbbf3568ae Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 7 Jul 2016 15:41:51 -0400 Subject: [PATCH 478/498] Comprensive support for empty request body This commit adds support for handling an empty request body with both HttpEntity where the body is not required and with @RequestBody where the body is required depending on the annotation's required flag. If the body is an explicit type (e.g. String, HttpEntity) and the body is required an exception is raised before the method is even invoked or otherwise the body is passed in as null. If the body is declared as an async type (e.g. Mono, HttpEntity>) and is required, the error will flow through the async type. If not required, the async type will be passed with no values (i.e. empty). A notable exception is rx.Single which can only have one value or one error and cannot be empty. As a result currently the use of rx.Single to represent the request body in any form effectively implies the body is required. --- ...tractMessageConverterArgumentResolver.java | 31 ++- .../HttpEntityArgumentResolver.java | 27 ++- .../RequestBodyArgumentResolver.java | 4 +- .../reactive/DispatcherHandlerErrorTests.java | 4 +- .../web/reactive/result/ResolvableMethod.java | 4 +- .../HttpEntityArgumentResolverTests.java | 81 ++++++- ...MessageConverterArgumentResolverTests.java | 17 +- .../RequestBodyArgumentResolverTests.java | 208 +++++++++++++++++- 8 files changed, 339 insertions(+), 37 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageConverterArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageConverterArgumentResolver.java index 44f462d658..5f43652c36 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageConverterArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageConverterArgumentResolver.java @@ -105,7 +105,8 @@ public abstract class AbstractMessageConverterArgumentResolver { } - protected Mono readBody(MethodParameter bodyParameter, ServerWebExchange exchange) { + protected Mono readBody(MethodParameter bodyParameter, boolean isBodyRequired, + ServerWebExchange exchange) { TypeDescriptor typeDescriptor = new TypeDescriptor(bodyParameter); boolean convertFromMono = getConversionService().canConvert(MONO_TYPE, typeDescriptor); @@ -125,14 +126,22 @@ public abstract class AbstractMessageConverterArgumentResolver { for (HttpMessageConverter converter : getMessageConverters()) { if (converter.canRead(elementType, mediaType)) { if (convertFromFlux) { - Flux flux = converter.read(elementType, request); + Flux flux = converter.read(elementType, request) + .onErrorResumeWith(ex -> Flux.error(getReadError(ex, bodyParameter))); + if (checkRequired(bodyParameter, isBodyRequired)) { + flux = flux.switchIfEmpty(Flux.error(getRequiredBodyError(bodyParameter))); + } if (this.validator != null) { flux = flux.map(applyValidationIfApplicable(bodyParameter)); } return Mono.just(getConversionService().convert(flux, FLUX_TYPE, typeDescriptor)); } else { - Mono mono = converter.readMono(elementType, request); + Mono mono = converter.readMono(elementType, request) + .otherwise(ex -> Mono.error(getReadError(ex, bodyParameter))); + if (checkRequired(bodyParameter, isBodyRequired)) { + mono = mono.otherwiseIfEmpty(Mono.error(getRequiredBodyError(bodyParameter))); + } if (this.validator != null) { mono = mono.map(applyValidationIfApplicable(bodyParameter)); } @@ -149,6 +158,22 @@ public abstract class AbstractMessageConverterArgumentResolver { return Mono.error(new UnsupportedMediaTypeStatusException(mediaType, this.supportedMediaTypes)); } + protected boolean checkRequired(MethodParameter bodyParameter, boolean isBodyRequired) { + if ("rx.Single".equals(bodyParameter.getNestedParameterType().getName())) { + return true; + } + return isBodyRequired; + } + + protected ServerWebInputException getReadError(Throwable ex, MethodParameter parameter) { + return new ServerWebInputException("Failed to read HTTP message", parameter, ex); + } + + protected ServerWebInputException getRequiredBodyError(MethodParameter parameter) { + return new ServerWebInputException("Required request body is missing: " + + parameter.getMethod().toGenericString()); + } + protected Function applyValidationIfApplicable(MethodParameter methodParam) { Annotation[] annotations = methodParam.getParameterAnnotations(); for (Annotation ann : annotations) { diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/HttpEntityArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/HttpEntityArgumentResolver.java index b2bb395e1d..0cd9bcf6b2 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/HttpEntityArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/HttpEntityArgumentResolver.java @@ -91,17 +91,22 @@ public class HttpEntityArgumentResolver extends AbstractMessageConverterArgument bodyParameter.increaseNestingLevel(); } - return readBody(bodyParameter, exchange) - .map(body -> { - ServerHttpRequest request = exchange.getRequest(); - HttpHeaders headers = request.getHeaders(); - if (RequestEntity.class == entityType.getRawClass()) { - return new RequestEntity<>(body, headers, request.getMethod(), request.getURI()); - } - else { - return new HttpEntity<>(body, headers); - } - }); + return readBody(bodyParameter, false, exchange) + .map(body -> createHttpEntity(body, entityType, exchange)) + .defaultIfEmpty(createHttpEntity(null, entityType, exchange)); + } + + private Object createHttpEntity(Object body, ResolvableType entityType, + ServerWebExchange exchange) { + + ServerHttpRequest request = exchange.getRequest(); + HttpHeaders headers = request.getHeaders(); + if (RequestEntity.class == entityType.getRawClass()) { + return new RequestEntity<>(body, headers, request.getMethod(), request.getURI()); + } + else { + return new HttpEntity<>(body, headers); + } } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java index 8d8e7f01ac..fc12bbeb26 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java @@ -21,7 +21,6 @@ import java.util.List; import reactor.core.publisher.Mono; import org.springframework.core.MethodParameter; -import org.springframework.core.ResolvableType; import org.springframework.core.convert.ConversionService; import org.springframework.http.converter.reactive.HttpMessageConverter; import org.springframework.ui.ModelMap; @@ -79,7 +78,8 @@ public class RequestBodyArgumentResolver extends AbstractMessageConverterArgumen @Override public Mono resolveArgument(MethodParameter param, ModelMap model, ServerWebExchange exchange) { - return readBody(param, exchange); + boolean isRequired = param.getParameterAnnotation(RequestBody.class).required(); + return readBody(param, isRequired, exchange); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java index affa9c3f75..65c721844e 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java @@ -49,6 +49,7 @@ import org.springframework.web.reactive.result.method.annotation.ResponseBodyRes import org.springframework.web.server.NotAcceptableStatusException; import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.ServerWebInputException; import org.springframework.web.server.WebExceptionHandler; import org.springframework.web.server.WebHandler; import org.springframework.web.server.adapter.DefaultServerWebExchange; @@ -163,7 +164,8 @@ public class DispatcherHandlerErrorTests { Mono publisher = this.dispatcherHandler.handle(this.exchange); TestSubscriber.subscribe(publisher) - .assertErrorWith(ex -> assertSame(EXCEPTION, ex)); + .assertError(ServerWebInputException.class) + .assertErrorWith(ex -> assertSame(EXCEPTION, ex.getCause())); } @Test diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/ResolvableMethod.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/ResolvableMethod.java index bd2ede3151..1070c488dc 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/ResolvableMethod.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/ResolvableMethod.java @@ -171,8 +171,8 @@ public class ResolvableMethod { matches.add(param); } - Assert.isTrue(!matches.isEmpty(), "No matching method argument: " + this); - Assert.isTrue(matches.size() == 1, "Multiple matching method arguments: " + matches); + Assert.isTrue(!matches.isEmpty(), "No matching arg on " + method.toString()); + Assert.isTrue(matches.size() == 1, "Multiple matching args: " + matches + " on " + method.toString()); return matches.get(0); } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/HttpEntityArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/HttpEntityArgumentResolverTests.java index f42dce42ee..0df938c38b 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/HttpEntityArgumentResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/HttpEntityArgumentResolverTests.java @@ -25,6 +25,8 @@ import java.util.concurrent.CompletableFuture; import org.junit.Before; import org.junit.Test; +import reactor.core.converter.RxJava1ObservableConverter; +import reactor.core.converter.RxJava1SingleConverter; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.test.TestSubscriber; @@ -51,12 +53,14 @@ import org.springframework.http.server.reactive.MockServerHttpResponse; import org.springframework.ui.ExtendedModelMap; import org.springframework.web.reactive.result.ResolvableMethod; import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.ServerWebInputException; import org.springframework.web.server.adapter.DefaultServerWebExchange; import org.springframework.web.server.session.MockWebSessionManager; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.springframework.core.ResolvableType.forClassWithGenerics; @@ -106,6 +110,68 @@ public class HttpEntityArgumentResolverTests { assertFalse(this.resolver.supportsParameter(this.testMethod.resolveParam(type))); } + @Test + public void emptyBodyWithString() throws Exception { + ResolvableType type = httpEntity(String.class); + HttpEntity entity = resolveValueWithEmptyBody(type); + + assertNull(entity.getBody()); + } + + @Test + public void emptyBodyWithMono() throws Exception { + ResolvableType type = httpEntity(forClassWithGenerics(Mono.class, String.class)); + HttpEntity> entity = resolveValueWithEmptyBody(type); + + TestSubscriber.subscribe(entity.getBody()) + .assertNoError() + .assertComplete() + .assertNoValues(); + } + + @Test + public void emptyBodyWithFlux() throws Exception { + ResolvableType type = httpEntity(forClassWithGenerics(Flux.class, String.class)); + HttpEntity> entity = resolveValueWithEmptyBody(type); + + TestSubscriber.subscribe(entity.getBody()) + .assertNoError() + .assertComplete() + .assertNoValues(); + } + + @Test + public void emptyBodyWithSingle() throws Exception { + ResolvableType type = httpEntity(forClassWithGenerics(Single.class, String.class)); + HttpEntity> entity = resolveValueWithEmptyBody(type); + + TestSubscriber.subscribe(RxJava1SingleConverter.from(entity.getBody())) + .assertNoValues() + .assertError(ServerWebInputException.class); + } + + @Test + public void emptyBodyWithObservable() throws Exception { + ResolvableType type = httpEntity(forClassWithGenerics(Observable.class, String.class)); + HttpEntity> entity = resolveValueWithEmptyBody(type); + + TestSubscriber.subscribe(RxJava1ObservableConverter.from(entity.getBody())) + .assertNoError() + .assertComplete() + .assertNoValues(); + } + + @Test + public void emptyBodyWithCompletableFuture() throws Exception { + ResolvableType type = httpEntity(forClassWithGenerics(CompletableFuture.class, String.class)); + HttpEntity> entity = resolveValueWithEmptyBody(type); + + entity.getBody().whenComplete((body, ex) -> { + assertNull(body); + assertNull(ex); + }); + } + @Test public void httpEntityWithStringBody() throws Exception { String body = "line1"; @@ -211,6 +277,17 @@ public class HttpEntityArgumentResolverTests { return (T) value; } + @SuppressWarnings("unchecked") + private HttpEntity resolveValueWithEmptyBody(ResolvableType type) { + this.request.writeWith(Flux.empty()); + MethodParameter param = this.testMethod.resolveParam(type); + Mono result = this.resolver.resolveArgument(param, new ExtendedModelMap(), this.exchange); + HttpEntity httpEntity = (HttpEntity) result.block(Duration.ofSeconds(5)); + + assertEquals(this.request.getHeaders(), httpEntity.getHeaders()); + return (HttpEntity) httpEntity; + } + private DataBuffer dataBuffer(String body) { byte[] bytes = body.getBytes(Charset.forName("UTF-8")); ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); @@ -224,10 +301,10 @@ public class HttpEntityArgumentResolverTests { Mono monoString, HttpEntity httpEntity, HttpEntity> monoBody, - HttpEntity> singleBody, - HttpEntity> completableFutureBody, HttpEntity> fluxBody, + HttpEntity> singleBody, HttpEntity> observableBody, + HttpEntity> completableFutureBody, RequestEntity requestEntity) {} } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterArgumentResolverTests.java index ea6d7cd81c..18b6c582a8 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterArgumentResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterArgumentResolverTests.java @@ -88,7 +88,7 @@ public class MessageConverterArgumentResolverTests { @Before public void setUp() throws Exception { - this.request = new MockServerHttpRequest(HttpMethod.GET, new URI("/path")); + this.request = new MockServerHttpRequest(HttpMethod.POST, new URI("/path")); MockServerHttpResponse response = new MockServerHttpResponse(); this.exchange = new DefaultServerWebExchange(this.request, response, new MockWebSessionManager()); } @@ -100,20 +100,23 @@ public class MessageConverterArgumentResolverTests { this.request.writeWith(Flux.just(dataBuffer(body))); ResolvableType type = forClassWithGenerics(Mono.class, TestBean.class); MethodParameter param = this.testMethod.resolveParam(type); - Mono result = this.resolver.readBody(param, this.exchange); + Mono result = this.resolver.readBody(param, true, this.exchange); TestSubscriber.subscribe(result) .assertError(UnsupportedMediaTypeStatusException.class); } - @Test // SPR-9942 - public void noContent() throws Exception { + // More extensive "empty body" tests in RequestBody- and HttpEntityArgumentResolverTests + + @Test @SuppressWarnings("unchecked") // SPR-9942 + public void emptyBody() throws Exception { this.request.writeWith(Flux.empty()); + this.request.getHeaders().setContentType(MediaType.APPLICATION_JSON); ResolvableType type = forClassWithGenerics(Mono.class, TestBean.class); MethodParameter param = this.testMethod.resolveParam(type); - Mono result = this.resolver.readBody(param, this.exchange); + Mono result = (Mono) this.resolver.readBody(param, true, this.exchange).block(); - TestSubscriber.subscribe(result).assertError(UnsupportedMediaTypeStatusException.class); + TestSubscriber.subscribe(result).assertError(ServerWebInputException.class); } @Test @@ -262,7 +265,7 @@ public class MessageConverterArgumentResolverTests { this.request.getHeaders().setContentType(MediaType.APPLICATION_JSON); this.request.writeWith(Flux.just(dataBuffer(body))); - Mono result = this.resolver.readBody(param, this.exchange); + Mono result = this.resolver.readBody(param, true, this.exchange); Object value = result.block(Duration.ofSeconds(5)); assertNotNull(value); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java index a694d9c98e..3300637bb1 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java @@ -15,26 +15,53 @@ */ package org.springframework.web.reactive.result.method.annotation; +import java.net.URI; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.time.Duration; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.Predicate; +import org.junit.Before; import org.junit.Test; +import reactor.core.converter.RxJava1ObservableConverter; +import reactor.core.converter.RxJava1SingleConverter; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.core.test.TestSubscriber; +import rx.Observable; +import rx.Single; import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.core.codec.StringDecoder; import org.springframework.core.convert.support.MonoToCompletableFutureConverter; import org.springframework.core.convert.support.ReactorToRxJava1Converter; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.format.support.FormattingConversionService; +import org.springframework.http.HttpMethod; import org.springframework.http.converter.reactive.CodecHttpMessageConverter; import org.springframework.http.converter.reactive.HttpMessageConverter; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.ui.ExtendedModelMap; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.reactive.result.ResolvableMethod; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.ServerWebInputException; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.MockWebSessionManager; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.springframework.core.ResolvableType.forClass; import static org.springframework.core.ResolvableType.forClassWithGenerics; /** @@ -46,21 +73,130 @@ import static org.springframework.core.ResolvableType.forClassWithGenerics; */ public class RequestBodyArgumentResolverTests { + private RequestBodyArgumentResolver resolver = resolver(); + + private ServerWebExchange exchange; + + private MockServerHttpRequest request; + + private ResolvableMethod testMethod = ResolvableMethod.on(this.getClass()).name("handle"); + + + @Before + public void setUp() throws Exception { + this.request = new MockServerHttpRequest(HttpMethod.POST, new URI("/path")); + MockServerHttpResponse response = new MockServerHttpResponse(); + this.exchange = new DefaultServerWebExchange(this.request, response, new MockWebSessionManager()); + } + @Test public void supports() throws Exception { - - ResolvableMethod testMethod = ResolvableMethod.on(getClass()).name("handle"); - RequestBodyArgumentResolver resolver = resolver(); - ResolvableType type = forClassWithGenerics(Mono.class, String.class); - MethodParameter param = testMethod.resolveParam(type); - assertTrue(resolver.supportsParameter(param)); + MethodParameter param = this.testMethod.resolveParam(type, requestBody(true)); + assertTrue(this.resolver.supportsParameter(param)); - MethodParameter parameter = testMethod.resolveParam(p -> !p.hasParameterAnnotations()); - assertFalse(resolver.supportsParameter(parameter)); + MethodParameter parameter = this.testMethod.resolveParam(p -> !p.hasParameterAnnotations()); + assertFalse(this.resolver.supportsParameter(parameter)); } + @Test + public void stringBody() throws Exception { + String body = "line1"; + ResolvableType type = forClass(String.class); + MethodParameter param = this.testMethod.resolveParam(type, requestBody(true)); + String value = resolveValue(param, body); + + assertEquals(body, value); + } + + @Test(expected = ServerWebInputException.class) + public void emptyBodyWithString() throws Exception { + resolveValueWithEmptyBody(forClass(String.class), true); + } + + @Test + public void emptyBodyWithStringNotRequired() throws Exception { + ResolvableType type = forClass(String.class); + String body = resolveValueWithEmptyBody(type, false); + + assertNull(body); + } + + @Test + public void emptyBodyWithMono() throws Exception { + ResolvableType type = forClassWithGenerics(Mono.class, String.class); + + TestSubscriber.subscribe(resolveValueWithEmptyBody(type, true)) + .assertNoValues() + .assertError(ServerWebInputException.class); + + TestSubscriber.subscribe(resolveValueWithEmptyBody(type, false)) + .assertNoValues() + .assertComplete(); + } + + @Test + public void emptyBodyWithFlux() throws Exception { + ResolvableType type = forClassWithGenerics(Flux.class, String.class); + + TestSubscriber.subscribe(resolveValueWithEmptyBody(type, true)) + .assertNoValues() + .assertError(ServerWebInputException.class); + + TestSubscriber.subscribe(resolveValueWithEmptyBody(type, false)) + .assertNoValues() + .assertComplete(); + } + + @Test + public void emptyBodyWithSingle() throws Exception { + ResolvableType type = forClassWithGenerics(Single.class, String.class); + + Single single = resolveValueWithEmptyBody(type, true); + TestSubscriber.subscribe(RxJava1SingleConverter.from(single)) + .assertNoValues() + .assertError(ServerWebInputException.class); + + single = resolveValueWithEmptyBody(type, false); + TestSubscriber.subscribe(RxJava1SingleConverter.from(single)) + .assertNoValues() + .assertError(ServerWebInputException.class); + } + + @Test + public void emptyBodyWithObservable() throws Exception { + ResolvableType type = forClassWithGenerics(Observable.class, String.class); + + Observable observable = resolveValueWithEmptyBody(type, true); + TestSubscriber.subscribe(RxJava1ObservableConverter.from(observable)) + .assertNoValues() + .assertError(ServerWebInputException.class); + + observable = resolveValueWithEmptyBody(type, false); + TestSubscriber.subscribe(RxJava1ObservableConverter.from(observable)) + .assertNoValues() + .assertComplete(); + } + + @Test + public void emptyBodyWithCompletableFuture() throws Exception { + ResolvableType type = forClassWithGenerics(CompletableFuture.class, String.class); + + CompletableFuture future = resolveValueWithEmptyBody(type, true); + future.whenComplete((text, ex) -> { + assertNull(text); + assertNotNull(ex); + }); + + future = resolveValueWithEmptyBody(type, false); + future.whenComplete((text, ex) -> { + assertNotNull(text); + assertNull(ex); + }); + } + + private RequestBodyArgumentResolver resolver() { List> converters = new ArrayList<>(); converters.add(new CodecHttpMessageConverter<>(new StringDecoder())); @@ -72,8 +208,62 @@ public class RequestBodyArgumentResolverTests { return new RequestBodyArgumentResolver(converters, service); } + private T resolveValue(MethodParameter param, String body) { + this.request.writeWith(Flux.just(dataBuffer(body))); + Mono result = this.resolver.readBody(param, true, this.exchange); + Object value = result.block(Duration.ofSeconds(5)); + + assertNotNull(value); + assertTrue("Unexpected return value type: " + value, + param.getParameterType().isAssignableFrom(value.getClass())); + + //noinspection unchecked + return (T) value; + } + + private T resolveValueWithEmptyBody(ResolvableType type, boolean required) { + this.request.writeWith(Flux.empty()); + MethodParameter param = this.testMethod.resolveParam(type, requestBody(required)); + Mono result = this.resolver.resolveArgument(param, new ExtendedModelMap(), this.exchange); + Object value = result.block(Duration.ofSeconds(5)); + + if (value != null) { + assertTrue("Unexpected return value type: " + value, + param.getParameterType().isAssignableFrom(value.getClass())); + } + + //noinspection unchecked + return (T) value; + } + + private Predicate requestBody(boolean required) { + return p -> { + RequestBody annotation = p.getParameterAnnotation(RequestBody.class); + return annotation != null && annotation.required() == required; + }; + } + + private DataBuffer dataBuffer(String body) { + byte[] bytes = body.getBytes(Charset.forName("UTF-8")); + ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); + return new DefaultDataBufferFactory().wrap(byteBuffer); + } + @SuppressWarnings("unused") - void handle(@RequestBody Mono monoString, String paramWithoutAnnotation) {} + void handle( + @RequestBody String string, + @RequestBody Mono mono, + @RequestBody Flux flux, + @RequestBody Single single, + @RequestBody Observable obs, + @RequestBody CompletableFuture future, + @RequestBody(required = false) String stringNotRequired, + @RequestBody(required = false) Mono monoNotRequired, + @RequestBody(required = false) Flux fluxNotRequired, + @RequestBody(required = false) Single singleNotRequired, + @RequestBody(required = false) Observable obsNotRequired, + @RequestBody(required = false) CompletableFuture futureNotRequired, + String notAnnotated) {} } From d7a43d2003a62dce8651a437a79adf881481f2c0 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 7 Jul 2016 23:04:27 -0400 Subject: [PATCH 479/498] Polish tests --- .../web/reactive/result/ResolvableMethod.java | 79 ++++++++-- .../result/SimpleResultHandlerTests.java | 2 +- .../method/InvocableHandlerMethodTests.java | 6 +- ...RequestMappingInfoHandlerMappingTests.java | 148 ++++++++++-------- .../HttpEntityArgumentResolverTests.java | 66 ++++---- ...MessageConverterArgumentResolverTests.java | 4 +- .../MessageConverterResultHandlerTests.java | 2 +- .../RequestBodyArgumentResolverTests.java | 28 ++-- .../ResponseEntityResultHandlerTests.java | 2 +- .../view/HttpMessageConverterViewTests.java | 2 +- .../ViewResolutionResultHandlerTests.java | 10 +- 11 files changed, 204 insertions(+), 145 deletions(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/ResolvableMethod.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/ResolvableMethod.java index 1070c488dc..0b572755bc 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/ResolvableMethod.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/ResolvableMethod.java @@ -30,6 +30,7 @@ import org.springframework.core.annotation.AnnotationUtils; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; +import org.springframework.web.reactive.result.method.InvocableHandlerMethod; /** * Convenience class for use in tests to resolve a {@link Method} and/or any of @@ -47,7 +48,7 @@ import org.springframework.util.ReflectionUtils; * response handling, the return type may be used as a hint: * *
    - * ResolvableMethod resolvableMethod = ResolvableMethod.on(TestController.class);
    + * ResolvableMethod resolvableMethod = ResolvableMethod.onClass(TestController.class);
     
      * ResolvableType type = ResolvableType.forClassWithGenerics(Mono.class, View.class);
      * Method method = resolvableMethod.returning(type).resolve();
    @@ -65,7 +66,10 @@ import org.springframework.util.ReflectionUtils;
      */
     public class ResolvableMethod {
     
    -	private final Class targetClass;
    +	private final Class objectClass;
    +
    +	private final Object object;
    +
     
     	private String methodName;
     
    @@ -75,36 +79,67 @@ public class ResolvableMethod {
     
     	private final List> annotationTypes = new ArrayList<>(4);
     
    +	private final List> predicates = new ArrayList<>(4);
     
    -	private ResolvableMethod(Class targetClass) {
    -		this.targetClass = targetClass;
    +
    +
    +	private ResolvableMethod(Class objectClass) {
    +		Assert.notNull(objectClass);
    +		this.objectClass = objectClass;
    +		this.object = null;
    +	}
    +
    +	private ResolvableMethod(Object object) {
    +		Assert.notNull(object);
    +		this.object = object;
    +		this.objectClass = object.getClass();
     	}
     
     
    +	/**
    +	 * Methods that match the given name (regardless of arguments).
    +	 */
     	public ResolvableMethod name(String methodName) {
     		this.methodName = methodName;
     		return this;
     	}
     
    +	/**
    +	 * Methods that match the given argument types.
    +	 */
     	public ResolvableMethod argumentTypes(Class... argumentTypes) {
     		this.argumentTypes = argumentTypes;
     		return this;
     	}
     
    +	/**
    +	 * Methods declared to return the given type.
    +	 */
     	public ResolvableMethod returning(ResolvableType resolvableType) {
     		this.returnType = resolvableType;
     		return this;
     	}
     
    +	/**
    +	 * Methods with the given annotation.
    +	 */
     	public ResolvableMethod annotated(Class annotationType) {
     		this.annotationTypes.add(annotationType);
     		return this;
     	}
     
    +	/**
    +	 * Methods matching the given predicate.
    +	 */
    +	public final ResolvableMethod matching(Predicate methodPredicate) {
    +		this.predicates.add(methodPredicate);
    +		return this;
    +	}
    +
     	// Resolve methods
     
     	public Method resolve() {
    -		Set methods = MethodIntrospector.selectMethods(this.targetClass,
    +		Set methods = MethodIntrospector.selectMethods(this.objectClass,
     				(ReflectionUtils.MethodFilter) method -> {
     					if (this.methodName != null && !this.methodName.equals(method.getName())) {
     						return false;
    @@ -116,15 +151,19 @@ public class ResolvableMethod {
     							return false;
     						}
     					}
    -					if (!ObjectUtils.isEmpty(this.argumentTypes)) {
    +					else if (!ObjectUtils.isEmpty(this.argumentTypes)) {
     						if (!Arrays.equals(this.argumentTypes, method.getParameterTypes())) {
     							return false;
     						}
     					}
    -					for (Class annotationType : this.annotationTypes) {
    -						if (AnnotationUtils.findAnnotation(method, annotationType) == null) {
    -							return false;
    -						}
    +					else if (this.annotationTypes.stream()
    +							.filter(annotType -> AnnotationUtils.findAnnotation(method, annotType) == null)
    +							.findFirst()
    +							.isPresent()) {
    +						return false;
    +					}
    +					else if (this.predicates.stream().filter(p -> !p.test(method)).findFirst().isPresent()) {
    +						return false;
     					}
     					return true;
     				});
    @@ -139,6 +178,11 @@ public class ResolvableMethod {
     		return this.returnType != null ? this.returnType.toString() : null;
     	}
     
    +	public InvocableHandlerMethod resolveHandlerMethod() {
    +		Assert.notNull(this.object);
    +		return new InvocableHandlerMethod(this.object, resolve());
    +	}
    +
     	public MethodParameter resolveReturnType() {
     		Method method = resolve();
     		return new MethodParameter(method, -1);
    @@ -177,18 +221,21 @@ public class ResolvableMethod {
     		return matches.get(0);
     	}
     
    -
    -
    -
     	@Override
     	public String toString() {
    -		return "Class=" + this.targetClass + ", name= " + this.methodName +
    -				", returnType=" + this.returnType + ", annotations=" + this.annotationTypes;
    +		return "Class=" + this.objectClass +
    +				", name=" + (this.methodName != null ? this.methodName : "") +
    +				", returnType=" + (this.returnType != null ? this.returnType : "") +
    +				", annotations=" + this.annotationTypes;
     	}
     
     
    -	public static ResolvableMethod on(Class clazz) {
    +	public static ResolvableMethod onClass(Class clazz) {
     		return new ResolvableMethod(clazz);
     	}
     
    +	public static ResolvableMethod on(Object object) {
    +		return new ResolvableMethod(object);
    +	}
    +
     }
    \ No newline at end of file
    diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/SimpleResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/SimpleResultHandlerTests.java
    index 7aba79ee55..646956d4ef 100644
    --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/SimpleResultHandlerTests.java
    +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/SimpleResultHandlerTests.java
    @@ -72,7 +72,7 @@ public class SimpleResultHandlerTests {
     	}
     
     	private void testSupports(ResolvableType type, boolean result) {
    -		MethodParameter param = ResolvableMethod.on(TestController.class).returning(type).resolveReturnType();
    +		MethodParameter param = ResolvableMethod.onClass(TestController.class).returning(type).resolveReturnType();
     		HandlerResult handlerResult = new HandlerResult(new TestController(), null, param);
     		assertEquals(result, this.resultHandler.supports(handlerResult));
     	}
    diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/InvocableHandlerMethodTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/InvocableHandlerMethodTests.java
    index 4984f9e056..08a8269369 100644
    --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/InvocableHandlerMethodTests.java
    +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/InvocableHandlerMethodTests.java
    @@ -15,7 +15,6 @@
      */
     package org.springframework.web.reactive.result.method;
     
    -import java.lang.reflect.Method;
     import java.net.URI;
     import java.util.Collections;
     import java.util.Optional;
    @@ -30,7 +29,6 @@ import org.springframework.http.server.reactive.MockServerHttpRequest;
     import org.springframework.http.server.reactive.MockServerHttpResponse;
     import org.springframework.ui.ExtendedModelMap;
     import org.springframework.ui.ModelMap;
    -import org.springframework.web.method.HandlerMethod;
     import org.springframework.web.reactive.HandlerResult;
     import org.springframework.web.reactive.result.ResolvableMethod;
     import org.springframework.web.server.ServerWebExchange;
    @@ -150,8 +148,8 @@ public class InvocableHandlerMethodTests {
     
     
     	private InvocableHandlerMethod handlerMethod(String name) throws Exception {
    -		Method method = ResolvableMethod.on(TestController.class).name(name).resolve();
    -		return new InvocableHandlerMethod(new HandlerMethod(new TestController(), method));
    +		TestController controller = new TestController();
    +		return ResolvableMethod.on(controller).name(name).resolveHandlerMethod();
     	}
     
     	private void addResolver(InvocableHandlerMethod handlerMethod, Mono resolvedValue) {
    diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java
    index 3d1955007f..000a8432d3 100644
    --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java
    +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java
    @@ -52,6 +52,7 @@ import org.springframework.web.bind.annotation.RequestMethod;
     import org.springframework.web.method.HandlerMethod;
     import org.springframework.web.reactive.HandlerMapping;
     import org.springframework.web.reactive.HandlerResult;
    +import org.springframework.web.reactive.result.ResolvableMethod;
     import org.springframework.web.reactive.result.method.RequestMappingInfo.BuilderConfiguration;
     import org.springframework.web.server.MethodNotAllowedException;
     import org.springframework.web.server.NotAcceptableStatusException;
    @@ -69,44 +70,31 @@ import static org.junit.Assert.assertNotNull;
     import static org.junit.Assert.assertNull;
     import static org.junit.Assert.assertThat;
     import static org.junit.Assert.assertTrue;
    -
    +import static org.springframework.web.bind.annotation.RequestMethod.GET;
    +import static org.springframework.web.bind.annotation.RequestMethod.HEAD;
    +import static org.springframework.web.bind.annotation.RequestMethod.OPTIONS;
    +import static org.springframework.web.reactive.result.method.RequestMappingInfo.paths;
     
     /**
      * Unit tests for {@link RequestMappingInfoHandlerMapping}.
    - *
      * @author Rossen Stoyanchev
      */
     public class RequestMappingInfoHandlerMappingTests {
     
     	private TestRequestMappingInfoHandlerMapping handlerMapping;
     
    -	private HandlerMethod fooMethod;
    -
    -	private HandlerMethod fooParamMethod;
    -
    -	private HandlerMethod barMethod;
    -
    -	private HandlerMethod emptyMethod;
    -
     
     	@Before
     	public void setUp() throws Exception {
    -		TestController testController = new TestController();
    -
    -		this.fooMethod = new HandlerMethod(testController, "foo");
    -		this.fooParamMethod = new HandlerMethod(testController, "fooParam");
    -		this.barMethod = new HandlerMethod(testController, "bar");
    -		this.emptyMethod = new HandlerMethod(testController, "empty");
    -
     		this.handlerMapping = new TestRequestMappingInfoHandlerMapping();
    -		this.handlerMapping.registerHandler(testController);
    +		this.handlerMapping.registerHandler(new TestController());
     	}
     
     
     	@Test
     	public void getMappingPathPatterns() throws Exception {
     		String[] patterns = {"/foo/*", "/foo", "/bar/*", "/bar"};
    -		RequestMappingInfo info = RequestMappingInfo.paths(patterns).build();
    +		RequestMappingInfo info = paths(patterns).build();
     		Set actual = this.handlerMapping.getMappingPathPatterns(info);
     
     		assertEquals(new HashSet<>(Arrays.asList(patterns)), actual);
    @@ -114,38 +102,53 @@ public class RequestMappingInfoHandlerMappingTests {
     
     	@Test
     	public void getHandlerDirectMatch() throws Exception {
    +		String[] patterns = new String[] {"/foo"};
    +		String[] params = new String[] {};
    +		Method expected = resolveMethod(new TestController(), patterns, null, params);
    +
     		ServerWebExchange exchange = createExchange(HttpMethod.GET, "/foo");
    -		HandlerMethod handlerMethod = getHandler(exchange);
    -		assertEquals(this.fooMethod.getMethod(), handlerMethod.getMethod());
    +		HandlerMethod hm = (HandlerMethod) this.handlerMapping.getHandler(exchange).block();
    +
    +		assertEquals(expected, hm.getMethod());
     	}
     
     	@Test
     	public void getHandlerGlobMatch() throws Exception {
    +		String[] patterns = new String[] {"/ba*"};
    +		RequestMethod[] methods = new RequestMethod[] {GET, HEAD};
    +		Method expected = resolveMethod(new TestController(), patterns, methods, null);
    +
     		ServerWebExchange exchange = createExchange(HttpMethod.GET, "/bar");
    -		HandlerMethod handlerMethod = getHandler(exchange);
    -		assertEquals(this.barMethod.getMethod(), handlerMethod.getMethod());
    +		HandlerMethod hm = (HandlerMethod) this.handlerMapping.getHandler(exchange).block();
    +
    +		assertEquals(expected, hm.getMethod());
     	}
     
     	@Test
     	public void getHandlerEmptyPathMatch() throws Exception {
    -		ServerWebExchange exchange = createExchange(HttpMethod.GET, "");
    -		HandlerMethod handlerMethod = getHandler(exchange);
    +		String[] patterns = new String[] {""};
    +		Method expected = resolveMethod(new TestController(), patterns, null, null);
     
    -		assertEquals(this.emptyMethod.getMethod(), handlerMethod.getMethod());
    +		ServerWebExchange exchange = createExchange(HttpMethod.GET, "");
    +		HandlerMethod hm = (HandlerMethod) this.handlerMapping.getHandler(exchange).block();
    +		assertEquals(expected, hm.getMethod());
     
     		exchange = createExchange(HttpMethod.GET, "/");
    -		handlerMethod = getHandler(exchange);
    -
    -		assertEquals(this.emptyMethod.getMethod(), handlerMethod.getMethod());
    +		hm = (HandlerMethod) this.handlerMapping.getHandler(exchange).block();
    +		assertEquals(expected, hm.getMethod());
     	}
     
     	@Test
     	public void getHandlerBestMatch() throws Exception {
    +		String[] patterns = new String[] {"/foo"};
    +		String[] params = new String[] {"p"};
    +		Method expected = resolveMethod(new TestController(), patterns, null, params);
    +
     		ServerWebExchange exchange = createExchange(HttpMethod.GET, "/foo");
     		exchange.getRequest().getQueryParams().add("p", "anything");
    -		HandlerMethod handlerMethod = getHandler(exchange);
    +		HandlerMethod hm = (HandlerMethod) this.handlerMapping.getHandler(exchange).block();
     
    -		assertEquals(this.fooParamMethod.getMethod(), handlerMethod.getMethod());
    +		assertEquals(expected, hm.getMethod());
     	}
     
     	@Test
    @@ -179,24 +182,21 @@ public class RequestMappingInfoHandlerMappingTests {
     		ServerWebExchange exchange = createExchange(HttpMethod.PUT, "/person/1");
     		exchange.getRequest().getHeaders().add("Content-Type", "bogus");
     		Mono mono = this.handlerMapping.getHandler(exchange);
    +
     		assertError(mono, UnsupportedMediaTypeStatusException.class,
     				ex -> assertEquals("Request failure [status: 415, " +
     						"reason: \"Invalid mime type \"bogus\": does not contain '/'\"]",
     						ex.getMessage()));
     	}
     
    -	// SPR-8462
    -
    -	@Test
    +	@Test // SPR-8462
     	public void getHandlerTestMediaTypeNotAcceptable() throws Exception {
     		testMediaTypeNotAcceptable("/persons");
     		testMediaTypeNotAcceptable("/persons/");
     		testMediaTypeNotAcceptable("/persons.json");
     	}
     
    -	// SPR-12854
    -
    -	@Test
    +	@Test // SPR-12854
     	public void getHandlerTestRequestParamMismatch() throws Exception {
     		ServerWebExchange exchange = createExchange(HttpMethod.GET, "/params");
     		Mono mono = this.handlerMapping.getHandler(exchange);
    @@ -218,23 +218,22 @@ public class RequestMappingInfoHandlerMappingTests {
     	public void getHandlerProducibleMediaTypesAttribute() throws Exception {
     		ServerWebExchange exchange = createExchange(HttpMethod.GET, "/content");
     		exchange.getRequest().getHeaders().setAccept(Collections.singletonList(MediaType.APPLICATION_XML));
    -		getHandler(exchange);
    +		this.handlerMapping.getHandler(exchange).block();
     
     		String name = HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE;
     		assertEquals(Collections.singleton(MediaType.APPLICATION_XML), exchange.getAttributes().get(name));
     
     		exchange = createExchange(HttpMethod.GET, "/content");
     		exchange.getRequest().getHeaders().setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
    -		getHandler(exchange);
    +		this.handlerMapping.getHandler(exchange).block();
     
     		assertNull("Negated expression shouldn't be listed as producible type",
     				exchange.getAttributes().get(name));
     	}
     
    -	@SuppressWarnings("unchecked")
    -	@Test
    +	@Test @SuppressWarnings("unchecked")
     	public void handleMatchUriTemplateVariables() throws Exception {
    -		RequestMappingInfo key = RequestMappingInfo.paths("/{path1}/{path2}").build();
    +		RequestMappingInfo key = paths("/{path1}/{path2}").build();
     		ServerWebExchange exchange = createExchange(HttpMethod.GET, "/1/2");
     		String lookupPath = exchange.getRequest().getURI().getPath();
     		this.handlerMapping.handleMatch(key, lookupPath, exchange);
    @@ -247,11 +246,9 @@ public class RequestMappingInfoHandlerMappingTests {
     		assertEquals("2", uriVariables.get("path2"));
     	}
     
    -	// SPR-9098
    -
    -	@Test
    +	@Test // SPR-9098
     	public void handleMatchUriTemplateVariablesDecode() throws Exception {
    -		RequestMappingInfo key = RequestMappingInfo.paths("/{group}/{identifier}").build();
    +		RequestMappingInfo key = paths("/{group}/{identifier}").build();
     		ServerWebExchange exchange = createExchange(HttpMethod.GET, "/group/a%2Fb");
     
     		HttpRequestPathHelper pathHelper = new HttpRequestPathHelper();
    @@ -272,7 +269,7 @@ public class RequestMappingInfoHandlerMappingTests {
     
     	@Test
     	public void handleMatchBestMatchingPatternAttribute() throws Exception {
    -		RequestMappingInfo key = RequestMappingInfo.paths("/{path1}/2", "/**").build();
    +		RequestMappingInfo key = paths("/{path1}/2", "/**").build();
     		ServerWebExchange exchange = createExchange(HttpMethod.GET, "/1/2");
     		this.handlerMapping.handleMatch(key, "/1/2", exchange);
     
    @@ -281,7 +278,7 @@ public class RequestMappingInfoHandlerMappingTests {
     
     	@Test
     	public void handleMatchBestMatchingPatternAttributeNoPatternsDefined() throws Exception {
    -		RequestMappingInfo key = RequestMappingInfo.paths().build();
    +		RequestMappingInfo key = paths().build();
     		ServerWebExchange exchange = createExchange(HttpMethod.GET, "/1/2");
     
     		this.handlerMapping.handleMatch(key, "/1/2", exchange);
    @@ -333,8 +330,7 @@ public class RequestMappingInfoHandlerMappingTests {
     	public void handleMatchMatrixVariablesDecoding() throws Exception {
     		HttpRequestPathHelper urlPathHelper = new HttpRequestPathHelper();
     		urlPathHelper.setUrlDecode(false);
    -
    -		this.handlerMapping.setPathHelper(urlPathHelper );
    +		this.handlerMapping.setPathHelper(urlPathHelper);
     
     		ServerWebExchange exchange = createExchange(HttpMethod.GET, "/");
     		handleMatch(exchange, "/path{filter}", "/path;mvar=a%2fb");
    @@ -365,11 +361,6 @@ public class RequestMappingInfoHandlerMappingTests {
     				});
     	}
     
    -	@SuppressWarnings("ConstantConditions")
    -	private HandlerMethod getHandler(ServerWebExchange exchange) throws Exception {
    -		Mono handler = this.handlerMapping.getHandler(exchange);
    -		return (HandlerMethod) handler.block();
    -	}
     
     	private void testHttpMediaTypeNotSupportedException(String url) throws Exception {
     		ServerWebExchange exchange = createExchange(HttpMethod.PUT, url);
    @@ -384,7 +375,7 @@ public class RequestMappingInfoHandlerMappingTests {
     
     	private void testHttpOptions(String requestURI, String allowHeader) throws Exception {
     		ServerWebExchange exchange = createExchange(HttpMethod.OPTIONS, requestURI);
    -		HandlerMethod handlerMethod = getHandler(exchange);
    +		HandlerMethod handlerMethod = (HandlerMethod) this.handlerMapping.getHandler(exchange).block();
     
     		ModelMap model = new ExtendedModelMap();
     		Mono mono = new InvocableHandlerMethod(handlerMethod).invokeForRequest(exchange, model);
    @@ -410,7 +401,7 @@ public class RequestMappingInfoHandlerMappingTests {
     	}
     
     	private void handleMatch(ServerWebExchange exchange, String pattern, String lookupPath) {
    -		RequestMappingInfo info = RequestMappingInfo.paths(pattern).build();
    +		RequestMappingInfo info = paths(pattern).build();
     		this.handlerMapping.handleMatch(info, lookupPath, exchange);
     	}
     
    @@ -426,6 +417,29 @@ public class RequestMappingInfoHandlerMappingTests {
     		return (Map) exchange.getAttributes().get(attrName);
     	}
     
    +	private Method resolveMethod(Object controller, String[] patterns,
    +			RequestMethod[] methods, String[] params) {
    +
    +		return ResolvableMethod.on(controller)
    +				.matching(method -> {
    +					RequestMapping annot = AnnotatedElementUtils.findMergedAnnotation(method, RequestMapping.class);
    +					if (annot == null) {
    +						return false;
    +					}
    +					else if (patterns != null && !Arrays.equals(annot.path(), patterns)) {
    +						return false;
    +					}
    +					else if (methods != null && !Arrays.equals(annot.method(), methods)) {
    +						return false;
    +					}
    +					else if (params != null && (!Arrays.equals(annot.params(), params))) {
    +						return false;
    +					}
    +					return true;
    +				})
    +				.resolve();
    +	}
    +
     
     	@SuppressWarnings("unused")
     	@Controller
    @@ -439,11 +453,11 @@ public class RequestMappingInfoHandlerMappingTests {
     		public void fooParam() {
     		}
     
    -		@RequestMapping(path = "/ba*", method = { RequestMethod.GET, RequestMethod.HEAD })
    +		@RequestMapping(path = "/ba*", method = { GET, HEAD })
     		public void bar() {
     		}
     
    -		@RequestMapping(value = "")
    +		@RequestMapping(path = "")
     		public void empty() {
     		}
     
    @@ -451,32 +465,32 @@ public class RequestMappingInfoHandlerMappingTests {
     		public void consumes(@RequestBody String text) {
     		}
     
    -		@RequestMapping(value = "/persons", produces="application/xml")
    +		@RequestMapping(path = "/persons", produces="application/xml")
     		public String produces() {
     			return "";
     		}
     
    -		@RequestMapping(value = "/params", params="foo=bar")
    +		@RequestMapping(path = "/params", params="foo=bar")
     		public String param() {
     			return "";
     		}
     
    -		@RequestMapping(value = "/params", params="bar=baz")
    +		@RequestMapping(path = "/params", params="bar=baz")
     		public String param2() {
     			return "";
     		}
     
    -		@RequestMapping(value = "/content", produces="application/xml")
    +		@RequestMapping(path = "/content", produces="application/xml")
     		public String xmlContent() {
     			return "";
     		}
     
    -		@RequestMapping(value = "/content", produces="!application/xml")
    +		@RequestMapping(path = "/content", produces="!application/xml")
     		public String nonXmlContent() {
     			return "";
     		}
     
    -		@RequestMapping(value = "/something", method = RequestMethod.OPTIONS)
    +		@RequestMapping(path = "/something", method = OPTIONS)
     		public HttpHeaders fooOptions() {
     			HttpHeaders headers = new HttpHeaders();
     			headers.add("Allow", "PUT,POST");
    @@ -499,7 +513,7 @@ public class RequestMappingInfoHandlerMappingTests {
     
     	private static class TestRequestMappingInfoHandlerMapping extends RequestMappingInfoHandlerMapping {
     
    -		public void registerHandler(Object handler) {
    +		void registerHandler(Object handler) {
     			super.detectHandlerMethods(handler);
     		}
     
    @@ -517,7 +531,7 @@ public class RequestMappingInfoHandlerMappingTests {
     				options.setPathMatcher(getPathMatcher());
     				options.setSuffixPatternMatch(true);
     				options.setTrailingSlashMatch(true);
    -				return RequestMappingInfo.paths(annot.value()).methods(annot.method())
    +				return paths(annot.value()).methods(annot.method())
     						.params(annot.params()).headers(annot.headers())
     						.consumes(annot.consumes()).produces(annot.produces())
     						.options(options).build();
    diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/HttpEntityArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/HttpEntityArgumentResolverTests.java
    index 0df938c38b..86085d67e4 100644
    --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/HttpEntityArgumentResolverTests.java
    +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/HttpEntityArgumentResolverTests.java
    @@ -73,13 +73,13 @@ import static org.springframework.core.ResolvableType.forClassWithGenerics;
      */
     public class HttpEntityArgumentResolverTests {
     
    -	private HttpEntityArgumentResolver resolver = resolver();
    +	private HttpEntityArgumentResolver resolver = createResolver();
     
     	private ServerWebExchange exchange;
     
     	private MockServerHttpRequest request;
     
    -	private ResolvableMethod testMethod = ResolvableMethod.on(getClass()).name("handle");
    +	private ResolvableMethod testMethod = ResolvableMethod.onClass(getClass()).name("handle");
     
     
     	@Before
    @@ -89,15 +89,26 @@ public class HttpEntityArgumentResolverTests {
     		this.exchange = new DefaultServerWebExchange(this.request, response, new MockWebSessionManager());
     	}
     
    +	private HttpEntityArgumentResolver createResolver() {
    +		List> converters = new ArrayList<>();
    +		converters.add(new CodecHttpMessageConverter<>(new StringDecoder()));
    +
    +		FormattingConversionService service = new DefaultFormattingConversionService();
    +		service.addConverter(new MonoToCompletableFutureConverter());
    +		service.addConverter(new ReactorToRxJava1Converter());
    +
    +		return new HttpEntityArgumentResolver(converters, service);
    +	}
    +
     
     	@Test
     	public void supports() throws Exception {
    -		testSupports(httpEntity(String.class));
    -		testSupports(httpEntity(forClassWithGenerics(Mono.class, String.class)));
    -		testSupports(httpEntity(forClassWithGenerics(Single.class, String.class)));
    -		testSupports(httpEntity(forClassWithGenerics(CompletableFuture.class, String.class)));
    -		testSupports(httpEntity(forClassWithGenerics(Flux.class, String.class)));
    -		testSupports(httpEntity(forClassWithGenerics(Observable.class, String.class)));
    +		testSupports(httpEntityType(String.class));
    +		testSupports(httpEntityType(forClassWithGenerics(Mono.class, String.class)));
    +		testSupports(httpEntityType(forClassWithGenerics(Single.class, String.class)));
    +		testSupports(httpEntityType(forClassWithGenerics(CompletableFuture.class, String.class)));
    +		testSupports(httpEntityType(forClassWithGenerics(Flux.class, String.class)));
    +		testSupports(httpEntityType(forClassWithGenerics(Observable.class, String.class)));
     		testSupports(forClassWithGenerics(RequestEntity.class, String.class));
     	}
     
    @@ -112,7 +123,7 @@ public class HttpEntityArgumentResolverTests {
     
     	@Test
     	public void emptyBodyWithString() throws Exception {
    -		ResolvableType type = httpEntity(String.class);
    +		ResolvableType type = httpEntityType(String.class);
     		HttpEntity entity = resolveValueWithEmptyBody(type);
     
     		assertNull(entity.getBody());
    @@ -120,7 +131,7 @@ public class HttpEntityArgumentResolverTests {
     
     	@Test
     	public void emptyBodyWithMono() throws Exception {
    -		ResolvableType type = httpEntity(forClassWithGenerics(Mono.class, String.class));
    +		ResolvableType type = httpEntityType(forClassWithGenerics(Mono.class, String.class));
     		HttpEntity> entity = resolveValueWithEmptyBody(type);
     
     		TestSubscriber.subscribe(entity.getBody())
    @@ -131,7 +142,7 @@ public class HttpEntityArgumentResolverTests {
     
     	@Test
     	public void emptyBodyWithFlux() throws Exception {
    -		ResolvableType type = httpEntity(forClassWithGenerics(Flux.class, String.class));
    +		ResolvableType type = httpEntityType(forClassWithGenerics(Flux.class, String.class));
     		HttpEntity> entity = resolveValueWithEmptyBody(type);
     
     		TestSubscriber.subscribe(entity.getBody())
    @@ -142,7 +153,7 @@ public class HttpEntityArgumentResolverTests {
     
     	@Test
     	public void emptyBodyWithSingle() throws Exception {
    -		ResolvableType type = httpEntity(forClassWithGenerics(Single.class, String.class));
    +		ResolvableType type = httpEntityType(forClassWithGenerics(Single.class, String.class));
     		HttpEntity> entity = resolveValueWithEmptyBody(type);
     
     		TestSubscriber.subscribe(RxJava1SingleConverter.from(entity.getBody()))
    @@ -152,7 +163,7 @@ public class HttpEntityArgumentResolverTests {
     
     	@Test
     	public void emptyBodyWithObservable() throws Exception {
    -		ResolvableType type = httpEntity(forClassWithGenerics(Observable.class, String.class));
    +		ResolvableType type = httpEntityType(forClassWithGenerics(Observable.class, String.class));
     		HttpEntity> entity = resolveValueWithEmptyBody(type);
     
     		TestSubscriber.subscribe(RxJava1ObservableConverter.from(entity.getBody()))
    @@ -163,7 +174,7 @@ public class HttpEntityArgumentResolverTests {
     
     	@Test
     	public void emptyBodyWithCompletableFuture() throws Exception {
    -		ResolvableType type = httpEntity(forClassWithGenerics(CompletableFuture.class, String.class));
    +		ResolvableType type = httpEntityType(forClassWithGenerics(CompletableFuture.class, String.class));
     		HttpEntity> entity = resolveValueWithEmptyBody(type);
     
     		entity.getBody().whenComplete((body, ex) -> {
    @@ -175,7 +186,7 @@ public class HttpEntityArgumentResolverTests {
     	@Test
     	public void httpEntityWithStringBody() throws Exception {
     		String body = "line1";
    -		ResolvableType type = httpEntity(String.class);
    +		ResolvableType type = httpEntityType(String.class);
     		HttpEntity httpEntity = resolveValue(type, body);
     
     		assertEquals(this.request.getHeaders(), httpEntity.getHeaders());
    @@ -185,7 +196,7 @@ public class HttpEntityArgumentResolverTests {
     	@Test
     	public void httpEntityWithMonoBody() throws Exception {
     		String body = "line1";
    -		ResolvableType type = httpEntity(forClassWithGenerics(Mono.class, String.class));
    +		ResolvableType type = httpEntityType(forClassWithGenerics(Mono.class, String.class));
     		HttpEntity> httpEntity = resolveValue(type, body);
     
     		assertEquals(this.request.getHeaders(), httpEntity.getHeaders());
    @@ -195,7 +206,7 @@ public class HttpEntityArgumentResolverTests {
     	@Test
     	public void httpEntityWithSingleBody() throws Exception {
     		String body = "line1";
    -		ResolvableType type = httpEntity(forClassWithGenerics(Single.class, String.class));
    +		ResolvableType type = httpEntityType(forClassWithGenerics(Single.class, String.class));
     		HttpEntity> httpEntity = resolveValue(type, body);
     
     		assertEquals(this.request.getHeaders(), httpEntity.getHeaders());
    @@ -205,7 +216,7 @@ public class HttpEntityArgumentResolverTests {
     	@Test
     	public void httpEntityWithCompletableFutureBody() throws Exception {
     		String body = "line1";
    -		ResolvableType type = httpEntity(forClassWithGenerics(CompletableFuture.class, String.class));
    +		ResolvableType type = httpEntityType(forClassWithGenerics(CompletableFuture.class, String.class));
     		HttpEntity> httpEntity = resolveValue(type, body);
     
     		assertEquals(this.request.getHeaders(), httpEntity.getHeaders());
    @@ -215,7 +226,7 @@ public class HttpEntityArgumentResolverTests {
     	@Test
     	public void httpEntityWithFluxBody() throws Exception {
     		String body = "line1\nline2\nline3\n";
    -		ResolvableType type = httpEntity(forClassWithGenerics(Flux.class, String.class));
    +		ResolvableType type = httpEntityType(forClassWithGenerics(Flux.class, String.class));
     		HttpEntity> httpEntity = resolveValue(type, body);
     
     		assertEquals(this.request.getHeaders(), httpEntity.getHeaders());
    @@ -235,25 +246,14 @@ public class HttpEntityArgumentResolverTests {
     	}
     
     
    -	private ResolvableType httpEntity(Class bodyType) {
    -		return httpEntity(ResolvableType.forClass(bodyType));
    +	private ResolvableType httpEntityType(Class bodyType) {
    +		return httpEntityType(ResolvableType.forClass(bodyType));
     	}
     
    -	private ResolvableType httpEntity(ResolvableType type) {
    +	private ResolvableType httpEntityType(ResolvableType type) {
     		return forClassWithGenerics(HttpEntity.class, type);
     	}
     
    -	private HttpEntityArgumentResolver resolver() {
    -		List> converters = new ArrayList<>();
    -		converters.add(new CodecHttpMessageConverter<>(new StringDecoder()));
    -
    -		FormattingConversionService service = new DefaultFormattingConversionService();
    -		service.addConverter(new MonoToCompletableFutureConverter());
    -		service.addConverter(new ReactorToRxJava1Converter());
    -
    -		return new HttpEntityArgumentResolver(converters, service);
    -	}
    -
     	private void testSupports(ResolvableType type) {
     		MethodParameter parameter = this.testMethod.resolveParam(type);
     		assertTrue(this.resolver.supportsParameter(parameter));
    diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterArgumentResolverTests.java
    index 18b6c582a8..a2590f8a08 100644
    --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterArgumentResolverTests.java
    +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterArgumentResolverTests.java
    @@ -83,7 +83,7 @@ public class MessageConverterArgumentResolverTests {
     
     	private MockServerHttpRequest request;
     
    -	private ResolvableMethod testMethod = ResolvableMethod.on(this.getClass()).name("handle");
    +	private ResolvableMethod testMethod = ResolvableMethod.onClass(this.getClass()).name("handle");
     
     
     	@Before
    @@ -252,7 +252,7 @@ public class MessageConverterArgumentResolverTests {
     	@Ignore
     	public void parameterizedMethodArgument() throws Exception {
     		Class clazz = ConcreteParameterizedController.class;
    -		MethodParameter param = ResolvableMethod.on(clazz).name("handleDto").resolveParam();
    +		MethodParameter param = ResolvableMethod.onClass(clazz).name("handleDto").resolveParam();
     		SimpleBean simpleBean = resolveValue(param, "{\"name\" : \"Jad\"}");
     
     		assertEquals("Jad", simpleBean.getName());
    diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterResultHandlerTests.java
    index cde0c86a90..256ea69891 100644
    --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterResultHandlerTests.java
    +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageConverterResultHandlerTests.java
    @@ -172,7 +172,7 @@ public class MessageConverterResultHandlerTests {
     
     
     	private MethodParameter returnType(ResolvableType bodyType) {
    -		return ResolvableMethod.on(TestController.class).returning(bodyType).resolveReturnType();
    +		return ResolvableMethod.onClass(TestController.class).returning(bodyType).resolveReturnType();
     	}
     
     	private AbstractMessageConverterResultHandler createResultHandler(HttpMessageConverter... converters) {
    diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java
    index 3300637bb1..ecd98007eb 100644
    --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java
    +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java
    @@ -79,7 +79,7 @@ public class RequestBodyArgumentResolverTests {
     
     	private MockServerHttpRequest request;
     
    -	private ResolvableMethod testMethod = ResolvableMethod.on(this.getClass()).name("handle");
    +	private ResolvableMethod testMethod = ResolvableMethod.onClass(this.getClass()).name("handle");
     
     
     	@Before
    @@ -89,6 +89,17 @@ public class RequestBodyArgumentResolverTests {
     		this.exchange = new DefaultServerWebExchange(this.request, response, new MockWebSessionManager());
     	}
     
    +	private RequestBodyArgumentResolver resolver() {
    +		List> converters = new ArrayList<>();
    +		converters.add(new CodecHttpMessageConverter<>(new StringDecoder()));
    +
    +		FormattingConversionService service = new DefaultFormattingConversionService();
    +		service.addConverter(new MonoToCompletableFutureConverter());
    +		service.addConverter(new ReactorToRxJava1Converter());
    +
    +		return new RequestBodyArgumentResolver(converters, service);
    +	}
    +
     
     	@Test
     	public void supports() throws Exception {
    @@ -197,17 +208,6 @@ public class RequestBodyArgumentResolverTests {
     	}
     
     
    -	private RequestBodyArgumentResolver resolver() {
    -		List> converters = new ArrayList<>();
    -		converters.add(new CodecHttpMessageConverter<>(new StringDecoder()));
    -
    -		FormattingConversionService service = new DefaultFormattingConversionService();
    -		service.addConverter(new MonoToCompletableFutureConverter());
    -		service.addConverter(new ReactorToRxJava1Converter());
    -
    -		return new RequestBodyArgumentResolver(converters, service);
    -	}
    -
     	private  T resolveValue(MethodParameter param, String body) {
     		this.request.writeWith(Flux.just(dataBuffer(body)));
     		Mono result = this.resolver.readBody(param, true, this.exchange);
    @@ -221,9 +221,9 @@ public class RequestBodyArgumentResolverTests {
     		return (T) value;
     	}
     
    -	private  T resolveValueWithEmptyBody(ResolvableType type, boolean required) {
    +	private  T resolveValueWithEmptyBody(ResolvableType bodyType, boolean isRequired) {
     		this.request.writeWith(Flux.empty());
    -		MethodParameter param = this.testMethod.resolveParam(type, requestBody(required));
    +		MethodParameter param = this.testMethod.resolveParam(bodyType, requestBody(isRequired));
     		Mono result = this.resolver.resolveArgument(param, new ExtendedModelMap(), this.exchange);
     		Object value = result.block(Duration.ofSeconds(5));
     
    diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java
    index 93860f2a16..22a878cbea 100644
    --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java
    +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java
    @@ -199,7 +199,7 @@ public class ResponseEntityResultHandlerTests {
     	}
     
     	private HandlerResult handlerResult(Object returnValue, ResolvableType type) {
    -		MethodParameter param = ResolvableMethod.on(TestController.class).returning(type).resolveReturnType();
    +		MethodParameter param = ResolvableMethod.onClass(TestController.class).returning(type).resolveReturnType();
     		return new HandlerResult(new TestController(), returnValue, param);
     	}
     
    diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/HttpMessageConverterViewTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/HttpMessageConverterViewTests.java
    index 4fb874abc5..7af0bb3849 100644
    --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/HttpMessageConverterViewTests.java
    +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/HttpMessageConverterViewTests.java
    @@ -72,7 +72,7 @@ public class HttpMessageConverterViewTests {
     
     	@Before
     	public void setup() throws Exception {
    -		MethodParameter param = ResolvableMethod.on(this.getClass()).name("handle").resolveReturnType();
    +		MethodParameter param = ResolvableMethod.onClass(this.getClass()).name("handle").resolveReturnType();
     		this.result = new HandlerResult(this, null, param, this.model);
     	}
     
    diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java
    index a06c70cd23..61f9720a28 100644
    --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java
    +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java
    @@ -104,7 +104,7 @@ public class ViewResolutionResultHandlerTests {
     		testSupports(ResolvableType.forClass(TestBean.class), true);
     		testSupports(ResolvableType.forClass(Integer.class), false);
     
    -		testSupports(ResolvableMethod.on(TestController.class).annotated(ModelAttribute.class), true);
    +		testSupports(ResolvableMethod.onClass(TestController.class).annotated(ModelAttribute.class), true);
     	}
     
     	@Test
    @@ -153,7 +153,7 @@ public class ViewResolutionResultHandlerTests {
     		String responseBody = "account: {id=123, testBean=TestBean[name=Joe]}";
     		testHandle("/account", returnType, returnValue, responseBody, resolver);
     
    -		testHandle("/account", ResolvableMethod.on(TestController.class).annotated(ModelAttribute.class),
    +		testHandle("/account", ResolvableMethod.onClass(TestController.class).annotated(ModelAttribute.class),
     				99L, "account: {id=123, num=99}", resolver);
     	}
     
    @@ -239,7 +239,7 @@ public class ViewResolutionResultHandlerTests {
     
     
     	private MethodParameter returnType(ResolvableType type) {
    -		return ResolvableMethod.on(TestController.class).returning(type).resolveReturnType();
    +		return ResolvableMethod.onClass(TestController.class).returning(type).resolveReturnType();
     	}
     
     	private ViewResolutionResultHandler createResultHandler(ViewResolver... resolvers) {
    @@ -258,7 +258,7 @@ public class ViewResolutionResultHandlerTests {
     	}
     
     	private void testSupports(ResolvableType type, boolean result) {
    -		testSupports(ResolvableMethod.on(TestController.class).returning(type), result);
    +		testSupports(ResolvableMethod.onClass(TestController.class).returning(type), result);
     	}
     
     	private void testSupports(ResolvableMethod resolvableMethod, boolean result) {
    @@ -271,7 +271,7 @@ public class ViewResolutionResultHandlerTests {
     	private void testHandle(String path, ResolvableType returnType, Object returnValue,
     			String responseBody, ViewResolver... resolvers) throws URISyntaxException {
     
    -		testHandle(path,  ResolvableMethod.on(TestController.class).returning(returnType),
    +		testHandle(path,  ResolvableMethod.onClass(TestController.class).returning(returnType),
     				returnValue, responseBody, resolvers);
     	}
     
    
    From d68232c880f770466993b4de7e66ebe958df9e7a Mon Sep 17 00:00:00 2001
    From: Violeta Georgieva 
    Date: Thu, 7 Jul 2016 15:21:58 +0300
    Subject: [PATCH 480/498] Refactor AbstractRequestBodyPublisher states
    
    The state machine is refactored in order to solve various concurrency
    issues.
    ---
     .../AbstractRequestBodyPublisher.java         | 126 +++++++++---------
     .../reactive/ServletHttpHandlerAdapter.java   |  21 ++-
     .../reactive/UndertowServerHttpRequest.java   |   5 +
     3 files changed, 84 insertions(+), 68 deletions(-)
    
    diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractRequestBodyPublisher.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractRequestBodyPublisher.java
    index 049b3c837b..2e0a7359c0 100644
    --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractRequestBodyPublisher.java
    +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractRequestBodyPublisher.java
    @@ -52,8 +52,6 @@ abstract class AbstractRequestBodyPublisher implements Publisher {
     
     	private Subscriber subscriber;
     
    -	private volatile boolean dataAvailable;
    -
     	@Override
     	public void subscribe(Subscriber subscriber) {
     		if (this.logger.isTraceEnabled()) {
    @@ -101,28 +99,24 @@ abstract class AbstractRequestBodyPublisher implements Publisher {
     	/**
     	 * Reads and publishes data buffers from the input. Continues till either there is no
     	 * more demand, or till there is no more data to be read.
    -	 * @return {@code true} if there is more data to be read; {@code false} otherwise
    +	 * @return {@code true} if there is more demand; {@code false} otherwise
     	 */
    -	private boolean readAndPublish() {
    -		try {
    -			while (hasDemand()) {
    -				DataBuffer dataBuffer = read();
    -				if (dataBuffer != null) {
    -					BackpressureUtils.getAndSub(this.demand, 1L);
    -					this.subscriber.onNext(dataBuffer);
    -				}
    -				else {
    -					return false;
    -				}
    +	private boolean readAndPublish() throws IOException {
    +		while (hasDemand()) {
    +			DataBuffer dataBuffer = read();
    +			if (dataBuffer != null) {
    +				BackpressureUtils.getAndSub(this.demand, 1L);
    +				this.subscriber.onNext(dataBuffer);
    +			}
    +			else {
    +				return true;
     			}
    -			return true;
    -		}
    -		catch (IOException ex) {
    -			onError(ex);
    -			return false;
     		}
    +		return false;
     	}
     
    +	protected abstract void checkOnDataAvailable();
    +
     	/**
     	 * Reads a data buffer from the input, if possible. Returns {@code null} if a buffer
     	 * could not be read.
    @@ -182,10 +176,13 @@ abstract class AbstractRequestBodyPublisher implements Publisher {
     	 *       UNSUBSCRIBED
     	 *        |
     	 *        v
    -	 * DATA_UNAVAILABLE <---> DATA_AVAILABLE
    -	 *                |       |
    -	 *                v       v
    -	 *                COMPLETED
    +	 * NO_DEMAND -------------------> DEMAND
    +	 *    |    ^                      ^    |
    +	 *    |    |                      |    |
    +	 *    |    --------- READING <-----    |
    +	 *    |                 |              |
    +	 *    |                 v              |
    +	 *    ------------> COMPLETED <---------
     	 * 
     	 * Refer to the individual states for more information.
     	 */
    @@ -194,16 +191,14 @@ abstract class AbstractRequestBodyPublisher implements Publisher {
     		/**
     		 * The initial unsubscribed state. Will respond to {@link
     		 * #subscribe(AbstractRequestBodyPublisher, Subscriber)} by
    -		 * changing state to {@link #DATA_UNAVAILABLE}.
    +		 * changing state to {@link #NO_DEMAND}.
     		 */
     		UNSUBSCRIBED {
     			@Override
     			void subscribe(AbstractRequestBodyPublisher publisher,
     					Subscriber subscriber) {
     				Objects.requireNonNull(subscriber);
    -				State newState =
    -						publisher.dataAvailable ? DATA_AVAILABLE : DATA_UNAVAILABLE;
    -				if (publisher.changeState(this, newState)) {
    +				if (publisher.changeState(this, NO_DEMAND)) {
     					Subscription subscription = new RequestBodySubscription(
     									publisher);
     					publisher.subscriber = subscriber;
    @@ -213,54 +208,55 @@ abstract class AbstractRequestBodyPublisher implements Publisher {
     					throw new IllegalStateException(toString());
     				}
     			}
    -
    -			@Override
    -			void onDataAvailable(AbstractRequestBodyPublisher publisher) {
    -				publisher.dataAvailable = true;
    -			}
     		},
     		/**
    -		 * State that gets entered when there is no data to be read. Responds to {@link
    -		 * #request(AbstractRequestBodyPublisher, long)} by increasing the demand, and
    -		 * responds to {@link #onDataAvailable(AbstractRequestBodyPublisher)} by
    -		 * reading the available data and changing state to {@link #DATA_AVAILABLE} if
    -		 * there continues to be more data available after the demand has been satisfied.
    +		 * State that gets entered when there is no demand. Responds to {@link
    +		 * #request(AbstractRequestBodyPublisher, long)} by increasing the demand,
    +		 * changing state to {@link #DEMAND} and will check whether there
    +		 * is data available for reading.
     		 */
    -		DATA_UNAVAILABLE {
    +		NO_DEMAND {
     			@Override
     			void request(AbstractRequestBodyPublisher publisher, long n) {
     				if (BackpressureUtils.checkRequest(n, publisher.subscriber)) {
     					BackpressureUtils.addAndGet(publisher.demand, n);
    -				}
    -			}
    -
    -			@Override
    -			void onDataAvailable(AbstractRequestBodyPublisher publisher) {
    -				boolean dataAvailable = publisher.readAndPublish();
    -				if (dataAvailable) {
    -					publisher.changeState(this, DATA_AVAILABLE);
    -				}
    -			}
    -
    -		},
    -		/**
    -		 * State that gets entered when there is data to be read. Responds to {@link
    -		 * #request(AbstractRequestBodyPublisher, long)} by increasing the demand, and
    -		 * by reading the available data and changing state to {@link #DATA_UNAVAILABLE}
    -		 * if there is no more data available.
    -		 */
    -		DATA_AVAILABLE {
    -			@Override
    -			void request(AbstractRequestBodyPublisher publisher, long n) {
    -				if (BackpressureUtils.checkRequest(n, publisher.subscriber)) {
    -					BackpressureUtils.addAndGet(publisher.demand, n);
    -					boolean dataAvailable = publisher.readAndPublish();
    -					if (!dataAvailable) {
    -						publisher.changeState(this, DATA_UNAVAILABLE);
    +					if (publisher.changeState(this, DEMAND)) {
    +						publisher.checkOnDataAvailable();
     					}
     				}
     			}
    -
    +		},
    +		/**
    +		 * State that gets entered when there is demand. Responds to
    +		 * {@link #onDataAvailable(AbstractRequestBodyPublisher)} by
    +		 * reading the available data. The state will be changed to
    +		 * {@link #NO_DEMAND} if there is no demand.
    +		 */
    +		DEMAND {
    +			@Override
    +			void onDataAvailable(AbstractRequestBodyPublisher publisher) {
    +				if (publisher.changeState(this, READING)) {
    +					try {
    +						boolean demandAvailable = publisher.readAndPublish();
    +						if (demandAvailable) {
    +							publisher.changeState(READING, DEMAND);
    +							publisher.checkOnDataAvailable();
    +						} else {
    +							publisher.changeState(READING, NO_DEMAND);
    +						}
    +					} catch (IOException ex) {
    +						publisher.onError(ex);
    +					}
    +				}
    +			}
    +		},
    +		READING {
    +			@Override
    +			void request(AbstractRequestBodyPublisher publisher, long n) {
    +				if (BackpressureUtils.checkRequest(n, publisher.subscriber)) {
    +					BackpressureUtils.addAndGet(publisher.demand, n);
    +				}
    +			}
     		},
     		/**
     		 * The terminal completed state. Does not respond to any events.
    diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java
    index 61ff7409e3..c8b03c4c50 100644
    --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java
    +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java
    @@ -159,13 +159,28 @@ public class ServletHttpHandlerAdapter extends HttpServlet {
     		}
     
     		public void registerListener() throws IOException {
    -			this.synchronizer.getRequest().getInputStream()
    -					.setReadListener(this.readListener);
    +			inputStream().setReadListener(this.readListener);
    +		}
    +
    +		private ServletInputStream inputStream() throws IOException {
    +			return this.synchronizer.getRequest().getInputStream();
    +		}
    +
    +		@Override
    +		protected void checkOnDataAvailable() {
    +			try {
    +				if (!inputStream().isFinished() && inputStream().isReady()) {
    +					onDataAvailable();
    +				}
    +			}
    +			catch (IOException ex) {
    +				onError(ex);
    +			}
     		}
     
     		@Override
     		protected DataBuffer read() throws IOException {
    -			ServletInputStream input = this.synchronizer.getRequest().getInputStream();
    +			ServletInputStream input = inputStream();
     			if (input.isReady()) {
     				int read = input.read(this.buffer);
     				if (logger.isTraceEnabled()) {
    diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java
    index 19f56d596d..1597a3af2c 100644
    --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java
    +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java
    @@ -129,6 +129,11 @@ public class UndertowServerHttpRequest extends AbstractServerHttpRequest {
     			this.requestChannel.resumeReads();
     		}
     
    +		@Override
    +		protected void checkOnDataAvailable() {
    +			onDataAvailable();
    +		}
    +
     		@Override
     		protected DataBuffer read() throws IOException {
     			ByteBuffer byteBuffer = this.pooledByteBuffer.getBuffer();
    
    From b5bce1f0178d94701021c4872feac7bcfa1b5a38 Mon Sep 17 00:00:00 2001
    From: Brian Clozel 
    Date: Fri, 8 Jul 2016 12:00:06 +0200
    Subject: [PATCH 481/498] Update after `reactor.core.converter.Converters`
     changes
    
    Reactor's `DependencyUtils` has been renamed to `Converters` and
    all the `from` converter methods have been disambiguated to
    `fromPublisher`, `toPublisher`.
    ---
     .../convert/support/ReactorToRxJava1Converter.java   | 12 ++++++------
     .../client/reactive/RxNettyClientHttpRequest.java    |  6 +++---
     .../client/reactive/RxNettyClientHttpResponse.java   |  2 +-
     .../server/reactive/RxNettyHttpHandlerAdapter.java   |  2 +-
     .../server/reactive/RxNettyServerHttpRequest.java    |  2 +-
     .../server/reactive/RxNettyServerHttpResponse.java   |  4 ++--
     .../reactive/RxJava1WebResponseExtractors.java       | 12 ++++++------
     .../reactive/config/WebReactiveConfiguration.java    |  4 ++--
     .../annotation/HttpEntityArgumentResolverTests.java  |  4 ++--
     .../annotation/RequestBodyArgumentResolverTests.java |  8 ++++----
     10 files changed, 28 insertions(+), 28 deletions(-)
    
    diff --git a/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactorToRxJava1Converter.java b/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactorToRxJava1Converter.java
    index 67c66bae9d..42b9d9d809 100644
    --- a/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactorToRxJava1Converter.java
    +++ b/spring-web-reactive/src/main/java/org/springframework/core/convert/support/ReactorToRxJava1Converter.java
    @@ -56,22 +56,22 @@ public final class ReactorToRxJava1Converter implements GenericConverter {
     			return null;
     		}
     		if (Observable.class.isAssignableFrom(sourceType.getType())) {
    -			return RxJava1ObservableConverter.from((Observable) source);
    +			return RxJava1ObservableConverter.toPublisher((Observable) source);
     		}
     		else if (Observable.class.isAssignableFrom(targetType.getType())) {
    -			return RxJava1ObservableConverter.from((Publisher) source);
    +			return RxJava1ObservableConverter.fromPublisher((Publisher) source);
     		}
     		else if (Single.class.isAssignableFrom(sourceType.getType())) {
    -			return RxJava1SingleConverter.from((Single) source);
    +			return RxJava1SingleConverter.toPublisher((Single) source);
     		}
     		else if (Single.class.isAssignableFrom(targetType.getType())) {
    -			return RxJava1SingleConverter.from((Publisher) source);
    +			return RxJava1SingleConverter.fromPublisher((Publisher) source);
     		}
     		else if (Completable.class.isAssignableFrom(sourceType.getType())) {
    -			return RxJava1CompletableConverter.from((Completable) source);
    +			return RxJava1CompletableConverter.toPublisher((Completable) source);
     		}
     		else if (Completable.class.isAssignableFrom(targetType.getType())) {
    -			return RxJava1CompletableConverter.from((Publisher) source);
    +			return RxJava1CompletableConverter.fromPublisher((Publisher) source);
     		}
     		return null;
     	}
    diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpRequest.java
    index e0804c07a8..8d3dbcd8ac 100644
    --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpRequest.java
    +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpRequest.java
    @@ -79,7 +79,7 @@ public class RxNettyClientHttpRequest extends AbstractClientHttpRequest {
     	@Override
     	public Mono writeWith(Publisher body) {
     
    -		this.body = RxJava1ObservableConverter.from(Flux.from(body)
    +		this.body = RxJava1ObservableConverter.fromPublisher(Flux.from(body)
     				.map(b -> dataBufferFactory.wrap(b.asByteBuffer()).getNativeBuffer()));
     
     		return Mono.empty();
    @@ -119,10 +119,10 @@ public class RxNettyClientHttpRequest extends AbstractClientHttpRequest {
     					})
     					.map(req -> {
     						if (this.body != null) {
    -							return RxJava1ObservableConverter.from(req.writeContent(this.body));
    +							return RxJava1ObservableConverter.toPublisher(req.writeContent(this.body));
     						}
     						else {
    -							return RxJava1ObservableConverter.from(req);
    +							return RxJava1ObservableConverter.toPublisher(req);
     						}
     					})
     					.flatMap(resp -> resp)
    diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpResponse.java
    index 4495d134f5..af25af009b 100644
    --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpResponse.java
    +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpResponse.java
    @@ -85,7 +85,7 @@ public class RxNettyClientHttpResponse implements ClientHttpResponse {
     	@Override
     	public Flux getBody() {
     		return RxJava1ObservableConverter
    -				.from(this.response.getContent().map(dataBufferFactory::wrap));
    +				.toPublisher(this.response.getContent().map(dataBufferFactory::wrap));
     	}
     
     	@Override
    diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyHttpHandlerAdapter.java
    index 6d0b8cb720..4d7cc5a69c 100644
    --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyHttpHandlerAdapter.java
    +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyHttpHandlerAdapter.java
    @@ -49,7 +49,7 @@ public class RxNettyHttpHandlerAdapter implements RequestHandler result = this.httpHandler.handle(adaptedRequest, adaptedResponse);
    -		return RxJava1ObservableConverter.from(result);
    +		return RxJava1ObservableConverter.fromPublisher(result);
     	}
     
     }
    diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java
    index fd5a5de1ef..4d77f6b484 100644
    --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java
    +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java
    @@ -94,7 +94,7 @@ public class RxNettyServerHttpRequest extends AbstractServerHttpRequest {
     	@Override
     	public Flux getBody() {
     		Observable content = this.request.getContent().map(dataBufferFactory::wrap);
    -		return RxJava1ObservableConverter.from(content);
    +		return RxJava1ObservableConverter.toPublisher(content);
     	}
     
     }
    diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java
    index 9d50f4d337..db90248a12 100644
    --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java
    +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java
    @@ -71,8 +71,8 @@ public class RxNettyServerHttpResponse extends AbstractServerHttpResponse {
     
     	@Override
     	protected Mono writeWithInternal(Publisher body) {
    -		Observable content = RxJava1ObservableConverter.from(body).map(this::toByteBuf);
    -		return RxJava1ObservableConverter.from(this.response.write(content, bb -> bb instanceof FlushingByteBuf)).then();
    +		Observable content = RxJava1ObservableConverter.fromPublisher(body).map(this::toByteBuf);
    +		return RxJava1ObservableConverter.toPublisher(this.response.write(content, bb -> bb instanceof FlushingByteBuf)).then();
     	}
     
     	private ByteBuf toByteBuf(DataBuffer buffer) {
    diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/RxJava1WebResponseExtractors.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/RxJava1WebResponseExtractors.java
    index 0eb51c70d8..03e1bd9258 100644
    --- a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/RxJava1WebResponseExtractors.java
    +++ b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/RxJava1WebResponseExtractors.java
    @@ -53,7 +53,7 @@ public class RxJava1WebResponseExtractors {
     
     		ResolvableType resolvableType = ResolvableType.forClass(sourceClass);
     		//noinspection unchecked
    -		return webResponse -> (Single) RxJava1SingleConverter.from(webResponse.getClientResponse()
    +		return webResponse -> (Single) RxJava1SingleConverter.fromPublisher(webResponse.getClientResponse()
     				.flatMap(resp -> decodeResponseBody(resp, resolvableType, webResponse.getMessageDecoders()))
     				.next());
     	}
    @@ -64,7 +64,7 @@ public class RxJava1WebResponseExtractors {
     	public static  WebResponseExtractor> bodyStream(Class sourceClass) {
     
     		ResolvableType resolvableType = ResolvableType.forClass(sourceClass);
    -		return webResponse -> RxJava1ObservableConverter.from(webResponse.getClientResponse()
    +		return webResponse -> RxJava1ObservableConverter.fromPublisher(webResponse.getClientResponse()
     				.flatMap(resp -> decodeResponseBody(resp, resolvableType, webResponse.getMessageDecoders())));
     	}
     
    @@ -76,7 +76,7 @@ public class RxJava1WebResponseExtractors {
     
     		ResolvableType resolvableType = ResolvableType.forClass(sourceClass);
     		return webResponse -> (Single>)
    -				RxJava1SingleConverter.from(webResponse.getClientResponse()
    +				RxJava1SingleConverter.fromPublisher(webResponse.getClientResponse()
     						.then(response ->
     								Mono.when(
     										decodeResponseBody(response, resolvableType, webResponse.getMessageDecoders()).next(),
    @@ -94,10 +94,10 @@ public class RxJava1WebResponseExtractors {
     	 */
     	public static  WebResponseExtractor>>> responseStream(Class sourceClass) {
     		ResolvableType resolvableType = ResolvableType.forClass(sourceClass);
    -		return webResponse -> RxJava1SingleConverter.from(webResponse.getClientResponse()
    +		return webResponse -> RxJava1SingleConverter.fromPublisher(webResponse.getClientResponse()
     				.map(response -> new ResponseEntity<>(
     						RxJava1ObservableConverter
    -								.from(decodeResponseBody(response, resolvableType, webResponse.getMessageDecoders())),
    +								.fromPublisher(decodeResponseBody(response, resolvableType, webResponse.getMessageDecoders())),
     						response.getHeaders(),
     						response.getStatusCode())));
     	}
    @@ -107,7 +107,7 @@ public class RxJava1WebResponseExtractors {
     	 */
     	public static WebResponseExtractor> headers() {
     		return webResponse -> RxJava1SingleConverter
    -				.from(webResponse.getClientResponse().map(resp -> resp.getHeaders()));
    +				.fromPublisher(webResponse.getClientResponse().map(resp -> resp.getHeaders()));
     	}
     
     	protected static  Flux decodeResponseBody(ClientHttpResponse response, ResolvableType responseType,
    diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java
    index 65cf61f880..6382e94e8e 100644
    --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java
    +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java
    @@ -20,7 +20,7 @@ import java.util.HashMap;
     import java.util.List;
     import java.util.Map;
     
    -import reactor.core.converter.DependencyUtils;
    +import reactor.core.converter.Converters;
     
     import org.springframework.beans.BeanUtils;
     import org.springframework.beans.factory.BeanInitializationException;
    @@ -288,7 +288,7 @@ public class WebReactiveConfiguration implements ApplicationContextAware {
     	 */
     	protected void addFormatters(FormatterRegistry registry) {
     		registry.addConverter(new MonoToCompletableFutureConverter());
    -		if (DependencyUtils.hasRxJava1()) {
    +		if (Converters.hasRxJava1()) {
     			registry.addConverter(new ReactorToRxJava1Converter());
     		}
     	}
    diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/HttpEntityArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/HttpEntityArgumentResolverTests.java
    index 86085d67e4..839960eaed 100644
    --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/HttpEntityArgumentResolverTests.java
    +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/HttpEntityArgumentResolverTests.java
    @@ -156,7 +156,7 @@ public class HttpEntityArgumentResolverTests {
     		ResolvableType type = httpEntityType(forClassWithGenerics(Single.class, String.class));
     		HttpEntity> entity = resolveValueWithEmptyBody(type);
     
    -		TestSubscriber.subscribe(RxJava1SingleConverter.from(entity.getBody()))
    +		TestSubscriber.subscribe(RxJava1SingleConverter.toPublisher(entity.getBody()))
     				.assertNoValues()
     				.assertError(ServerWebInputException.class);
     	}
    @@ -166,7 +166,7 @@ public class HttpEntityArgumentResolverTests {
     		ResolvableType type = httpEntityType(forClassWithGenerics(Observable.class, String.class));
     		HttpEntity> entity = resolveValueWithEmptyBody(type);
     
    -		TestSubscriber.subscribe(RxJava1ObservableConverter.from(entity.getBody()))
    +		TestSubscriber.subscribe(RxJava1ObservableConverter.toPublisher(entity.getBody()))
     				.assertNoError()
     				.assertComplete()
     				.assertNoValues();
    diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java
    index ecd98007eb..b75c5aa017 100644
    --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java
    +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java
    @@ -165,12 +165,12 @@ public class RequestBodyArgumentResolverTests {
     		ResolvableType type = forClassWithGenerics(Single.class, String.class);
     
     		Single single = resolveValueWithEmptyBody(type, true);
    -		TestSubscriber.subscribe(RxJava1SingleConverter.from(single))
    +		TestSubscriber.subscribe(RxJava1SingleConverter.toPublisher(single))
     				.assertNoValues()
     				.assertError(ServerWebInputException.class);
     
     		single = resolveValueWithEmptyBody(type, false);
    -		TestSubscriber.subscribe(RxJava1SingleConverter.from(single))
    +		TestSubscriber.subscribe(RxJava1SingleConverter.toPublisher(single))
     				.assertNoValues()
     				.assertError(ServerWebInputException.class);
     	}
    @@ -180,12 +180,12 @@ public class RequestBodyArgumentResolverTests {
     		ResolvableType type = forClassWithGenerics(Observable.class, String.class);
     
     		Observable observable = resolveValueWithEmptyBody(type, true);
    -		TestSubscriber.subscribe(RxJava1ObservableConverter.from(observable))
    +		TestSubscriber.subscribe(RxJava1ObservableConverter.toPublisher(observable))
     				.assertNoValues()
     				.assertError(ServerWebInputException.class);
     
     		observable = resolveValueWithEmptyBody(type, false);
    -		TestSubscriber.subscribe(RxJava1ObservableConverter.from(observable))
    +		TestSubscriber.subscribe(RxJava1ObservableConverter.toPublisher(observable))
     				.assertNoValues()
     				.assertComplete();
     	}
    
    From 4892436efe462a5836de2b039d2f18dda390bc6b Mon Sep 17 00:00:00 2001
    From: Brian Clozel 
    Date: Fri, 8 Jul 2016 11:40:12 +0200
    Subject: [PATCH 482/498] Refactor HTTP client contracts
    
    This commit refactors the `ClientHttpRequestFactory` into an
    `ClientHttpConnector` abstraction, in order to reflect that
    `ClientHttpRequest`s only "exist" once the client is connected
    to the origin server.
    
    This is why the HTTP client is now callback-based, containing all
    interactions with the request within a
    `Function>` that signals when it's done
    writing to the request.
    
    The `ClientHttpRequest` contract also adopts `setComplete()`
    and promotes that method to the `ReactiveHttpOutputMessage` contract.
    
    This commit also adapts all other APIs to that change and fixes a few
    issues, including:
    
    * use `HttpMessageConverter`s instead of `Encoders`/`Decoders`
    * better handle type information about request content publishers
    * support client cookies in HTTP requests
    * temporarily remove the RxNetty client support
    ---
     .../http/ReactiveHttpOutputMessage.java       |  11 +
     .../reactive/AbstractClientHttpRequest.java   |  24 ++-
     .../client/reactive/ClientHttpConnector.java  |  52 +++++
     .../client/reactive/ClientHttpRequest.java    |  14 +-
     ...y.java => ReactorClientHttpConnector.java} |  32 +--
     .../reactive/ReactorClientHttpRequest.java    |  85 +++-----
     .../reactive/ReactorClientHttpResponse.java   |  34 +--
     .../ReactorHttpClientRequestFactory.java      |  53 -----
     .../reactive/RxNettyClientHttpRequest.java    | 137 ------------
     .../reactive/RxNettyClientHttpResponse.java   | 101 ---------
     .../RxNettyHttpClientRequestFactory.java      |  48 -----
     .../web/client/reactive/ClientWebRequest.java |  96 +++++++++
     ...ctor.java => ClientWebRequestBuilder.java} |  13 +-
     .../reactive/ClientWebRequestBuilders.java    | 111 ++++++++++
     ...ava => ClientWebRequestPostProcessor.java} |  18 +-
     .../DefaultClientWebRequestBuilder.java       | 196 ++++++++++++++++++
     .../reactive/DefaultHttpRequestBuilder.java   | 167 ---------------
     .../client/reactive/DefaultWebResponse.java   |  52 -----
     .../client/reactive/HttpRequestBuilder.java   |  40 ----
     .../client/reactive/HttpRequestBuilders.java  | 110 ----------
     ...ebResponse.java => ResponseExtractor.java} |  20 +-
     .../client/reactive/ResponseExtractors.java   | 163 +++++++++++++++
     .../RxJava1WebResponseExtractors.java         | 131 ------------
     .../web/client/reactive/WebClient.java        | 188 ++++++++++++-----
     .../client/reactive/WebResponseActions.java   |   2 +-
     .../reactive/WebResponseExtractors.java       | 161 --------------
     .../reactive/FlushingIntegrationTests.java    |   9 +-
     ...ava => DefaultWebRequestBuilderTests.java} |  26 ++-
     .../reactive/WebClientIntegrationTests.java   |  39 +++-
     .../annotation/SseIntegrationTests.java       |  30 +--
     30 files changed, 942 insertions(+), 1221 deletions(-)
     create mode 100644 spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ClientHttpConnector.java
     rename spring-web-reactive/src/main/java/org/springframework/http/client/reactive/{ClientHttpRequestFactory.java => ReactorClientHttpConnector.java} (52%)
     delete mode 100644 spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorHttpClientRequestFactory.java
     delete mode 100644 spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpRequest.java
     delete mode 100644 spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpResponse.java
     delete mode 100644 spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyHttpClientRequestFactory.java
     create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/client/reactive/ClientWebRequest.java
     rename spring-web-reactive/src/main/java/org/springframework/web/client/reactive/{WebResponseExtractor.java => ClientWebRequestBuilder.java} (65%)
     create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/client/reactive/ClientWebRequestBuilders.java
     rename spring-web-reactive/src/main/java/org/springframework/web/client/reactive/{RequestPostProcessor.java => ClientWebRequestPostProcessor.java} (59%)
     create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultClientWebRequestBuilder.java
     delete mode 100644 spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultHttpRequestBuilder.java
     delete mode 100644 spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultWebResponse.java
     delete mode 100644 spring-web-reactive/src/main/java/org/springframework/web/client/reactive/HttpRequestBuilder.java
     delete mode 100644 spring-web-reactive/src/main/java/org/springframework/web/client/reactive/HttpRequestBuilders.java
     rename spring-web-reactive/src/main/java/org/springframework/web/client/reactive/{WebResponse.java => ResponseExtractor.java} (61%)
     create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/client/reactive/ResponseExtractors.java
     delete mode 100644 spring-web-reactive/src/main/java/org/springframework/web/client/reactive/RxJava1WebResponseExtractors.java
     delete mode 100644 spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebResponseExtractors.java
     rename spring-web-reactive/src/test/java/org/springframework/web/client/reactive/{DefaultHttpRequestBuilderTests.java => DefaultWebRequestBuilderTests.java} (55%)
    
    diff --git a/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java b/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java
    index cda8fd58ed..1c6a4af051 100644
    --- a/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java
    +++ b/spring-web-reactive/src/main/java/org/springframework/http/ReactiveHttpOutputMessage.java
    @@ -62,4 +62,15 @@ public interface ReactiveHttpOutputMessage extends HttpMessage {
     	 */
     	DataBufferFactory bufferFactory();
     
    +	/**
    +	 * Indicate that message handling is complete, allowing for any cleanup or
    +	 * end-of-processing tasks to be performed such as applying header changes
    +	 * made via {@link #getHeaders()} to the underlying HTTP message (if not
    +	 * applied already).
    +	 * 

    This method should be automatically invoked at the end of message + * processing so typically applications should not have to invoke it. + * If invoked multiple times it should have no side effects. + */ + Mono setComplete(); + } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/AbstractClientHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/AbstractClientHttpRequest.java index 6ecb0caea8..e448cb149f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/AbstractClientHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/AbstractClientHttpRequest.java @@ -46,13 +46,13 @@ public abstract class AbstractClientHttpRequest implements ClientHttpRequest { private final List>> beforeCommitActions = new ArrayList<>(4); - public AbstractClientHttpRequest(HttpHeaders httpHeaders) { - if (httpHeaders == null) { - this.headers = new HttpHeaders(); - } - else { - this.headers = httpHeaders; - } + public AbstractClientHttpRequest() { + this(new HttpHeaders()); + } + + public AbstractClientHttpRequest(HttpHeaders headers) { + Assert.notNull(headers); + this.headers = headers; this.cookies = new LinkedMultiValueMap<>(); } @@ -85,8 +85,8 @@ public abstract class AbstractClientHttpRequest implements ClientHttpRequest { }) .then(() -> { this.state.set(State.COMITTED); - //writeHeaders(); - //writeCookies(); + writeHeaders(); + writeCookies(); return Mono.empty(); }); } @@ -99,5 +99,9 @@ public abstract class AbstractClientHttpRequest implements ClientHttpRequest { this.beforeCommitActions.add(action); } + protected abstract void writeHeaders(); + + protected abstract void writeCookies(); + private enum State {NEW, COMMITTING, COMITTED} -} +} \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ClientHttpConnector.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ClientHttpConnector.java new file mode 100644 index 0000000000..bea20e29e0 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ClientHttpConnector.java @@ -0,0 +1,52 @@ +/* + * 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.http.client.reactive; + +import java.net.URI; +import java.util.function.Function; + +import reactor.core.publisher.Mono; + +import org.springframework.http.HttpMethod; + +/** + * Client abstraction for HTTP client runtimes. + * {@link ClientHttpConnector} drives the underlying HTTP client implementation + * so as to connect to the origin server and provide all the necessary infrastructure + * to send the actual {@link ClientHttpRequest} and receive the {@link ClientHttpResponse} + * + * @author Brian Clozel + */ +public interface ClientHttpConnector { + + /** + * Connect to the origin server using the given {@code HttpMethod} and {@code URI}, + * then apply the given {@code requestCallback} on the {@link ClientHttpRequest} + * once the connection has been established. + *

    Return a publisher of the {@link ClientHttpResponse}. + * + * @param method the HTTP request method + * @param uri the HTTP request URI + * @param requestCallback a function that prepares and writes the request, + * returning a publisher that signals when it's done interacting with the request. + * Implementations should return a {@code Mono} by calling + * {@link ClientHttpRequest#writeWith} or {@link ClientHttpRequest#setComplete}. + * @return a publisher of the {@link ClientHttpResponse} + */ + Mono connect(HttpMethod method, URI uri, + Function> requestCallback); + +} \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ClientHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ClientHttpRequest.java index 3e7098a043..9819a372e3 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ClientHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ClientHttpRequest.java @@ -18,8 +18,6 @@ package org.springframework.http.client.reactive; import java.net.URI; -import reactor.core.publisher.Mono; - import org.springframework.http.HttpCookie; import org.springframework.http.HttpMethod; import org.springframework.http.ReactiveHttpOutputMessage; @@ -48,14 +46,4 @@ public interface ClientHttpRequest extends ReactiveHttpOutputMessage { */ MultiValueMap getCookies(); - /** - * Execute this request, resulting in a reactive stream of a single - * {@link org.springframework.http.client.ClientHttpResponse}. - * - * @return a {@code Mono} that signals when the the response - * status and headers have been received. The response body is made available with - * a separate Publisher within the {@code ClientHttpResponse}. - */ - Mono execute(); - -} +} \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ClientHttpRequestFactory.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpConnector.java similarity index 52% rename from spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ClientHttpRequestFactory.java rename to spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpConnector.java index ce8b5b7e17..a5f38eb162 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ClientHttpRequestFactory.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpConnector.java @@ -13,31 +13,31 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.http.client.reactive; import java.net.URI; +import java.util.function.Function; -import org.reactivestreams.Publisher; - -import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; +import reactor.core.publisher.Mono; + /** - * Factory for {@link ClientHttpRequest} objects. + * Reactor-Netty implementation of {@link ClientHttpConnector} * * @author Brian Clozel */ -public interface ClientHttpRequestFactory { +public class ReactorClientHttpConnector implements ClientHttpConnector { - /** - * Create a new {@link ClientHttpRequest} for the specified HTTP method, URI and headers - *

    The returned request can be {@link ClientHttpRequest#writeWith(Publisher) written to}, - * and then executed by calling {@link ClientHttpRequest#execute()} - * - * @param httpMethod the HTTP method to execute - * @param uri the URI to create a request for - * @param headers the HTTP request headers - */ - ClientHttpRequest createRequest(HttpMethod httpMethod, URI uri, HttpHeaders headers); + @Override + public Mono connect(HttpMethod method, URI uri, + Function> requestCallback) { -} + return reactor.io.netty.http.HttpClient.create(uri.getHost(), uri.getPort()) + .request(io.netty.handler.codec.http.HttpMethod.valueOf(method.name()), + uri.toString(), + httpOutbound -> requestCallback.apply(new ReactorClientHttpRequest(method, uri, httpOutbound))) + .map(httpInbound -> new ReactorClientHttpResponse(httpInbound)); + } +} \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java index 4bec8778a1..f9573f7ea4 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java @@ -17,7 +17,6 @@ package org.springframework.http.client.reactive; import java.net.URI; -import java.util.Collection; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; @@ -26,45 +25,40 @@ import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.io.netty.http.HttpClient; +import reactor.io.netty.http.HttpClientRequest; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; -import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.core.io.buffer.NettyDataBuffer; -import org.springframework.http.HttpHeaders; +import org.springframework.core.io.buffer.NettyDataBufferFactory; import org.springframework.http.HttpMethod; /** - * {@link ClientHttpRequest} implementation for the Reactor Net HTTP client + * {@link ClientHttpRequest} implementation for the Reactor-Netty HTTP client * * @author Brian Clozel - * @see HttpClient + * @see reactor.io.netty.http.HttpClient */ public class ReactorClientHttpRequest extends AbstractClientHttpRequest { - private final DataBufferFactory dataBufferFactory; - private final HttpMethod httpMethod; private final URI uri; - private final HttpClient httpClient; + private final HttpClientRequest httpRequest; - private Flux body; + private final NettyDataBufferFactory bufferFactory; - - public ReactorClientHttpRequest(HttpMethod httpMethod, URI uri, HttpClient httpClient, HttpHeaders headers) { - super(headers); - //FIXME use Netty factory - this.dataBufferFactory = new DefaultDataBufferFactory(); + public ReactorClientHttpRequest(HttpMethod httpMethod, URI uri, HttpClientRequest httpRequest) { this.httpMethod = httpMethod; this.uri = uri; - this.httpClient = httpClient; + this.httpRequest = httpRequest; + this.bufferFactory = new NettyDataBufferFactory(httpRequest.delegate().alloc()); } @Override public DataBufferFactory bufferFactory() { - return this.dataBufferFactory; + return this.bufferFactory; } @Override @@ -77,51 +71,15 @@ public class ReactorClientHttpRequest extends AbstractClientHttpRequest { return this.uri; } - /** - * Set the body of the message to the given {@link Publisher}. - * - *

    Since the HTTP channel is not yet created when this method - * is called, the {@code Mono} return value completes immediately. - * For an event that signals that we're done writing the request, check the - * {@link #execute()} method. - * - * @return a publisher that completes immediately. - * @see #execute() - */ @Override public Mono writeWith(Publisher body) { - - this.body = Flux.from(body).map(this::toByteBuf); - return Mono.empty(); + return applyBeforeCommit() + .then(httpRequest.send(Flux.from(body).map(this::toByteBuf))); } @Override - public Mono execute() { - - return this.httpClient.request(new io.netty.handler.codec.http.HttpMethod(httpMethod.toString()), uri.toString(), - channel -> { - // see https://github.com/reactor/reactor-io/pull/8 - if (body == null) { - channel.removeTransferEncodingChunked(); - } - return applyBeforeCommit() - .then(() -> { - getHeaders().entrySet().stream().forEach(e -> - channel.headers().set(e.getKey(), e.getValue())); - getCookies().values().stream().flatMap(Collection::stream).forEach(cookie -> - channel.addCookie(new DefaultCookie(cookie.getName(), cookie.getValue()))); - return Mono.empty(); - }) - .then(() -> { - if (body != null) { - return channel.send(body); - } - else { - return channel.sendHeaders(); - } - }); - }).map(httpChannel -> new ReactorClientHttpResponse(httpChannel, - dataBufferFactory)); + public Mono setComplete() { + return applyBeforeCommit().then(httpRequest.sendHeaders()); } private ByteBuf toByteBuf(DataBuffer buffer) { @@ -133,5 +91,18 @@ public class ReactorClientHttpRequest extends AbstractClientHttpRequest { } } -} + @Override + protected void writeHeaders() { + getHeaders().entrySet().stream() + .forEach(e -> this.httpRequest.headers().set(e.getKey(), e.getValue())); + } + @Override + protected void writeCookies() { + getCookies().values() + .stream().flatMap(cookies -> cookies.stream()) + .map(cookie -> new DefaultCookie(cookie.getName(), cookie.getValue())) + .forEach(cookie -> this.httpRequest.addCookie(cookie)); + } + +} \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpResponse.java index 0a6e4f6d73..049f20fd63 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpResponse.java @@ -17,12 +17,15 @@ package org.springframework.http.client.reactive; import java.util.Collection; +import java.util.function.Function; +import io.netty.buffer.ByteBuf; import reactor.core.publisher.Flux; -import reactor.io.netty.http.HttpInbound; +import reactor.io.netty.http.HttpClientResponse; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.NettyDataBufferFactory; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseCookie; @@ -31,44 +34,47 @@ import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; /** - * {@link ClientHttpResponse} implementation for the Reactor Net HTTP client + * {@link ClientHttpResponse} implementation for the Reactor-Netty HTTP client * * @author Brian Clozel * @see reactor.io.netty.http.HttpClient */ public class ReactorClientHttpResponse implements ClientHttpResponse { - private final DataBufferFactory dataBufferFactory; + private final NettyDataBufferFactory dataBufferFactory; - private final HttpInbound channel; + private final HttpClientResponse response; - public ReactorClientHttpResponse(HttpInbound channel, - DataBufferFactory dataBufferFactory) { - this.dataBufferFactory = dataBufferFactory; - this.channel = channel; + public ReactorClientHttpResponse(HttpClientResponse response) { + this.response = response; + this.dataBufferFactory = new NettyDataBufferFactory(response.delegate().alloc()); } @Override public Flux getBody() { - return channel.receiveByteBuffer().map(dataBufferFactory::wrap); + return response.receive() + .map(buf -> { + buf.retain(); + return dataBufferFactory.wrap(buf); + }); } @Override public HttpHeaders getHeaders() { HttpHeaders headers = new HttpHeaders(); - this.channel.responseHeaders().entries().stream().forEach(e -> headers.add(e.getKey(), e.getValue())); + this.response.responseHeaders().entries().stream().forEach(e -> headers.add(e.getKey(), e.getValue())); return headers; } @Override public HttpStatus getStatusCode() { - return HttpStatus.valueOf(this.channel.status().code()); + return HttpStatus.valueOf(this.response.status().code()); } @Override public MultiValueMap getCookies() { MultiValueMap result = new LinkedMultiValueMap<>(); - this.channel.cookies().values().stream().flatMap(Collection::stream) + this.response.cookies().values().stream().flatMap(Collection::stream) .forEach(cookie -> { ResponseCookie responseCookie = ResponseCookie.from(cookie.name(), cookie.value()) .domain(cookie.domain()) @@ -85,8 +91,8 @@ public class ReactorClientHttpResponse implements ClientHttpResponse { @Override public String toString() { return "ReactorClientHttpResponse{" + - "request=" + this.channel.method().name() + " " + this.channel.uri() + "," + + "request=" + this.response.method().name() + " " + this.response.uri() + "," + "status=" + getStatusCode() + '}'; } -} +} \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorHttpClientRequestFactory.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorHttpClientRequestFactory.java deleted file mode 100644 index 70becdd999..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/ReactorHttpClientRequestFactory.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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.http.client.reactive; - -import java.net.URI; - -import reactor.io.netty.http.HttpClient; - -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.util.Assert; - -/** - * Create a {@link ClientHttpRequest} for the Reactor Net HTTP client - * - * @author Brian Clozel - */ -public class ReactorHttpClientRequestFactory implements ClientHttpRequestFactory { - - private final HttpClient httpClient; - - public ReactorHttpClientRequestFactory() { - this(reactor.io.netty.http.HttpClient.create()); - } - - protected ReactorHttpClientRequestFactory(HttpClient httpClient) { - this.httpClient = httpClient; - } - - @Override - public ClientHttpRequest createRequest(HttpMethod httpMethod, URI uri, HttpHeaders headers) { - Assert.notNull(httpMethod, "HTTP method is required"); - Assert.notNull(uri, "request URI is required"); - Assert.notNull(headers, "request headers are required"); - - return new ReactorClientHttpRequest(httpMethod, uri, this.httpClient, headers); - } - -} diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpRequest.java deleted file mode 100644 index 8d3dbcd8ac..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpRequest.java +++ /dev/null @@ -1,137 +0,0 @@ -/* - * 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.http.client.reactive; - -import java.net.URI; -import java.util.List; -import java.util.Map; - -import io.netty.buffer.ByteBuf; -import io.netty.handler.codec.http.cookie.DefaultCookie; -import io.reactivex.netty.protocol.http.client.HttpClient; -import io.reactivex.netty.protocol.http.client.HttpClientRequest; -import org.reactivestreams.Publisher; -import reactor.core.converter.RxJava1ObservableConverter; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import rx.Observable; - -import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferFactory; -import org.springframework.core.io.buffer.NettyDataBufferFactory; -import org.springframework.http.HttpCookie; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; - -/** - * {@link ClientHttpRequest} implementation for the RxNetty HTTP client - * - * @author Brian Clozel - */ -public class RxNettyClientHttpRequest extends AbstractClientHttpRequest { - - private final NettyDataBufferFactory dataBufferFactory; - - private final HttpMethod httpMethod; - - private final URI uri; - - private Observable body; - - public RxNettyClientHttpRequest(HttpMethod httpMethod, URI uri, HttpHeaders headers, - NettyDataBufferFactory dataBufferFactory) { - super(headers); - this.httpMethod = httpMethod; - this.uri = uri; - this.dataBufferFactory = dataBufferFactory; - } - - @Override - public DataBufferFactory bufferFactory() { - return this.dataBufferFactory; - } - - /** - * Set the body of the message to the given {@link Publisher}. - * - *

    Since the HTTP channel is not yet created when this method - * is called, the {@code Mono} return value completes immediately. - * For an event that signals that we're done writing the request, check the - * {@link #execute()} method. - * - * @return a publisher that completes immediately. - * @see #execute() - */ - @Override - public Mono writeWith(Publisher body) { - - this.body = RxJava1ObservableConverter.fromPublisher(Flux.from(body) - .map(b -> dataBufferFactory.wrap(b.asByteBuffer()).getNativeBuffer())); - - return Mono.empty(); - } - - @Override - public HttpMethod getMethod() { - return this.httpMethod; - } - - @Override - public URI getURI() { - return this.uri; - } - - @Override - public Mono execute() { - try { - HttpClientRequest request = HttpClient - .newClient(this.uri.getHost(), this.uri.getPort()) - .createRequest(io.netty.handler.codec.http.HttpMethod.valueOf(this.httpMethod.name()), uri.getRawPath()); - - return applyBeforeCommit() - .then(() -> Mono.just(request)) - .map(req -> { - for (Map.Entry> entry : getHeaders().entrySet()) { - for (String value : entry.getValue()) { - req = req.addHeader(entry.getKey(), value); - } - } - for (Map.Entry> entry : getCookies().entrySet()) { - for (HttpCookie cookie : entry.getValue()) { - req.addCookie(new DefaultCookie(cookie.getName(), cookie.getValue())); - } - } - return req; - }) - .map(req -> { - if (this.body != null) { - return RxJava1ObservableConverter.toPublisher(req.writeContent(this.body)); - } - else { - return RxJava1ObservableConverter.toPublisher(req); - } - }) - .flatMap(resp -> resp) - .next().map(response -> new RxNettyClientHttpResponse(response, - this.dataBufferFactory)); - } - catch (IllegalArgumentException exc) { - return Mono.error(exc); - } - } - -} diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpResponse.java deleted file mode 100644 index af25af009b..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyClientHttpResponse.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * 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.http.client.reactive; - -import java.util.Collection; - -import io.netty.buffer.ByteBuf; -import io.reactivex.netty.protocol.http.client.HttpClientResponse; -import reactor.core.converter.RxJava1ObservableConverter; -import reactor.core.publisher.Flux; - -import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.NettyDataBufferFactory; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseCookie; -import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -/** - * {@link ClientHttpResponse} implementation for the RxNetty HTTP client - * - * @author Brian Clozel - */ -public class RxNettyClientHttpResponse implements ClientHttpResponse { - - private final HttpClientResponse response; - - private final HttpHeaders headers; - - private final MultiValueMap cookies; - - private final NettyDataBufferFactory dataBufferFactory; - - - public RxNettyClientHttpResponse(HttpClientResponse response, - NettyDataBufferFactory dataBufferFactory) { - Assert.notNull("'request', request must not be null"); - Assert.notNull(dataBufferFactory, "'dataBufferFactory' must not be null"); - this.dataBufferFactory = dataBufferFactory; - this.response = response; - this.headers = new HttpHeaders(); - this.response.headerIterator().forEachRemaining(e -> this.headers.set(e.getKey().toString(), e.getValue().toString())); - this.cookies = initCookies(response); - } - - private static MultiValueMap initCookies(HttpClientResponse response) { - MultiValueMap result = new LinkedMultiValueMap<>(); - response.getCookies().values().stream().flatMap(Collection::stream) - .forEach(cookie -> { - ResponseCookie responseCookie = ResponseCookie.from(cookie.name(), cookie.value()) - .domain(cookie.domain()) - .path(cookie.path()) - .maxAge(cookie.maxAge()) - .secure(cookie.isSecure()) - .httpOnly(cookie.isHttpOnly()) - .build(); - result.add(cookie.name(), responseCookie); - }); - return CollectionUtils.unmodifiableMultiValueMap(result); - } - - - @Override - public HttpStatus getStatusCode() { - return HttpStatus.valueOf(this.response.getStatus().code()); - } - - @Override - public Flux getBody() { - return RxJava1ObservableConverter - .toPublisher(this.response.getContent().map(dataBufferFactory::wrap)); - } - - @Override - public HttpHeaders getHeaders() { - return this.headers; - } - - @Override - public MultiValueMap getCookies() { - return this.cookies; - } - -} diff --git a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyHttpClientRequestFactory.java b/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyHttpClientRequestFactory.java deleted file mode 100644 index 0b935af812..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/http/client/reactive/RxNettyHttpClientRequestFactory.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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.http.client.reactive; - -import java.net.URI; - -import org.springframework.core.io.buffer.NettyDataBufferFactory; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.util.Assert; - -/** - * Create a {@link ClientHttpRequestFactory} for the RxNetty HTTP client - * - * @author Brian Clozel - */ -public class RxNettyHttpClientRequestFactory implements ClientHttpRequestFactory { - - private final NettyDataBufferFactory dataBufferFactory; - - public RxNettyHttpClientRequestFactory(NettyDataBufferFactory dataBufferFactory) { - this.dataBufferFactory = dataBufferFactory; - } - - @Override - public ClientHttpRequest createRequest(HttpMethod httpMethod, URI uri, HttpHeaders headers) { - Assert.notNull(httpMethod, "HTTP method is required"); - Assert.notNull(uri, "request URI is required"); - Assert.notNull(headers, "request headers are required"); - - return new RxNettyClientHttpRequest(httpMethod, uri, headers, - this.dataBufferFactory); - } -} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/ClientWebRequest.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/ClientWebRequest.java new file mode 100644 index 0000000000..c16950f87c --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/ClientWebRequest.java @@ -0,0 +1,96 @@ +/* + * 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.web.client.reactive; + +import java.net.URI; + +import org.reactivestreams.Publisher; + +import org.springframework.core.ResolvableType; +import org.springframework.http.HttpCookie; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.util.MultiValueMap; + +/** + * Holds all the application information required to build an actual HTTP client request. + *

    The request body is materialized by a {@code Publisher} of Objects and their type + * by a {@code ResolvableType} instance; it should be later converted to a + * {@code Publisher} to be written to the actual HTTP client request. + * + * @author Brian Clozel + */ +public class ClientWebRequest { + + protected final HttpMethod httpMethod; + + protected final URI url; + + protected HttpHeaders httpHeaders; + + private MultiValueMap cookies; + + protected Publisher body; + + protected ResolvableType elementType; + + + public ClientWebRequest(HttpMethod httpMethod, URI url) { + this.httpMethod = httpMethod; + this.url = url; + } + + public HttpMethod getMethod() { + return httpMethod; + } + + public URI getUrl() { + return url; + } + + public HttpHeaders getHttpHeaders() { + return httpHeaders; + } + + public void setHttpHeaders(HttpHeaders httpHeaders) { + this.httpHeaders = httpHeaders; + } + + public MultiValueMap getCookies() { + return cookies; + } + + public void setCookies(MultiValueMap cookies) { + this.cookies = cookies; + } + + public Publisher getBody() { + return body; + } + + public void setBody(Publisher body) { + this.body = body; + } + + public ResolvableType getElementType() { + return elementType; + } + + public void setElementType(ResolvableType elementType) { + this.elementType = elementType; + } +} \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebResponseExtractor.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/ClientWebRequestBuilder.java similarity index 65% rename from spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebResponseExtractor.java rename to spring-web-reactive/src/main/java/org/springframework/web/client/reactive/ClientWebRequestBuilder.java index 2bb1592447..1baf43a2e9 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebResponseExtractor.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/ClientWebRequestBuilder.java @@ -17,15 +17,12 @@ package org.springframework.web.client.reactive; /** - * A {@code WebResponseExtractor} extracts the relevant part of a - * raw {@link org.springframework.http.client.reactive.ClientHttpResponse}, - * optionally decoding the response body and using a target composition API. - * - *

    See static factory methods in {@link WebResponseExtractors}. + * Build {@link ClientWebRequest}s * * @author Brian Clozel */ -public interface WebResponseExtractor { +public interface ClientWebRequestBuilder { - T extract(WebResponse webResponse); -} + ClientWebRequest build(); + +} \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/ClientWebRequestBuilders.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/ClientWebRequestBuilders.java new file mode 100644 index 0000000000..277dd57039 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/ClientWebRequestBuilders.java @@ -0,0 +1,111 @@ +/* + * 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.web.client.reactive; + + +import org.springframework.http.HttpMethod; + +/** + * Static factory methods for {@link DefaultClientWebRequestBuilder ClientWebRequestBuilders} + * + * @author Brian Clozel + */ +public abstract class ClientWebRequestBuilders { + + /** + * Create a {@link DefaultClientWebRequestBuilder} for a GET request. + * + * @param urlTemplate a URL template; the resulting URL will be encoded + * @param urlVariables zero or more URL variables + */ + public static DefaultClientWebRequestBuilder get(String urlTemplate, Object... urlVariables) { + return new DefaultClientWebRequestBuilder(HttpMethod.GET, urlTemplate, urlVariables); + } + + /** + * Create a {@link DefaultClientWebRequestBuilder} for a POST request. + * + * @param urlTemplate a URL template; the resulting URL will be encoded + * @param urlVariables zero or more URL variables + */ + public static DefaultClientWebRequestBuilder post(String urlTemplate, Object... urlVariables) { + return new DefaultClientWebRequestBuilder(HttpMethod.POST, urlTemplate, urlVariables); + } + + + /** + * Create a {@link DefaultClientWebRequestBuilder} for a PUT request. + * + * @param urlTemplate a URL template; the resulting URL will be encoded + * @param urlVariables zero or more URL variables + */ + public static DefaultClientWebRequestBuilder put(String urlTemplate, Object... urlVariables) { + return new DefaultClientWebRequestBuilder(HttpMethod.PUT, urlTemplate, urlVariables); + } + + /** + * Create a {@link DefaultClientWebRequestBuilder} for a PATCH request. + * + * @param urlTemplate a URL template; the resulting URL will be encoded + * @param urlVariables zero or more URL variables + */ + public static DefaultClientWebRequestBuilder patch(String urlTemplate, Object... urlVariables) { + return new DefaultClientWebRequestBuilder(HttpMethod.PATCH, urlTemplate, urlVariables); + } + + /** + * Create a {@link DefaultClientWebRequestBuilder} for a DELETE request. + * + * @param urlTemplate a URL template; the resulting URL will be encoded + * @param urlVariables zero or more URL variables + */ + public static DefaultClientWebRequestBuilder delete(String urlTemplate, Object... urlVariables) { + return new DefaultClientWebRequestBuilder(HttpMethod.DELETE, urlTemplate, urlVariables); + } + + /** + * Create a {@link DefaultClientWebRequestBuilder} for an OPTIONS request. + * + * @param urlTemplate a URL template; the resulting URL will be encoded + * @param urlVariables zero or more URL variables + */ + public static DefaultClientWebRequestBuilder options(String urlTemplate, Object... urlVariables) { + return new DefaultClientWebRequestBuilder(HttpMethod.OPTIONS, urlTemplate, urlVariables); + } + + /** + * Create a {@link DefaultClientWebRequestBuilder} for a HEAD request. + * + * @param urlTemplate a URL template; the resulting URL will be encoded + * @param urlVariables zero or more URL variables + */ + public static DefaultClientWebRequestBuilder head(String urlTemplate, Object... urlVariables) { + return new DefaultClientWebRequestBuilder(HttpMethod.HEAD, urlTemplate, urlVariables); + } + + /** + * Create a {@link DefaultClientWebRequestBuilder} for a request with the given HTTP method. + * + * @param httpMethod the HTTP method + * @param urlTemplate a URL template; the resulting URL will be encoded + * @param urlVariables zero or more URL variables + */ + public static DefaultClientWebRequestBuilder request(HttpMethod httpMethod, String urlTemplate, Object... urlVariables) { + return new DefaultClientWebRequestBuilder(httpMethod, urlTemplate, urlVariables); + } + +} \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/RequestPostProcessor.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/ClientWebRequestPostProcessor.java similarity index 59% rename from spring-web-reactive/src/main/java/org/springframework/web/client/reactive/RequestPostProcessor.java rename to spring-web-reactive/src/main/java/org/springframework/web/client/reactive/ClientWebRequestPostProcessor.java index ac8ec9783e..40df349531 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/RequestPostProcessor.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/ClientWebRequestPostProcessor.java @@ -17,20 +17,20 @@ package org.springframework.web.client.reactive; /** - * Allows post processing the {@link DefaultHttpRequestBuilder} for strategy for - * performing more complex operations. + * Allow post processing and/or wrapping the {@link ClientWebRequest} before + * it's sent to the origin server. * * @author Rob Winch - * @see DefaultHttpRequestBuilder#apply(RequestPostProcessor) + * @author Brian Clozel + * @see DefaultClientWebRequestBuilder#apply(ClientWebRequestPostProcessor) */ -public interface RequestPostProcessor { +public interface ClientWebRequestPostProcessor { /** - * Implementations can modify the {@link DefaultHttpRequestBuilder} passed - * in. + * Implementations can modify and/or wrap the {@link ClientWebRequest} passed in + * and return it * - * @param toPostProcess - * the {@link DefaultHttpRequestBuilder} to be modified. + * @param request the {@link ClientWebRequest} to be modified and/or wrapped. */ - void postProcess(DefaultHttpRequestBuilder toPostProcess); + ClientWebRequest postProcess(ClientWebRequest request); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultClientWebRequestBuilder.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultClientWebRequestBuilder.java new file mode 100644 index 0000000000..4e399f65f1 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultClientWebRequestBuilder.java @@ -0,0 +1,196 @@ +/* + * 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.web.client.reactive; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import org.reactivestreams.Publisher; +import reactor.core.publisher.Mono; + +import org.springframework.core.ResolvableType; +import org.springframework.http.HttpCookie; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.client.reactive.ClientHttpRequest; +import org.springframework.util.Assert; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.util.DefaultUriTemplateHandler; +import org.springframework.web.util.UriTemplateHandler; + +/** + * Builds a {@link ClientHttpRequest} using a {@link Publisher} + * as request body. + * + *

    See static factory methods in {@link ClientWebRequestBuilders} + * + * @author Brian Clozel + * @see ClientWebRequestBuilders + */ +public class DefaultClientWebRequestBuilder implements ClientWebRequestBuilder { + + + private final UriTemplateHandler uriTemplateHandler = new DefaultUriTemplateHandler(); + + private HttpMethod httpMethod; + + private HttpHeaders httpHeaders; + + private URI url; + + private final MultiValueMap cookies = new LinkedMultiValueMap<>(); + + private Publisher body; + + private ResolvableType elementType; + + private List postProcessors = new ArrayList<>(); + + protected DefaultClientWebRequestBuilder() { + } + + public DefaultClientWebRequestBuilder(HttpMethod httpMethod, String urlTemplate, + Object... urlVariables) { + this.httpMethod = httpMethod; + this.httpHeaders = new HttpHeaders(); + this.url = this.uriTemplateHandler.expand(urlTemplate, urlVariables); + } + + public DefaultClientWebRequestBuilder(HttpMethod httpMethod, URI url) { + this.httpMethod = httpMethod; + this.httpHeaders = new HttpHeaders(); + this.url = url; + } + + /** + * Add an HTTP request header + */ + public DefaultClientWebRequestBuilder header(String name, String... values) { + Arrays.stream(values).forEach(value -> this.httpHeaders.add(name, value)); + return this; + } + + /** + * Add all provided HTTP request headers + */ + public DefaultClientWebRequestBuilder headers(HttpHeaders httpHeaders) { + this.httpHeaders = httpHeaders; + return this; + } + + /** + * Set the Content-Type request header to the given {@link MediaType} + */ + public DefaultClientWebRequestBuilder contentType(MediaType contentType) { + this.httpHeaders.setContentType(contentType); + return this; + } + + /** + * Set the Content-Type request header to the given media type + */ + public DefaultClientWebRequestBuilder contentType(String contentType) { + this.httpHeaders.setContentType(MediaType.parseMediaType(contentType)); + return this; + } + + /** + * Set the Accept request header to the given {@link MediaType}s + */ + public DefaultClientWebRequestBuilder accept(MediaType... mediaTypes) { + this.httpHeaders.setAccept(Arrays.asList(mediaTypes)); + return this; + } + + /** + * Set the Accept request header to the given media types + */ + public DefaultClientWebRequestBuilder accept(String... mediaTypes) { + this.httpHeaders.setAccept( + Arrays.stream(mediaTypes).map(type -> MediaType.parseMediaType(type)) + .collect(Collectors.toList())); + return this; + } + + /** + * Add a Cookie to the HTTP request + */ + public DefaultClientWebRequestBuilder cookie(String name, String value) { + return cookie(new HttpCookie(name, value)); + } + + /** + * Add a Cookie to the HTTP request + */ + public DefaultClientWebRequestBuilder cookie(HttpCookie cookie) { + this.cookies.add(cookie.getName(), cookie); + return this; + } + + /** + * Allows performing more complex operations with a strategy. For example, a + * {@link ClientWebRequestPostProcessor} implementation might accept the arguments of username + * and password and set an HTTP Basic authentication header. + * + * @param postProcessor the {@link ClientWebRequestPostProcessor} to use. Cannot be null. + * + * @return this instance for further modifications. + */ + public DefaultClientWebRequestBuilder apply(ClientWebRequestPostProcessor postProcessor) { + Assert.notNull(postProcessor, "`postProcessor` is required"); + this.postProcessors.add(postProcessor); + return this; + } + + /** + * Use the given object as the request body + */ + public DefaultClientWebRequestBuilder body(Object content) { + this.body = Mono.just(content); + this.elementType = ResolvableType.forInstance(content); + return this; + } + + /** + * Use the given {@link Publisher} as the request body and use its {@link ResolvableType} + * as type information for the element published by this reactive stream + */ + public DefaultClientWebRequestBuilder body(Publisher content, ResolvableType publisherType) { + this.body = content; + this.elementType = publisherType; + return this; + } + + @Override + public ClientWebRequest build() { + ClientWebRequest clientWebRequest = new ClientWebRequest(this.httpMethod, this.url); + clientWebRequest.setHttpHeaders(this.httpHeaders); + clientWebRequest.setCookies(this.cookies); + clientWebRequest.setBody(this.body); + clientWebRequest.setElementType(this.elementType); + for (ClientWebRequestPostProcessor postProcessor : this.postProcessors) { + clientWebRequest = postProcessor.postProcess(clientWebRequest); + } + return clientWebRequest; + } + +} \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultHttpRequestBuilder.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultHttpRequestBuilder.java deleted file mode 100644 index d76b53f50a..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultHttpRequestBuilder.java +++ /dev/null @@ -1,167 +0,0 @@ -/* - * 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.web.client.reactive; - -import java.net.URI; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; - -import org.reactivestreams.Publisher; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -import org.springframework.core.ResolvableType; -import org.springframework.core.codec.Encoder; -import org.springframework.http.HttpCookie; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.MediaType; -import org.springframework.http.client.reactive.ClientHttpRequest; -import org.springframework.http.client.reactive.ClientHttpRequestFactory; -import org.springframework.util.Assert; -import org.springframework.web.client.RestClientException; -import org.springframework.web.util.DefaultUriTemplateHandler; -import org.springframework.web.util.UriTemplateHandler; - -/** - * Builds a {@link ClientHttpRequest} - * - *

    See static factory methods in {@link HttpRequestBuilders} - * - * @author Brian Clozel - * @see HttpRequestBuilders - */ -public class DefaultHttpRequestBuilder implements HttpRequestBuilder { - - private final UriTemplateHandler uriTemplateHandler = new DefaultUriTemplateHandler(); - - protected HttpMethod httpMethod; - - protected HttpHeaders httpHeaders; - - protected URI url; - - protected Publisher contentPublisher; - - protected ResolvableType contentType; - - protected final List cookies = new ArrayList(); - - protected DefaultHttpRequestBuilder() { - } - - public DefaultHttpRequestBuilder(HttpMethod httpMethod, String urlTemplate, Object... urlVariables) throws RestClientException { - this.httpMethod = httpMethod; - this.httpHeaders = new HttpHeaders(); - this.url = this.uriTemplateHandler.expand(urlTemplate, urlVariables); - } - - public DefaultHttpRequestBuilder(HttpMethod httpMethod, URI url) { - this.httpMethod = httpMethod; - this.httpHeaders = new HttpHeaders(); - this.url = url; - } - - public DefaultHttpRequestBuilder header(String name, String... values) { - Arrays.stream(values).forEach(value -> this.httpHeaders.add(name, value)); - return this; - } - - public DefaultHttpRequestBuilder headers(HttpHeaders httpHeaders) { - this.httpHeaders = httpHeaders; - return this; - } - - public DefaultHttpRequestBuilder contentType(MediaType contentType) { - this.httpHeaders.setContentType(contentType); - return this; - } - - public DefaultHttpRequestBuilder contentType(String contentType) { - this.httpHeaders.setContentType(MediaType.parseMediaType(contentType)); - return this; - } - - public DefaultHttpRequestBuilder accept(MediaType... mediaTypes) { - this.httpHeaders.setAccept(Arrays.asList(mediaTypes)); - return this; - } - - public DefaultHttpRequestBuilder accept(String... mediaTypes) { - this.httpHeaders.setAccept(Arrays.stream(mediaTypes) - .map(type -> MediaType.parseMediaType(type)) - .collect(Collectors.toList())); - return this; - } - - public DefaultHttpRequestBuilder content(Object content) { - this.contentPublisher = Mono.just(content); - this.contentType = ResolvableType.forInstance(content); - return this; - } - - public DefaultHttpRequestBuilder contentStream(Publisher content, ResolvableType type) { - this.contentPublisher = Flux.from(content); - this.contentType = type; - return this; - } - - /** - * Allows performing more complex operations with a strategy. For example, a - * {@link RequestPostProcessor} implementation might accept the arguments of - * username and password and set an HTTP Basic authentication header. - * - * @param postProcessor the {@link RequestPostProcessor} to use. Cannot be null. - * - * @return this instance for further modifications. - */ - public DefaultHttpRequestBuilder apply(RequestPostProcessor postProcessor) { - Assert.notNull(postProcessor, "`postProcessor` is required"); - postProcessor.postProcess(this); - return this; - } - - public ClientHttpRequest build(ClientHttpRequestFactory factory, List> messageEncoders) { - ClientHttpRequest request = factory.createRequest(this.httpMethod, this.url, this.httpHeaders); - request.getHeaders().putAll(this.httpHeaders); - - if (this.contentPublisher != null) { - MediaType mediaType = request.getHeaders().getContentType(); - - Optional> messageEncoder = messageEncoders - .stream() - .filter(e -> e.canEncode(this.contentType, mediaType)) - .findFirst(); - - if (messageEncoder.isPresent()) { - request.writeWith(messageEncoder.get() - .encode(this.contentPublisher, request.bufferFactory(), - this.contentType, mediaType)); - } - else { - throw new WebClientException("Can't write request body " + - "of type '" + this.contentType.toString() + - "' for content-type '" + mediaType.toString() + "'"); - } - } - return request; - } - -} \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultWebResponse.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultWebResponse.java deleted file mode 100644 index 328d1198e2..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/DefaultWebResponse.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * 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.web.client.reactive; - -import java.util.List; - -import reactor.core.publisher.Mono; - -import org.springframework.core.codec.Decoder; -import org.springframework.http.client.reactive.ClientHttpResponse; - -/** - * Default implementation of the {@link WebResponse} interface - * - * @author Brian Clozel - */ -public class DefaultWebResponse implements WebResponse { - - private final Mono clientResponse; - - private final List> messageDecoders; - - - public DefaultWebResponse(Mono clientResponse, List> messageDecoders) { - this.clientResponse = clientResponse; - this.messageDecoders = messageDecoders; - } - - @Override - public Mono getClientResponse() { - return this.clientResponse; - } - - @Override - public List> getMessageDecoders() { - return this.messageDecoders; - } -} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/HttpRequestBuilder.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/HttpRequestBuilder.java deleted file mode 100644 index 32e2527a5e..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/HttpRequestBuilder.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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.web.client.reactive; - -import java.util.List; - -import org.springframework.core.codec.Encoder; -import org.springframework.http.client.reactive.ClientHttpRequest; -import org.springframework.http.client.reactive.ClientHttpRequestFactory; - -/** - * Build {@link ClientHttpRequest} using a {@link ClientHttpRequestFactory} - * which wraps an HTTP client implementation. - * - * @author Brian Clozel - */ -public interface HttpRequestBuilder { - - /** - * Build a {@link ClientHttpRequest} - * - * @param factory the factory that creates the actual {@link ClientHttpRequest} - * @param messageEncoders the {@link Encoder}s to use for encoding the request body - */ - ClientHttpRequest build(ClientHttpRequestFactory factory, List> messageEncoders); -} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/HttpRequestBuilders.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/HttpRequestBuilders.java deleted file mode 100644 index 13009f3651..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/HttpRequestBuilders.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * 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.web.client.reactive; - -import org.springframework.http.HttpMethod; - -/** - * Static factory methods for {@link DefaultHttpRequestBuilder RequestBuilders}. - * - * @author Brian Clozel - */ -public abstract class HttpRequestBuilders { - - /** - * Create a {@link DefaultHttpRequestBuilder} for a GET request. - * - * @param urlTemplate a URL template; the resulting URL will be encoded - * @param urlVariables zero or more URL variables - */ - public static DefaultHttpRequestBuilder get(String urlTemplate, Object... urlVariables) { - return new DefaultHttpRequestBuilder(HttpMethod.GET, urlTemplate, urlVariables); - } - - /** - * Create a {@link DefaultHttpRequestBuilder} for a POST request. - * - * @param urlTemplate a URL template; the resulting URL will be encoded - * @param urlVariables zero or more URL variables - */ - public static DefaultHttpRequestBuilder post(String urlTemplate, Object... urlVariables) { - return new DefaultHttpRequestBuilder(HttpMethod.POST, urlTemplate, urlVariables); - } - - - /** - * Create a {@link DefaultHttpRequestBuilder} for a PUT request. - * - * @param urlTemplate a URL template; the resulting URL will be encoded - * @param urlVariables zero or more URL variables - */ - public static DefaultHttpRequestBuilder put(String urlTemplate, Object... urlVariables) { - return new DefaultHttpRequestBuilder(HttpMethod.PUT, urlTemplate, urlVariables); - } - - /** - * Create a {@link DefaultHttpRequestBuilder} for a PATCH request. - * - * @param urlTemplate a URL template; the resulting URL will be encoded - * @param urlVariables zero or more URL variables - */ - public static DefaultHttpRequestBuilder patch(String urlTemplate, Object... urlVariables) { - return new DefaultHttpRequestBuilder(HttpMethod.PATCH, urlTemplate, urlVariables); - } - - /** - * Create a {@link DefaultHttpRequestBuilder} for a DELETE request. - * - * @param urlTemplate a URL template; the resulting URL will be encoded - * @param urlVariables zero or more URL variables - */ - public static DefaultHttpRequestBuilder delete(String urlTemplate, Object... urlVariables) { - return new DefaultHttpRequestBuilder(HttpMethod.DELETE, urlTemplate, urlVariables); - } - - /** - * Create a {@link DefaultHttpRequestBuilder} for an OPTIONS request. - * - * @param urlTemplate a URL template; the resulting URL will be encoded - * @param urlVariables zero or more URL variables - */ - public static DefaultHttpRequestBuilder options(String urlTemplate, Object... urlVariables) { - return new DefaultHttpRequestBuilder(HttpMethod.OPTIONS, urlTemplate, urlVariables); - } - - /** - * Create a {@link DefaultHttpRequestBuilder} for a HEAD request. - * - * @param urlTemplate a URL template; the resulting URL will be encoded - * @param urlVariables zero or more URL variables - */ - public static DefaultHttpRequestBuilder head(String urlTemplate, Object... urlVariables) { - return new DefaultHttpRequestBuilder(HttpMethod.HEAD, urlTemplate, urlVariables); - } - - /** - * Create a {@link DefaultHttpRequestBuilder} for a request with the given HTTP method. - * - * @param httpMethod the HTTP method - * @param urlTemplate a URL template; the resulting URL will be encoded - * @param urlVariables zero or more URL variables - */ - public static DefaultHttpRequestBuilder request(HttpMethod httpMethod, String urlTemplate, Object... urlVariables) { - return new DefaultHttpRequestBuilder(httpMethod, urlTemplate, urlVariables); - } - -} \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebResponse.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/ResponseExtractor.java similarity index 61% rename from spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebResponse.java rename to spring-web-reactive/src/main/java/org/springframework/web/client/reactive/ResponseExtractor.java index 6a92847dd1..3ed162c97d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/ResponseExtractor.java @@ -20,25 +20,19 @@ import java.util.List; import reactor.core.publisher.Mono; -import org.springframework.core.codec.Decoder; import org.springframework.http.client.reactive.ClientHttpResponse; +import org.springframework.http.converter.reactive.HttpMessageConverter; /** - * Result of a {@code ClientHttpRequest} sent to a remote server by the {@code WebClient} + * A {@code ResponseExtractor} extracts the relevant part of a + * raw {@link org.springframework.http.client.reactive.ClientHttpResponse}, + * optionally decoding the response body and using a target composition API. * - *

    Contains all the required information to extract relevant information from the raw response. + *

    See static factory methods in {@link ResponseExtractors}. * * @author Brian Clozel */ -public interface WebResponse { +public interface ResponseExtractor { - /** - * Return the raw response received by the {@code WebClient} - */ - Mono getClientResponse(); - - /** - * Return the configured list of {@link Decoder}s that can be used to decode the raw response body - */ - List> getMessageDecoders(); + T extract(Mono clientResponse, List> messageConverters); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/ResponseExtractors.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/ResponseExtractors.java new file mode 100644 index 0000000000..1e650ec91a --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/ResponseExtractors.java @@ -0,0 +1,163 @@ +/* + * 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.web.client.reactive; + +import java.util.List; +import java.util.Optional; + +import org.springframework.core.ResolvableType; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.reactive.ClientHttpResponse; +import org.springframework.http.converter.reactive.HttpMessageConverter; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * Static factory methods for {@link ResponseExtractor} based on the {@link Flux} and + * {@link Mono} API. + * + * @author Brian Clozel + */ +public class ResponseExtractors { + + private static final Object EMPTY_BODY = new Object(); + + /** + * Extract the response body and decode it, returning it as a {@code Mono} + * @see ResolvableType#forClassWithGenerics(Class, Class[]) + */ + public static ResponseExtractor> body(ResolvableType bodyType) { + // noinspection unchecked + return (clientResponse, messageConverters) -> (Mono) clientResponse + .flatMap(resp -> decodeResponseBody(resp, bodyType, + messageConverters)) + .next(); + } + + /** + * Extract the response body and decode it, returning it as a {@code Mono} + */ + public static ResponseExtractor> body(Class sourceClass) { + ResolvableType bodyType = ResolvableType.forClass(sourceClass); + return body(bodyType); + } + + /** + * Extract the response body and decode it, returning it as a {@code Flux} + * @see ResolvableType#forClassWithGenerics(Class, Class[]) + */ + public static ResponseExtractor> bodyStream(ResolvableType bodyType) { + return (clientResponse, messageConverters) -> clientResponse + .flatMap(resp -> decodeResponseBody(resp, bodyType, messageConverters)); + } + + /** + * Extract the response body and decode it, returning it as a {@code Flux} + */ + public static ResponseExtractor> bodyStream(Class sourceClass) { + ResolvableType bodyType = ResolvableType.forClass(sourceClass); + return bodyStream(bodyType); + } + + /** + * Extract the full response body as a {@code ResponseEntity} with its body decoded as + * a single type {@code T} + * @see ResolvableType#forClassWithGenerics(Class, Class[]) + */ + public static ResponseExtractor>> response( + ResolvableType bodyType) { + return (clientResponse, messageConverters) -> clientResponse.then(response -> { + return Mono.when( + decodeResponseBody(response, bodyType, + messageConverters).next().defaultIfEmpty( + EMPTY_BODY), + Mono.just(response.getHeaders()), + Mono.just(response.getStatusCode())); + }).map(tuple -> { + Object body = (tuple.getT1() != EMPTY_BODY ? tuple.getT1() : null); + // noinspection unchecked + return new ResponseEntity<>((T) body, tuple.getT2(), tuple.getT3()); + }); + } + + /** + * Extract the full response body as a {@code ResponseEntity} with its body decoded as + * a single type {@code T} + */ + public static ResponseExtractor>> response( + Class bodyClass) { + ResolvableType bodyType = ResolvableType.forClass(bodyClass); + return response(bodyType); + } + + /** + * Extract the full response body as a {@code ResponseEntity} with its body decoded as + * a {@code Flux} + * @see ResolvableType#forClassWithGenerics(Class, Class[]) + */ + public static ResponseExtractor>>> responseStream( + ResolvableType type) { + return (clientResponse, messageConverters) -> clientResponse + .map(response -> new ResponseEntity<>( + decodeResponseBody(response, type, + messageConverters), + response.getHeaders(), response.getStatusCode())); + } + + /** + * Extract the full response body as a {@code ResponseEntity} with its body decoded as + * a {@code Flux} + */ + public static ResponseExtractor>>> responseStream( + Class sourceClass) { + ResolvableType resolvableType = ResolvableType.forClass(sourceClass); + return responseStream(resolvableType); + } + + /** + * Extract the response headers as an {@code HttpHeaders} instance + */ + public static ResponseExtractor> headers() { + return (clientResponse, messageConverters) -> clientResponse.map(resp -> resp.getHeaders()); + } + + protected static Flux decodeResponseBody(ClientHttpResponse response, + ResolvableType responseType, + List> messageConverters) { + + MediaType contentType = response.getHeaders().getContentType(); + Optional> converter = resolveConverter(messageConverters, + responseType, contentType); + if (!converter.isPresent()) { + return Flux.error(new IllegalStateException( + "Could not decode response body of type '" + contentType + + "' with target type '" + responseType.toString() + "'")); + } + // noinspection unchecked + return (Flux) converter.get().read(responseType, response); + } + + protected static Optional> resolveConverter( + List> messageConverters, ResolvableType type, + MediaType mediaType) { + return messageConverters.stream().filter(e -> e.canRead(type, mediaType)) + .findFirst(); + } +} \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/RxJava1WebResponseExtractors.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/RxJava1WebResponseExtractors.java deleted file mode 100644 index 03e1bd9258..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/RxJava1WebResponseExtractors.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * 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.web.client.reactive; - -import java.nio.charset.Charset; -import java.util.List; -import java.util.Optional; - -import reactor.core.converter.RxJava1ObservableConverter; -import reactor.core.converter.RxJava1SingleConverter; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import rx.Observable; -import rx.Single; - -import org.springframework.core.ResolvableType; -import org.springframework.core.codec.Decoder; -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.http.client.reactive.ClientHttpResponse; - -/** - * Static factory methods for {@link WebResponseExtractor} - * based on the {@link Observable} and {@link Single} API. - * - * @author Brian Clozel - */ -public class RxJava1WebResponseExtractors { - - private static final Charset UTF_8 = Charset.forName("UTF-8"); - - private static final Object[] HINTS = new Object[] {UTF_8}; - - /** - * Extract the response body and decode it, returning it as a {@code Single} - */ - public static WebResponseExtractor> body(Class sourceClass) { - - ResolvableType resolvableType = ResolvableType.forClass(sourceClass); - //noinspection unchecked - return webResponse -> (Single) RxJava1SingleConverter.fromPublisher(webResponse.getClientResponse() - .flatMap(resp -> decodeResponseBody(resp, resolvableType, webResponse.getMessageDecoders())) - .next()); - } - - /** - * Extract the response body and decode it, returning it as an {@code Observable} - */ - public static WebResponseExtractor> bodyStream(Class sourceClass) { - - ResolvableType resolvableType = ResolvableType.forClass(sourceClass); - return webResponse -> RxJava1ObservableConverter.fromPublisher(webResponse.getClientResponse() - .flatMap(resp -> decodeResponseBody(resp, resolvableType, webResponse.getMessageDecoders()))); - } - - /** - * Extract the full response body as a {@code ResponseEntity} - * with its body decoded as a single type {@code T} - */ - public static WebResponseExtractor>> response(Class sourceClass) { - - ResolvableType resolvableType = ResolvableType.forClass(sourceClass); - return webResponse -> (Single>) - RxJava1SingleConverter.fromPublisher(webResponse.getClientResponse() - .then(response -> - Mono.when( - decodeResponseBody(response, resolvableType, webResponse.getMessageDecoders()).next(), - Mono.just(response.getHeaders()), - Mono.just(response.getStatusCode()))) - .map(tuple -> { - //noinspection unchecked - return new ResponseEntity<>((T) tuple.getT1(), tuple.getT2(), tuple.getT3()); - })); - } - - /** - * Extract the full response body as a {@code ResponseEntity} - * with its body decoded as an {@code Observable} - */ - public static WebResponseExtractor>>> responseStream(Class sourceClass) { - ResolvableType resolvableType = ResolvableType.forClass(sourceClass); - return webResponse -> RxJava1SingleConverter.fromPublisher(webResponse.getClientResponse() - .map(response -> new ResponseEntity<>( - RxJava1ObservableConverter - .fromPublisher(decodeResponseBody(response, resolvableType, webResponse.getMessageDecoders())), - response.getHeaders(), - response.getStatusCode()))); - } - - /** - * Extract the response headers as an {@code HttpHeaders} instance - */ - public static WebResponseExtractor> headers() { - return webResponse -> RxJava1SingleConverter - .fromPublisher(webResponse.getClientResponse().map(resp -> resp.getHeaders())); - } - - protected static Flux decodeResponseBody(ClientHttpResponse response, ResolvableType responseType, - List> messageDecoders) { - - MediaType contentType = response.getHeaders().getContentType(); - Optional> decoder = resolveDecoder(messageDecoders, responseType, contentType); - if (!decoder.isPresent()) { - return Flux.error(new IllegalStateException("Could not decode response body of type '" + contentType + - "' with target type '" + responseType.toString() + "'")); - } - //noinspection unchecked - return (Flux) decoder.get().decode(response.getBody(), responseType, contentType, HINTS); - } - - - protected static Optional> resolveDecoder(List> messageDecoders, ResolvableType type, - MediaType mediaType) { - return messageDecoders.stream().filter(e -> e.canDecode(type, mediaType)).findFirst(); - } -} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebClient.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebClient.java index 270eeb53aa..94f4840c54 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebClient.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebClient.java @@ -16,12 +16,17 @@ package org.springframework.web.client.reactive; -import java.util.Arrays; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Optional; import java.util.function.Consumer; +import java.util.function.Function; +import java.util.logging.Level; -import reactor.core.publisher.Mono; +import org.reactivestreams.Publisher; +import org.springframework.core.ResolvableType; import org.springframework.core.codec.Decoder; import org.springframework.core.codec.Encoder; import org.springframework.core.codec.ByteBufferDecoder; @@ -31,105 +36,194 @@ import org.springframework.http.codec.json.JacksonJsonEncoder; import org.springframework.core.codec.StringDecoder; import org.springframework.core.codec.StringEncoder; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.client.reactive.ClientHttpRequest; -import org.springframework.http.client.reactive.ClientHttpRequestFactory; import org.springframework.http.client.reactive.ClientHttpResponse; +import org.springframework.http.client.reactive.ClientHttpConnector; +import org.springframework.http.codec.xml.Jaxb2Decoder; +import org.springframework.http.codec.xml.Jaxb2Encoder; +import org.springframework.http.converter.reactive.CodecHttpMessageConverter; +import org.springframework.http.converter.reactive.HttpMessageConverter; +import org.springframework.http.converter.reactive.ResourceHttpMessageConverter; +import org.springframework.util.ClassUtils; + +import reactor.core.publisher.Mono; /** * Reactive Web client supporting the HTTP/1.1 protocol * *

    Here is a simple example of a GET request: + * *

    - * WebClient client = new WebClient(new ReactorHttpClientRequestFactory());
    + * // should be shared between HTTP calls
    + * WebClient client = new WebClient(new ReactorHttpClient());
    + *
      * Mono<String> result = client
    - * 		.perform(HttpRequestBuilders.get("http://example.org/resource")
    - * 			.accept(MediaType.TEXT_PLAIN))
    - * 		.extract(WebResponseExtractors.body(String.class));
    + * 		.perform(ClientWebRequestBuilders.get("http://example.org/resource")
    + * 				.accept(MediaType.TEXT_PLAIN))
    + * 		.extract(ResponseExtractors.body(String.class));
      * 
    * *

    This Web client relies on *

      - *
    • a {@link ClientHttpRequestFactory} that drives the underlying library (e.g. Reactor-Net, RxNetty...)
    • - *
    • an {@link HttpRequestBuilder} which create a Web request with a builder API (see {@link HttpRequestBuilders})
    • - *
    • an {@link WebResponseExtractor} which extracts the relevant part of the server response - * with the composition API of choice (see {@link WebResponseExtractors}
    • + *
    • an {@link ClientHttpConnector} implementation that drives the underlying library (e.g. Reactor-Netty)
    • + *
    • a {@link ClientWebRequestBuilder} which creates a Web request with a builder API (see + * {@link ClientWebRequestBuilders})
    • + *
    • an {@link ResponseExtractor} which extracts the relevant part of the server + * response with the composition API of choice (see {@link ResponseExtractors}
    • *
    * * @author Brian Clozel - * @see HttpRequestBuilders - * @see WebResponseExtractors + * @see ClientWebRequestBuilders + * @see ResponseExtractors */ public final class WebClient { - private ClientHttpRequestFactory requestFactory; + private static final ClassLoader classLoader = WebClient.class.getClassLoader(); - private List> messageEncoders; + private static final boolean jackson2Present = ClassUtils + .isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader) + && ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", + classLoader); - private List> messageDecoders; + private static final boolean jaxb2Present = ClassUtils + .isPresent("javax.xml.bind.Binder", classLoader); + + private ClientHttpConnector clientHttpConnector; + + private List> messageConverters; /** - * Create a {@code ReactiveRestClient} instance, using the {@link ClientHttpRequestFactory} - * implementation given as an argument to drive the underlying HTTP client implementation. + * Create a {@code WebClient} instance, using the {@link ClientHttpConnector} + * implementation given as an argument to drive the underlying + * implementation. * * Register by default the following Encoders and Decoders: *
      - *
    • {@link ByteBufferEncoder} / {@link ByteBufferDecoder}
    • - *
    • {@link StringEncoder} / {@link StringDecoder}
    • - *
    • {@link JacksonJsonEncoder} / {@link JacksonJsonDecoder}
    • + *
    • {@link ByteBufferEncoder} / {@link ByteBufferDecoder}
    • + *
    • {@link StringEncoder} / {@link StringDecoder}
    • + *
    • {@link Jaxb2Encoder} / {@link Jaxb2Decoder}
    • + *
    • {@link JacksonJsonEncoder} / {@link JacksonJsonDecoder}
    • *
    * - * @param requestFactory the {@code ClientHttpRequestFactory} to use + * @param clientHttpConnector the {@code ClientHttpRequestFactory} to use */ - public WebClient(ClientHttpRequestFactory requestFactory) { - this.requestFactory = requestFactory; - this.messageEncoders = Arrays.asList(new ByteBufferEncoder(), new StringEncoder(), - new JacksonJsonEncoder()); - this.messageDecoders = Arrays.asList(new ByteBufferDecoder(), new StringDecoder(), - new JacksonJsonDecoder()); + public WebClient(ClientHttpConnector clientHttpConnector) { + this.clientHttpConnector = clientHttpConnector; + this.messageConverters = new ArrayList<>(); + addDefaultHttpMessageConverters(this.messageConverters); } /** - * Set the list of {@link Encoder}s to use for encoding messages + * Adds default HTTP message converters */ - public void setMessageEncoders(List> messageEncoders) { - this.messageEncoders = messageEncoders; + protected final void addDefaultHttpMessageConverters( + List> converters) { + converters.add(converter(new ByteBufferEncoder(), new ByteBufferDecoder())); + converters.add(converter(new StringEncoder(), new StringDecoder())); + converters.add(new ResourceHttpMessageConverter()); + if (jaxb2Present) { + converters.add(converter(new Jaxb2Encoder(), new Jaxb2Decoder())); + } + if (jackson2Present) { + converters.add(converter(new JacksonJsonEncoder(), new JacksonJsonDecoder())); + } + } + + private static HttpMessageConverter converter(Encoder encoder, + Decoder decoder) { + return new CodecHttpMessageConverter<>(encoder, decoder); } /** - * Set the list of {@link Decoder}s to use for decoding messages + * Set the list of {@link HttpMessageConverter}s to use for encoding and decoding HTTP + * messages */ - public void setMessageDecoders(List> messageDecoders) { - this.messageDecoders = messageDecoders; + public void setMessageConverters(List> messageConverters) { + this.messageConverters = messageConverters; } /** * Perform the actual HTTP request/response exchange * - *

    Pulling demand from the exposed {@code Flux} will result in: + *

    + * Requesting from the exposed {@code Flux} will result in: *

      - *
    • building the actual HTTP request using the provided {@code RequestBuilder}
    • - *
    • encoding the HTTP request body with the configured {@code Encoder}s
    • - *
    • returning the response with a publisher of the body
    • + *
    • building the actual HTTP request using the provided {@code ClientWebRequestBuilder}
    • + *
    • encoding the HTTP request body with the configured {@code HttpMessageConverter}s
    • + *
    • returning the response with a publisher of the body
    • *
    */ - public WebResponseActions perform(HttpRequestBuilder builder) { + public WebResponseActions perform(ClientWebRequestBuilder builder) { - ClientHttpRequest request = builder.build(this.requestFactory, this.messageEncoders); - final Mono clientResponse = request.execute() - .log("org.springframework.http.client.reactive"); + ClientWebRequest clientWebRequest = builder.build(); + + final Mono clientResponse = this.clientHttpConnector + .connect(clientWebRequest.getMethod(), clientWebRequest.getUrl(), + new DefaultRequestCallback(clientWebRequest)) + .log("org.springframework.web.client.reactive", Level.FINE); return new WebResponseActions() { @Override public void doWithStatus(Consumer consumer) { - // TODO: implement + clientResponse.doOnNext(clientHttpResponse -> + consumer.accept(clientHttpResponse.getStatusCode())); } @Override - public T extract(WebResponseExtractor extractor) { - return extractor.extract(new DefaultWebResponse(clientResponse, messageDecoders)); + public T extract(ResponseExtractor extractor) { + return extractor.extract(clientResponse, messageConverters); } - }; } -} + protected class DefaultRequestCallback implements Function> { + + private final ClientWebRequest clientWebRequest; + + public DefaultRequestCallback(ClientWebRequest clientWebRequest) { + this.clientWebRequest = clientWebRequest; + } + + @Override + public Mono apply(ClientHttpRequest clientHttpRequest) { + clientHttpRequest.getHeaders().putAll(this.clientWebRequest.getHttpHeaders()); + if (clientHttpRequest.getHeaders().getAccept().isEmpty()) { + clientHttpRequest.getHeaders().setAccept( + Collections.singletonList(MediaType.ALL)); + } + clientWebRequest.getCookies().values() + .stream().flatMap(cookies -> cookies.stream()) + .forEach(cookie -> clientHttpRequest.getCookies().add(cookie.getName(), cookie)); + if (this.clientWebRequest.getBody() != null) { + return writeRequestBody(this.clientWebRequest.getBody(), + this.clientWebRequest.getElementType(), clientHttpRequest, messageConverters); + } + else { + return clientHttpRequest.setComplete(); + } + } + + protected Mono writeRequestBody(Publisher content, + ResolvableType requestType, ClientHttpRequest request, + List> messageConverters) { + + MediaType contentType = request.getHeaders().getContentType(); + Optional> converter = resolveConverter(messageConverters, requestType, contentType); + if (!converter.isPresent()) { + return Mono.error(new IllegalStateException( + "Could not encode request body of type '" + contentType + + "' with target type '" + requestType.toString() + "'")); + } + // noinspection unchecked + return converter.get().write((Publisher) content, requestType, contentType, request); + } + + protected Optional> resolveConverter( + List> messageConverters, ResolvableType type, + MediaType mediaType) { + return messageConverters.stream().filter(e -> e.canWrite(type, mediaType)).findFirst(); + } + } + +} \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebResponseActions.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebResponseActions.java index e18e2decdf..0c6e709ff1 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebResponseActions.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebResponseActions.java @@ -45,5 +45,5 @@ public interface WebResponseActions { * .extract(response(String.class)); * */ - T extract(WebResponseExtractor extractor); + T extract(ResponseExtractor extractor); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebResponseExtractors.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebResponseExtractors.java deleted file mode 100644 index 2d3d7aeda8..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/WebResponseExtractors.java +++ /dev/null @@ -1,161 +0,0 @@ -/* - * 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.web.client.reactive; - -import java.nio.charset.Charset; -import java.util.List; -import java.util.Optional; - -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -import org.springframework.core.ResolvableType; -import org.springframework.core.codec.Decoder; -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.http.client.reactive.ClientHttpResponse; - -/** - * Static factory methods for {@link WebResponseExtractor} - * based on the {@link Flux} and {@link Mono} API. - * - * @author Brian Clozel - */ -public class WebResponseExtractors { - - private static final Charset UTF_8 = Charset.forName("UTF-8"); - - private static final Object[] HINTS = new Object[] {UTF_8}; - - private static final Object EMPTY_BODY = new Object(); - - - /** - * Extract the response body and decode it, returning it as a {@code Mono} - * @see ResolvableType#forClassWithGenerics(Class, Class[]) - */ - public static WebResponseExtractor> body(ResolvableType bodyType) { - //noinspection unchecked - return webResponse -> (Mono) webResponse.getClientResponse() - .flatMap(resp -> decodeResponseBody(resp, bodyType, webResponse.getMessageDecoders())) - .next(); - } - - /** - * Extract the response body and decode it, returning it as a {@code Mono} - */ - public static WebResponseExtractor> body(Class sourceClass) { - ResolvableType bodyType = ResolvableType.forClass(sourceClass); - return body(bodyType); - } - - - /** - * Extract the response body and decode it, returning it as a {@code Flux} - * @see ResolvableType#forClassWithGenerics(Class, Class[]) - */ - public static WebResponseExtractor> bodyStream(ResolvableType bodyType) { - return webResponse -> webResponse.getClientResponse() - .flatMap(resp -> decodeResponseBody(resp, bodyType, webResponse.getMessageDecoders())); - } - - /** - * Extract the response body and decode it, returning it as a {@code Flux} - */ - public static WebResponseExtractor> bodyStream(Class sourceClass) { - ResolvableType bodyType = ResolvableType.forClass(sourceClass); - return bodyStream(bodyType); - } - - /** - * Extract the full response body as a {@code ResponseEntity} - * with its body decoded as a single type {@code T} - * @see ResolvableType#forClassWithGenerics(Class, Class[]) - */ - public static WebResponseExtractor>> response(ResolvableType bodyType) { - return webResponse -> webResponse.getClientResponse() - .then(response -> { - List> decoders = webResponse.getMessageDecoders(); - return Mono.when( - decodeResponseBody(response, bodyType, decoders).next().defaultIfEmpty(EMPTY_BODY), - Mono.just(response.getHeaders()), - Mono.just(response.getStatusCode())); - }) - .map(tuple -> { - Object body = (tuple.getT1() != EMPTY_BODY ? tuple.getT1() : null); - //noinspection unchecked - return new ResponseEntity<>((T) body, tuple.getT2(), tuple.getT3()); - }); - } - - /** - * Extract the full response body as a {@code ResponseEntity} - * with its body decoded as a single type {@code T} - */ - public static WebResponseExtractor>> response(Class bodyClass) { - ResolvableType bodyType = ResolvableType.forClass(bodyClass); - return response(bodyType); - } - - /** - * Extract the full response body as a {@code ResponseEntity} - * with its body decoded as a {@code Flux} - * @see ResolvableType#forClassWithGenerics(Class, Class[]) - */ - public static WebResponseExtractor>>> responseStream(ResolvableType type) { - return webResponse -> webResponse.getClientResponse() - .map(response -> new ResponseEntity<>( - decodeResponseBody(response, type, webResponse.getMessageDecoders()), - response.getHeaders(), response.getStatusCode())); - } - - /** - * Extract the full response body as a {@code ResponseEntity} - * with its body decoded as a {@code Flux} - */ - public static WebResponseExtractor>>> responseStream(Class sourceClass) { - ResolvableType resolvableType = ResolvableType.forClass(sourceClass); - return responseStream(resolvableType); - } - - /** - * Extract the response headers as an {@code HttpHeaders} instance - */ - public static WebResponseExtractor> headers() { - return webResponse -> webResponse.getClientResponse().map(resp -> resp.getHeaders()); - } - - protected static Flux decodeResponseBody(ClientHttpResponse response, ResolvableType responseType, - List> messageDecoders) { - - MediaType contentType = response.getHeaders().getContentType(); - Optional> decoder = resolveDecoder(messageDecoders, responseType, contentType); - if (!decoder.isPresent()) { - return Flux.error(new IllegalStateException("Could not decode response body of type '" + contentType + - "' with target type '" + responseType.toString() + "'")); - } - //noinspection unchecked - return (Flux) decoder.get().decode(response.getBody(), responseType, contentType, HINTS); - } - - - protected static Optional> resolveDecoder(List> messageDecoders, ResolvableType type, - MediaType mediaType) { - return messageDecoders.stream().filter(e -> e.canDecode(type, mediaType)).findFirst(); - } -} diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/FlushingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/FlushingIntegrationTests.java index d3a45693ec..831e5a55ad 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/FlushingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/FlushingIntegrationTests.java @@ -18,15 +18,16 @@ package org.springframework.http.server.reactive; import org.junit.Before; import org.junit.Test; -import static org.springframework.web.client.reactive.HttpRequestBuilders.get; -import static org.springframework.web.client.reactive.WebResponseExtractors.bodyStream; + +import static org.springframework.web.client.reactive.ClientWebRequestBuilders.get; +import static org.springframework.web.client.reactive.ResponseExtractors.bodyStream; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.test.TestSubscriber; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.FlushingDataBuffer; -import org.springframework.http.client.reactive.ReactorHttpClientRequestFactory; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.web.client.reactive.WebClient; /** @@ -39,7 +40,7 @@ public class FlushingIntegrationTests extends AbstractHttpHandlerIntegrationTest @Before public void setup() throws Exception { super.setup(); - this.webClient = new WebClient(new ReactorHttpClientRequestFactory()); + this.webClient = new WebClient(new ReactorClientHttpConnector()); } @Test diff --git a/spring-web-reactive/src/test/java/org/springframework/web/client/reactive/DefaultHttpRequestBuilderTests.java b/spring-web-reactive/src/test/java/org/springframework/web/client/reactive/DefaultWebRequestBuilderTests.java similarity index 55% rename from spring-web-reactive/src/test/java/org/springframework/web/client/reactive/DefaultHttpRequestBuilderTests.java rename to spring-web-reactive/src/test/java/org/springframework/web/client/reactive/DefaultWebRequestBuilderTests.java index 112c62d6d7..ba7364c315 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/client/reactive/DefaultHttpRequestBuilderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/client/reactive/DefaultWebRequestBuilderTests.java @@ -16,33 +16,41 @@ package org.springframework.web.client.reactive; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.*; import org.junit.Before; import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + import org.springframework.http.HttpMethod; /** * * @author Rob Winch - * */ -public class DefaultHttpRequestBuilderTests { - private DefaultHttpRequestBuilder builder; +public class DefaultWebRequestBuilderTests { + private DefaultClientWebRequestBuilder builder; @Before public void setup() { - builder = new DefaultHttpRequestBuilder(HttpMethod.GET, "https://example.com/foo"); + builder = new DefaultClientWebRequestBuilder(HttpMethod.GET, "https://example.com/foo"); } @Test public void apply() { - RequestPostProcessor postProcessor = mock(RequestPostProcessor.class); + ClientWebRequestPostProcessor postProcessor = mock(ClientWebRequestPostProcessor.class); + when(postProcessor.postProcess(any(ClientWebRequest.class))).thenAnswer(new Answer() { + @Override + public ClientWebRequest answer(InvocationOnMock invocation) throws Throwable { + return (ClientWebRequest) invocation.getArguments()[0]; + } + }); - builder.apply(postProcessor); + ClientWebRequest webRequest = builder.apply(postProcessor).build(); - verify(postProcessor).postProcess(builder); + verify(postProcessor).postProcess(webRequest); } @Test(expected = IllegalArgumentException.class) diff --git a/spring-web-reactive/src/test/java/org/springframework/web/client/reactive/WebClientIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/client/reactive/WebClientIntegrationTests.java index f038ab1cb5..88d97aa84e 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/client/reactive/WebClientIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/client/reactive/WebClientIntegrationTests.java @@ -17,8 +17,8 @@ package org.springframework.web.client.reactive; import static org.junit.Assert.*; -import static org.springframework.web.client.reactive.HttpRequestBuilders.*; -import static org.springframework.web.client.reactive.WebResponseExtractors.*; +import static org.springframework.web.client.reactive.ClientWebRequestBuilders.*; +import static org.springframework.web.client.reactive.ResponseExtractors.*; import java.util.function.Consumer; @@ -38,9 +38,11 @@ import org.springframework.http.codec.Pojo; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.http.client.reactive.ReactorHttpClientRequestFactory; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; /** + * {@link WebClient} integration tests with the {@code Flux} and {@code Mono} API. + * * @author Brian Clozel */ public class WebClientIntegrationTests { @@ -52,7 +54,7 @@ public class WebClientIntegrationTests { @Before public void setup() { this.server = new MockWebServer(); - this.webClient = new WebClient(new ReactorHttpClientRequestFactory()); + this.webClient = new WebClient(new ReactorClientHttpConnector()); } @Test @@ -228,12 +230,14 @@ public class WebClientIntegrationTests { public void shouldPostPojoAsJson() throws Exception { HttpUrl baseUrl = server.url("/pojo/capitalize"); - this.server.enqueue(new MockResponse().setBody("{\"bar\":\"BARBAR\",\"foo\":\"FOOFOO\"}")); + this.server.enqueue(new MockResponse() + .setHeader("Content-Type", "application/json") + .setBody("{\"bar\":\"BARBAR\",\"foo\":\"FOOFOO\"}")); Pojo spring = new Pojo("foofoo", "barbar"); Mono result = this.webClient .perform(post(baseUrl.toString()) - .content(spring) + .body(spring) .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON)) .extract(body(Pojo.class)); @@ -252,6 +256,28 @@ public class WebClientIntegrationTests { assertEquals("application/json", request.getHeader(HttpHeaders.CONTENT_TYPE)); } + @Test + public void shouldSendCookieHeader() throws Exception { + HttpUrl baseUrl = server.url("/test"); + this.server.enqueue(new MockResponse() + .setHeader("Content-Type", "text/plain").setBody("test")); + + Mono result = this.webClient + .perform(get(baseUrl.toString()) + .cookie("testkey", "testvalue")) + .extract(body(String.class)); + + TestSubscriber + .subscribe(result) + .awaitAndAssertNextValues("test") + .assertComplete(); + + RecordedRequest request = server.takeRequest(); + assertEquals(1, server.getRequestCount()); + assertEquals("/test", request.getPath()); + assertEquals("testkey=testvalue", request.getHeader(HttpHeaders.COOKIE)); + } + @Test public void shouldGetErrorWhen404() throws Exception { @@ -262,7 +288,6 @@ public class WebClientIntegrationTests { .perform(get(baseUrl.toString())) .extract(body(String.class)); - // TODO: error message should be converted to a ClientException TestSubscriber .subscribe(result) .await() diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java index 79beae54ef..9de15cab87 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java @@ -16,7 +16,11 @@ package org.springframework.web.reactive.result.method.annotation; +import static org.springframework.web.client.reactive.ClientWebRequestBuilders.*; +import static org.springframework.web.client.reactive.ResponseExtractors.*; + import java.time.Duration; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -29,14 +33,16 @@ import reactor.core.test.TestSubscriber; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.core.codec.Encoder; import org.springframework.core.codec.ByteBufferDecoder; +import org.springframework.core.codec.ByteBufferEncoder; +import org.springframework.core.codec.Encoder; +import org.springframework.core.codec.StringDecoder; +import org.springframework.core.codec.StringEncoder; +import org.springframework.http.MediaType; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.http.codec.SseEventEncoder; import org.springframework.http.codec.json.JacksonJsonDecoder; import org.springframework.http.codec.json.JacksonJsonEncoder; -import org.springframework.core.codec.StringDecoder; -import org.springframework.http.MediaType; -import org.springframework.http.client.reactive.ReactorHttpClientRequestFactory; -import org.springframework.http.codec.SseEventEncoder; import org.springframework.http.converter.reactive.CodecHttpMessageConverter; import org.springframework.http.converter.reactive.HttpMessageConverter; import org.springframework.http.server.reactive.AbstractHttpHandlerIntegrationTests; @@ -49,9 +55,6 @@ import org.springframework.web.reactive.config.WebReactiveConfiguration; import org.springframework.web.reactive.sse.SseEvent; import org.springframework.web.server.adapter.WebHttpHandlerBuilder; -import static org.springframework.web.client.reactive.HttpRequestBuilders.get; -import static org.springframework.web.client.reactive.WebResponseExtractors.bodyStream; - /** * @author Sebastien Deleuze */ @@ -64,11 +67,12 @@ public class SseIntegrationTests extends AbstractHttpHandlerIntegrationTests { @Before public void setup() throws Exception { super.setup(); - this.webClient = new WebClient(new ReactorHttpClientRequestFactory()); - this.webClient.setMessageDecoders(Arrays.asList( - new ByteBufferDecoder(), - new StringDecoder(false), - new JacksonJsonDecoder())); + this.webClient = new WebClient(new ReactorClientHttpConnector()); + List> converters = new ArrayList<>(); + converters.add(new CodecHttpMessageConverter<>(new ByteBufferEncoder(), new ByteBufferDecoder())); + converters.add(new CodecHttpMessageConverter<>(new StringEncoder(), new StringDecoder(false))); + converters.add(new CodecHttpMessageConverter<>(new JacksonJsonEncoder(), new JacksonJsonDecoder())); + this.webClient.setMessageConverters(converters); } @Override From 99697118d8b722242241a46923124d9f6f7ff539 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Fri, 8 Jul 2016 11:42:32 +0200 Subject: [PATCH 483/498] Add RxJava1 support for request building/extracting This commit adds the required infrastructure to build HTTP requests as well as extracting relevant information from HTTP responses using the RxJava 1.x API, where Observable and Single don't extend Publisher. --- .../RxJava1ClientWebRequestBuilder.java | 170 +++++++++ .../RxJava1ClientWebRequestBuilders.java | 110 ++++++ .../support/RxJava1ResponseExtractors.java | 128 +++++++ .../RxJava1WebClientIntegrationTests.java | 340 ++++++++++++++++++ 4 files changed, 748 insertions(+) create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/client/reactive/support/RxJava1ClientWebRequestBuilder.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/client/reactive/support/RxJava1ClientWebRequestBuilders.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/client/reactive/support/RxJava1ResponseExtractors.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/client/reactive/RxJava1WebClientIntegrationTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/support/RxJava1ClientWebRequestBuilder.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/support/RxJava1ClientWebRequestBuilder.java new file mode 100644 index 0000000000..d7f6ed4828 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/support/RxJava1ClientWebRequestBuilder.java @@ -0,0 +1,170 @@ +/* + * 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.web.client.reactive.support; + +import java.net.URI; + +import reactor.core.converter.RxJava1ObservableConverter; +import reactor.core.converter.RxJava1SingleConverter; +import reactor.core.publisher.Mono; +import rx.Observable; +import rx.Single; + +import org.springframework.core.ResolvableType; +import org.springframework.http.HttpCookie; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.client.reactive.ClientHttpRequest; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.reactive.ClientWebRequest; +import org.springframework.web.client.reactive.ClientWebRequestBuilder; +import org.springframework.web.client.reactive.ClientWebRequestPostProcessor; +import org.springframework.web.client.reactive.DefaultClientWebRequestBuilder; + +/** + * Builds a {@link ClientHttpRequest} using a {@code Observable} + * or {@code Single} as request body. + * + *

    See static factory methods in {@link RxJava1ClientWebRequestBuilders} + * + * @author Brian Clozel + * @see RxJava1ClientWebRequestBuilders + */ +public class RxJava1ClientWebRequestBuilder implements ClientWebRequestBuilder { + + private final DefaultClientWebRequestBuilder delegate; + + public RxJava1ClientWebRequestBuilder(HttpMethod httpMethod, String urlTemplate, + Object... urlVariables) throws RestClientException { + this.delegate = new DefaultClientWebRequestBuilder(httpMethod, urlTemplate, urlVariables); + } + + public RxJava1ClientWebRequestBuilder(HttpMethod httpMethod, URI url) { + this.delegate = new DefaultClientWebRequestBuilder(httpMethod, url); + } + + /** + * Add an HTTP request header + */ + public RxJava1ClientWebRequestBuilder header(String name, String... values) { + this.delegate.header(name, values); + return this; + } + + /** + * Add all provided HTTP request headers + */ + public RxJava1ClientWebRequestBuilder headers(HttpHeaders httpHeaders) { + this.delegate.headers(httpHeaders); + return this; + } + + /** + * Set the Content-Type request header to the given {@link MediaType} + */ + public RxJava1ClientWebRequestBuilder contentType(MediaType contentType) { + this.delegate.contentType(contentType); + return this; + } + + /** + * Set the Content-Type request header to the given media type + */ + public RxJava1ClientWebRequestBuilder contentType(String contentType) { + this.delegate.contentType(contentType); + return this; + } + + /** + * Set the Accept request header to the given {@link MediaType}s + */ + public RxJava1ClientWebRequestBuilder accept(MediaType... mediaTypes) { + this.delegate.accept(mediaTypes); + return this; + } + + /** + * Set the Accept request header to the given media types + */ + public RxJava1ClientWebRequestBuilder accept(String... mediaTypes) { + this.delegate.accept(mediaTypes); + return this; + } + + /** + * Add a Cookie to the HTTP request + */ + public RxJava1ClientWebRequestBuilder cookie(String name, String value) { + this.delegate.cookie(name, value); + return this; + } + + /** + * Add a Cookie to the HTTP request + */ + public RxJava1ClientWebRequestBuilder cookie(HttpCookie cookie) { + this.delegate.cookie(cookie); + return this; + } + + /** + * Allows performing more complex operations with a strategy. For example, a + * {@link ClientWebRequestPostProcessor} implementation might accept the arguments of username + * and password and set an HTTP Basic authentication header. + * + * @param postProcessor the {@link ClientWebRequestPostProcessor} to use. Cannot be null. + * + * @return this instance for further modifications. + */ + public RxJava1ClientWebRequestBuilder apply(ClientWebRequestPostProcessor postProcessor) { + this.delegate.apply(postProcessor); + return this; + } + + /** + * Use the given object as the request body + */ + public RxJava1ClientWebRequestBuilder body(Object content) { + this.delegate.body(Mono.just(content), ResolvableType.forInstance(content)); + return this; + } + + /** + * Use the given {@link Single} as the request body and use its {@link ResolvableType} + * as type information for the element published by this reactive stream + */ + public RxJava1ClientWebRequestBuilder body(Single content, ResolvableType elementType) { + this.delegate.body(RxJava1SingleConverter.toPublisher(content), elementType); + return this; + } + + /** + * Use the given {@link Observable} as the request body and use its {@link ResolvableType} + * as type information for the elements published by this reactive stream + */ + public RxJava1ClientWebRequestBuilder body(Observable content, ResolvableType elementType) { + this.delegate.body(RxJava1ObservableConverter.toPublisher(content), elementType); + return this; + } + + @Override + public ClientWebRequest build() { + return this.delegate.build(); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/support/RxJava1ClientWebRequestBuilders.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/support/RxJava1ClientWebRequestBuilders.java new file mode 100644 index 0000000000..4a6a8281b8 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/support/RxJava1ClientWebRequestBuilders.java @@ -0,0 +1,110 @@ +/* + * 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.web.client.reactive.support; + +import org.springframework.http.HttpMethod; + +/** + * Static factory methods for {@link RxJava1ClientWebRequestBuilder ClientWebRequestBuilders} + * using the {@link rx.Observable} and {@link rx.Single} API. + * + * @author Brian Clozel + */ +public abstract class RxJava1ClientWebRequestBuilders { + + /** + * Create a {@link RxJava1ClientWebRequestBuilder} for a GET request. + * + * @param urlTemplate a URL template; the resulting URL will be encoded + * @param urlVariables zero or more URL variables + */ + public static RxJava1ClientWebRequestBuilder get(String urlTemplate, Object... urlVariables) { + return new RxJava1ClientWebRequestBuilder(HttpMethod.GET, urlTemplate, urlVariables); + } + + /** + * Create a {@link RxJava1ClientWebRequestBuilder} for a POST request. + * + * @param urlTemplate a URL template; the resulting URL will be encoded + * @param urlVariables zero or more URL variables + */ + public static RxJava1ClientWebRequestBuilder post(String urlTemplate, Object... urlVariables) { + return new RxJava1ClientWebRequestBuilder(HttpMethod.POST, urlTemplate, urlVariables); + } + + + /** + * Create a {@link RxJava1ClientWebRequestBuilder} for a PUT request. + * + * @param urlTemplate a URL template; the resulting URL will be encoded + * @param urlVariables zero or more URL variables + */ + public static RxJava1ClientWebRequestBuilder put(String urlTemplate, Object... urlVariables) { + return new RxJava1ClientWebRequestBuilder(HttpMethod.PUT, urlTemplate, urlVariables); + } + + /** + * Create a {@link RxJava1ClientWebRequestBuilder} for a PATCH request. + * + * @param urlTemplate a URL template; the resulting URL will be encoded + * @param urlVariables zero or more URL variables + */ + public static RxJava1ClientWebRequestBuilder patch(String urlTemplate, Object... urlVariables) { + return new RxJava1ClientWebRequestBuilder(HttpMethod.PATCH, urlTemplate, urlVariables); + } + + /** + * Create a {@link RxJava1ClientWebRequestBuilder} for a DELETE request. + * + * @param urlTemplate a URL template; the resulting URL will be encoded + * @param urlVariables zero or more URL variables + */ + public static RxJava1ClientWebRequestBuilder delete(String urlTemplate, Object... urlVariables) { + return new RxJava1ClientWebRequestBuilder(HttpMethod.DELETE, urlTemplate, urlVariables); + } + + /** + * Create a {@link RxJava1ClientWebRequestBuilder} for an OPTIONS request. + * + * @param urlTemplate a URL template; the resulting URL will be encoded + * @param urlVariables zero or more URL variables + */ + public static RxJava1ClientWebRequestBuilder options(String urlTemplate, Object... urlVariables) { + return new RxJava1ClientWebRequestBuilder(HttpMethod.OPTIONS, urlTemplate, urlVariables); + } + + /** + * Create a {@link RxJava1ClientWebRequestBuilder} for a HEAD request. + * + * @param urlTemplate a URL template; the resulting URL will be encoded + * @param urlVariables zero or more URL variables + */ + public static RxJava1ClientWebRequestBuilder head(String urlTemplate, Object... urlVariables) { + return new RxJava1ClientWebRequestBuilder(HttpMethod.HEAD, urlTemplate, urlVariables); + } + + /** + * Create a {@link RxJava1ClientWebRequestBuilder} for a request with the given HTTP method. + * + * @param httpMethod the HTTP method + * @param urlTemplate a URL template; the resulting URL will be encoded + * @param urlVariables zero or more URL variables + */ + public static RxJava1ClientWebRequestBuilder request(HttpMethod httpMethod, String urlTemplate, Object... urlVariables) { + return new RxJava1ClientWebRequestBuilder(httpMethod, urlTemplate, urlVariables); + } +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/support/RxJava1ResponseExtractors.java b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/support/RxJava1ResponseExtractors.java new file mode 100644 index 0000000000..96de49f9a0 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/client/reactive/support/RxJava1ResponseExtractors.java @@ -0,0 +1,128 @@ +/* + * 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.web.client.reactive.support; + +import java.util.List; +import java.util.Optional; + +import org.springframework.core.ResolvableType; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.reactive.ClientHttpResponse; +import org.springframework.http.converter.reactive.HttpMessageConverter; +import org.springframework.web.client.reactive.ResponseExtractor; + +import reactor.core.converter.RxJava1ObservableConverter; +import reactor.core.converter.RxJava1SingleConverter; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import rx.Observable; +import rx.Single; + +/** + * Static factory methods for {@link ResponseExtractor} + * based on the {@link Observable} and {@link Single} API. + * + * @author Brian Clozel + */ +public class RxJava1ResponseExtractors { + + /** + * Extract the response body and decode it, returning it as a {@code Single} + */ + public static ResponseExtractor> body(Class sourceClass) { + + ResolvableType resolvableType = ResolvableType.forClass(sourceClass); + //noinspection unchecked + return (clientResponse, messageConverters) -> (Single) RxJava1SingleConverter + .fromPublisher(clientResponse + .flatMap(resp -> decodeResponseBody(resp, resolvableType, messageConverters)).next()); + } + + /** + * Extract the response body and decode it, returning it as an {@code Observable} + */ + public static ResponseExtractor> bodyStream(Class sourceClass) { + + ResolvableType resolvableType = ResolvableType.forClass(sourceClass); + return (clientResponse, messageConverters) -> RxJava1ObservableConverter + .fromPublisher(clientResponse + .flatMap(resp -> decodeResponseBody(resp, resolvableType, messageConverters))); + } + + /** + * Extract the full response body as a {@code ResponseEntity} + * with its body decoded as a single type {@code T} + */ + public static ResponseExtractor>> response(Class sourceClass) { + + ResolvableType resolvableType = ResolvableType.forClass(sourceClass); + return (clientResponse, messageConverters) -> (Single>) + RxJava1SingleConverter.fromPublisher(clientResponse + .then(response -> + Mono.when( + decodeResponseBody(response, resolvableType, messageConverters).next(), + Mono.just(response.getHeaders()), + Mono.just(response.getStatusCode()))) + .map(tuple -> { + //noinspection unchecked + return new ResponseEntity<>((T) tuple.getT1(), tuple.getT2(), tuple.getT3()); + })); + } + + /** + * Extract the full response body as a {@code ResponseEntity} + * with its body decoded as an {@code Observable} + */ + public static ResponseExtractor>>> responseStream(Class sourceClass) { + ResolvableType resolvableType = ResolvableType.forClass(sourceClass); + return (clientResponse, messageConverters) -> RxJava1SingleConverter.fromPublisher(clientResponse + .map(response -> new ResponseEntity<>( + RxJava1ObservableConverter + .fromPublisher(decodeResponseBody(response, resolvableType, messageConverters)), + response.getHeaders(), + response.getStatusCode()))); + } + + /** + * Extract the response headers as an {@code HttpHeaders} instance + */ + public static ResponseExtractor> headers() { + return (clientResponse, messageConverters) -> RxJava1SingleConverter + .fromPublisher(clientResponse.map(resp -> resp.getHeaders())); + } + + protected static Flux decodeResponseBody(ClientHttpResponse response, ResolvableType responseType, + List> messageConverters) { + + MediaType contentType = response.getHeaders().getContentType(); + Optional> converter = resolveConverter(messageConverters, responseType, contentType); + if (!converter.isPresent()) { + return Flux.error(new IllegalStateException("Could not decode response body of type '" + contentType + + "' with target type '" + responseType.toString() + "'")); + } + //noinspection unchecked + return (Flux) converter.get().read(responseType, response); + } + + + protected static Optional> resolveConverter(List> messageConverters, + ResolvableType type, MediaType mediaType) { + return messageConverters.stream().filter(e -> e.canRead(type, mediaType)).findFirst(); + } +} \ No newline at end of file diff --git a/spring-web-reactive/src/test/java/org/springframework/web/client/reactive/RxJava1WebClientIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/client/reactive/RxJava1WebClientIntegrationTests.java new file mode 100644 index 0000000000..97ebe3eed0 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/client/reactive/RxJava1WebClientIntegrationTests.java @@ -0,0 +1,340 @@ +/* + * 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.web.client.reactive; + +import static org.junit.Assert.*; +import static org.springframework.web.client.reactive.support.RxJava1ClientWebRequestBuilders.*; +import static org.springframework.web.client.reactive.support.RxJava1ResponseExtractors.*; + +import java.util.concurrent.TimeUnit; + +import okhttp3.HttpUrl; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.hamcrest.Matchers; +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import rx.Observable; +import rx.Single; +import rx.observers.TestSubscriber; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.http.codec.Pojo; + +/** + * {@link WebClient} integration tests with the {@code Obserable} and {@code Single} API. + * + * @author Brian Clozel + */ +public class RxJava1WebClientIntegrationTests { + + private MockWebServer server; + + private WebClient webClient; + + @Before + public void setup() { + this.server = new MockWebServer(); + this.webClient = new WebClient(new ReactorClientHttpConnector()); + } + + @Test + public void shouldGetHeaders() throws Exception { + + HttpUrl baseUrl = server.url("/greeting?name=Spring"); + this.server.enqueue(new MockResponse().setHeader("Content-Type", "text/plain").setBody("Hello Spring!")); + + Single result = this.webClient + .perform(get(baseUrl.toString())) + .extract(headers()); + + TestSubscriber ts = new TestSubscriber(); + result.subscribe(ts); + ts.awaitTerminalEvent(2, TimeUnit.SECONDS); + + HttpHeaders httpHeaders = ts.getOnNextEvents().get(0); + assertEquals(MediaType.TEXT_PLAIN, httpHeaders.getContentType()); + assertEquals(13L, httpHeaders.getContentLength()); + ts.assertValueCount(1); + ts.assertCompleted(); + + RecordedRequest request = server.takeRequest(); + assertEquals(1, server.getRequestCount()); + assertEquals("*/*", request.getHeader(HttpHeaders.ACCEPT)); + assertEquals("/greeting?name=Spring", request.getPath()); + } + + @Test + public void shouldGetPlainTextResponseAsObject() throws Exception { + + HttpUrl baseUrl = server.url("/greeting?name=Spring"); + this.server.enqueue(new MockResponse().setBody("Hello Spring!")); + + Single result = this.webClient + .perform(get(baseUrl.toString()) + .header("X-Test-Header", "testvalue")) + .extract(body(String.class)); + + TestSubscriber ts = new TestSubscriber(); + result.subscribe(ts); + ts.awaitTerminalEvent(2, TimeUnit.SECONDS); + + String response = ts.getOnNextEvents().get(0); + assertEquals("Hello Spring!", response); + ts.assertValueCount(1); + ts.assertCompleted(); + + RecordedRequest request = server.takeRequest(); + assertEquals(1, server.getRequestCount()); + assertEquals("testvalue", request.getHeader("X-Test-Header")); + assertEquals("*/*", request.getHeader(HttpHeaders.ACCEPT)); + assertEquals("/greeting?name=Spring", request.getPath()); + } + + @Test + public void shouldGetPlainTextResponse() throws Exception { + + HttpUrl baseUrl = server.url("/greeting?name=Spring"); + this.server.enqueue(new MockResponse().setHeader("Content-Type", "text/plain").setBody("Hello Spring!")); + + Single> result = this.webClient + .perform(get(baseUrl.toString()) + .accept(MediaType.TEXT_PLAIN)) + .extract(response(String.class)); + + TestSubscriber> ts = new TestSubscriber>(); + result.subscribe(ts); + ts.awaitTerminalEvent(2, TimeUnit.SECONDS); + + ResponseEntity response = ts.getOnNextEvents().get(0); + assertEquals(200, response.getStatusCode().value()); + assertEquals(MediaType.TEXT_PLAIN, response.getHeaders().getContentType()); + assertEquals("Hello Spring!", response.getBody()); + ts.assertValueCount(1); + ts.assertCompleted(); + + RecordedRequest request = server.takeRequest(); + assertEquals(1, server.getRequestCount()); + assertEquals("/greeting?name=Spring", request.getPath()); + assertEquals("text/plain", request.getHeader(HttpHeaders.ACCEPT)); + } + + @Test + public void shouldGetJsonAsMonoOfString() throws Exception { + + HttpUrl baseUrl = server.url("/json"); + String content = "{\"bar\":\"barbar\",\"foo\":\"foofoo\"}"; + this.server.enqueue(new MockResponse().setHeader("Content-Type", "application/json") + .setBody(content)); + + Single result = this.webClient + .perform(get(baseUrl.toString()) + .accept(MediaType.APPLICATION_JSON)) + .extract(body(String.class)); + + TestSubscriber ts = new TestSubscriber(); + result.subscribe(ts); + ts.awaitTerminalEvent(2, TimeUnit.SECONDS); + + String response = ts.getOnNextEvents().get(0); + assertEquals(content, response); + ts.assertValueCount(1); + ts.assertCompleted(); + + RecordedRequest request = server.takeRequest(); + assertEquals(1, server.getRequestCount()); + assertEquals("/json", request.getPath()); + assertEquals("application/json", request.getHeader(HttpHeaders.ACCEPT)); + } + + @Test + public void shouldGetJsonAsMonoOfPojo() throws Exception { + + HttpUrl baseUrl = server.url("/pojo"); + this.server.enqueue(new MockResponse().setHeader("Content-Type", "application/json") + .setBody("{\"bar\":\"barbar\",\"foo\":\"foofoo\"}")); + + Single result = this.webClient + .perform(get(baseUrl.toString()) + .accept(MediaType.APPLICATION_JSON)) + .extract(body(Pojo.class)); + + TestSubscriber ts = new TestSubscriber(); + result.subscribe(ts); + ts.awaitTerminalEvent(2, TimeUnit.SECONDS); + + Pojo response = ts.getOnNextEvents().get(0); + assertEquals("barbar", response.getBar()); + ts.assertValueCount(1); + ts.assertCompleted(); + + RecordedRequest request = server.takeRequest(); + assertEquals(1, server.getRequestCount()); + assertEquals("/pojo", request.getPath()); + assertEquals("application/json", request.getHeader(HttpHeaders.ACCEPT)); + } + + @Test + public void shouldGetJsonAsFluxOfPojos() throws Exception { + + HttpUrl baseUrl = server.url("/pojos"); + this.server.enqueue(new MockResponse().setHeader("Content-Type", "application/json") + .setBody("[{\"bar\":\"bar1\",\"foo\":\"foo1\"},{\"bar\":\"bar2\",\"foo\":\"foo2\"}]")); + + Observable result = this.webClient + .perform(get(baseUrl.toString()) + .accept(MediaType.APPLICATION_JSON)) + .extract(bodyStream(Pojo.class)); + + TestSubscriber ts = new TestSubscriber(); + result.subscribe(ts); + ts.awaitTerminalEvent(2, TimeUnit.SECONDS); + + assertThat(ts.getOnNextEvents().get(0).getBar(), Matchers.is("bar1")); + assertThat(ts.getOnNextEvents().get(1).getBar(), Matchers.is("bar2")); + ts.assertValueCount(2); + ts.assertCompleted(); + + RecordedRequest request = server.takeRequest(); + assertEquals(1, server.getRequestCount()); + assertEquals("/pojos", request.getPath()); + assertEquals("application/json", request.getHeader(HttpHeaders.ACCEPT)); + } + + @Test + public void shouldGetJsonAsResponseOfPojosStream() throws Exception { + + HttpUrl baseUrl = server.url("/pojos"); + this.server.enqueue(new MockResponse().setHeader("Content-Type", "application/json") + .setBody("[{\"bar\":\"bar1\",\"foo\":\"foo1\"},{\"bar\":\"bar2\",\"foo\":\"foo2\"}]")); + + Single>> result = this.webClient + .perform(get(baseUrl.toString()) + .accept(MediaType.APPLICATION_JSON)) + .extract(responseStream(Pojo.class)); + + TestSubscriber>> ts = new TestSubscriber>>(); + result.subscribe(ts); + ts.awaitTerminalEvent(2, TimeUnit.SECONDS); + + ResponseEntity> response = ts.getOnNextEvents().get(0); + assertEquals(200, response.getStatusCode().value()); + assertEquals(MediaType.APPLICATION_JSON, response.getHeaders().getContentType()); + ts.assertValueCount(1); + ts.assertCompleted(); + + RecordedRequest request = server.takeRequest(); + assertEquals(1, server.getRequestCount()); + assertEquals("/pojos", request.getPath()); + assertEquals("application/json", request.getHeader(HttpHeaders.ACCEPT)); + } + + @Test + public void shouldPostPojoAsJson() throws Exception { + + HttpUrl baseUrl = server.url("/pojo/capitalize"); + this.server.enqueue(new MockResponse() + .setHeader("Content-Type", "application/json") + .setBody("{\"bar\":\"BARBAR\",\"foo\":\"FOOFOO\"}")); + + Pojo spring = new Pojo("foofoo", "barbar"); + Single result = this.webClient + .perform(post(baseUrl.toString()) + .body(spring) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .extract(body(Pojo.class)); + + TestSubscriber ts = new TestSubscriber(); + result.subscribe(ts); + ts.awaitTerminalEvent(2, TimeUnit.SECONDS); + + assertThat(ts.getOnNextEvents().get(0).getBar(), Matchers.is("BARBAR")); + ts.assertValueCount(1); + ts.assertCompleted(); + + RecordedRequest request = server.takeRequest(); + assertEquals(1, server.getRequestCount()); + assertEquals("/pojo/capitalize", request.getPath()); + assertEquals("{\"foo\":\"foofoo\",\"bar\":\"barbar\"}", request.getBody().readUtf8()); + assertEquals("chunked", request.getHeader(HttpHeaders.TRANSFER_ENCODING)); + assertEquals("application/json", request.getHeader(HttpHeaders.ACCEPT)); + assertEquals("application/json", request.getHeader(HttpHeaders.CONTENT_TYPE)); + } + + @Test + public void shouldSendCookieHeader() throws Exception { + HttpUrl baseUrl = server.url("/test"); + this.server.enqueue(new MockResponse() + .setHeader("Content-Type", "text/plain").setBody("test")); + + Single result = this.webClient + .perform(get(baseUrl.toString()) + .cookie("testkey", "testvalue")) + .extract(body(String.class)); + + TestSubscriber ts = new TestSubscriber(); + result.subscribe(ts); + ts.awaitTerminalEvent(2, TimeUnit.SECONDS); + + String response = ts.getOnNextEvents().get(0); + assertEquals("test", response); + ts.assertValueCount(1); + ts.assertCompleted(); + + RecordedRequest request = server.takeRequest(); + assertEquals(1, server.getRequestCount()); + assertEquals("/test", request.getPath()); + assertEquals("testkey=testvalue", request.getHeader(HttpHeaders.COOKIE)); + } + + @Test + @Ignore + public void shouldGetErrorWhen404() throws Exception { + + HttpUrl baseUrl = server.url("/greeting?name=Spring"); + this.server.enqueue(new MockResponse().setResponseCode(404)); + + Single result = this.webClient + .perform(get(baseUrl.toString())) + .extract(body(String.class)); + + // TODO: error message should be converted to a ClientException + TestSubscriber ts = new TestSubscriber(); + result.subscribe(ts); + ts.awaitTerminalEvent(2, TimeUnit.SECONDS); + + ts.assertError(WebClientException.class); + + RecordedRequest request = server.takeRequest(); + assertEquals(1, server.getRequestCount()); + assertEquals("*/*", request.getHeader(HttpHeaders.ACCEPT)); + assertEquals("/greeting?name=Spring", request.getPath()); + } + + @After + public void tearDown() throws Exception { + this.server.shutdown(); + } + +} From 407715b89eaaf1780f10edef873e33101c5e5bbe Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 8 Jul 2016 12:17:36 -0400 Subject: [PATCH 484/498] Helpers for get/post in RequestMappingIntegrationTests This commit replaces the current helper methods in RequestMappingIntegrationTests with generic helper methods to perform HTTP GET and POST requests. This results in more transparent code that shows the exact HTTP inputs and outputs and is also more flexible to change for variations in testing. --- .../RequestMappingIntegrationTests.java | 284 +++++++++--------- 1 file changed, 145 insertions(+), 139 deletions(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java index b29495fd99..d3a3e839d2 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java @@ -37,7 +37,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ResolvableType; -import org.springframework.http.codec.json.JacksonJsonEncoder; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.core.io.buffer.DataBuffer; @@ -47,6 +46,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; +import org.springframework.http.codec.json.JacksonJsonEncoder; import org.springframework.http.server.reactive.AbstractHttpHandlerIntegrationTests; import org.springframework.http.server.reactive.HttpHandler; import org.springframework.http.server.reactive.ZeroCopyIntegrationTests; @@ -65,7 +65,12 @@ import org.springframework.web.reactive.config.WebReactiveConfiguration; import org.springframework.web.reactive.result.view.freemarker.FreeMarkerConfigurer; import org.springframework.web.server.adapter.WebHttpHandlerBuilder; -import static org.junit.Assert.*; +import static java.util.Arrays.asList; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.springframework.http.MediaType.APPLICATION_XML; +import static org.springframework.http.RequestEntity.get; /** @@ -76,6 +81,12 @@ import static org.junit.Assert.*; */ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrationTests { + private static final ParameterizedTypeReference> PERSON_LIST = + new ParameterizedTypeReference>() {}; + + private static final MediaType JSON = MediaType.APPLICATION_JSON; + + private AnnotationConfigApplicationContext wac; private RestTemplate restTemplate = new RestTemplate(); @@ -87,286 +98,274 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati this.wac.register(FrameworkConfig.class, ApplicationConfig.class); this.wac.refresh(); - DispatcherHandler webHandler = new DispatcherHandler(); - webHandler.setApplicationContext(this.wac); + DispatcherHandler handler = new DispatcherHandler(); + handler.setApplicationContext(this.wac); - return WebHttpHandlerBuilder.webHandler(webHandler).build(); + return WebHttpHandlerBuilder.webHandler(handler).build(); } @Test - public void helloWithQueryParam() throws Exception { - URI url = new URI("http://localhost:" + port + "/param?name=George"); - RequestEntity request = RequestEntity.get(url).build(); - ResponseEntity response = restTemplate.exchange(request, String.class); - - assertEquals("Hello George!", response.getBody()); + public void handleWithParam() throws Exception { + String expected = "Hello George!"; + assertEquals(expected, performGet("/param?name=George", null, String.class).getBody()); } @Test public void rawPojoResponse() throws Exception { - URI url = new URI("http://localhost:" + port + "/raw"); - RequestEntity request = - RequestEntity.get(url).accept(MediaType.APPLICATION_JSON).build(); - Person person = restTemplate.exchange(request, Person.class).getBody(); - - assertEquals(new Person("Robert"), person); + Person expected = new Person("Robert"); + assertEquals(expected, performGet("/raw", JSON, Person.class).getBody()); } @Test public void rawFluxResponse() throws Exception { - URI url = new URI("http://localhost:" + port + "/raw-flux"); - RequestEntity request = RequestEntity.get(url).build(); - ResponseEntity response = restTemplate.exchange(request, String.class); - - assertEquals("Hello!", response.getBody()); + String expected = "Hello!"; + assertEquals(expected, performGet("/raw-flux", null, String.class).getBody()); } @Test public void rawObservableResponse() throws Exception { - URI url = new URI("http://localhost:" + port + "/raw-observable"); - RequestEntity request = RequestEntity.get(url).build(); - ResponseEntity response = restTemplate.exchange(request, String.class); - - assertEquals("Hello!", response.getBody()); + String expected = "Hello!"; + assertEquals(expected, performGet("/raw-observable", null, String.class).getBody()); } @Test public void handleWithThrownException() throws Exception { - URI url = new URI("http://localhost:" + port + "/thrown-exception"); - RequestEntity request = RequestEntity.get(url).build(); - ResponseEntity response = restTemplate.exchange(request, String.class); - - assertEquals("Recovered from error: Boo", response.getBody()); + String expected = "Recovered from error: Boo"; + assertEquals(expected, performGet("/thrown-exception", null, String.class).getBody()); } @Test public void handleWithErrorSignal() throws Exception { - URI url = new URI("http://localhost:" + port + "/error-signal"); - RequestEntity request = RequestEntity.get(url).build(); - ResponseEntity response = restTemplate.exchange(request, String.class); - - assertEquals("Recovered from error: Boo", response.getBody()); + String expected = "Recovered from error: Boo"; + assertEquals(expected, performGet("/error-signal", null, String.class).getBody()); } @Test public void streamResult() throws Exception { - URI url = new URI("http://localhost:" + port + "/stream-result"); - RequestEntity request = RequestEntity.get(url).build(); - ResponseEntity response = restTemplate.exchange(request, String[].class); - - assertArrayEquals(new String[]{"0", "1", "2", "3", "4"}, response.getBody()); + String[] expected = {"0", "1", "2", "3", "4"}; + assertArrayEquals(expected, performGet("/stream-result", null, String[].class).getBody()); } @Test public void serializeAsPojo() throws Exception { - serializeAsPojo("http://localhost:" + port + "/person"); + Person expected = new Person("Robert"); + assertEquals(expected, performGet("/person", JSON, Person.class).getBody()); } @Test public void serializeAsCompletableFuture() throws Exception { - serializeAsPojo("http://localhost:" + port + "/completable-future"); + Person expected = new Person("Robert"); + assertEquals(expected, performGet("/completable-future", JSON, Person.class).getBody()); } @Test public void serializeAsMonoResponseEntity() throws Exception { - serializeAsPojo("http://localhost:" + port + "/monoResponseEntity"); + Person expected = new Person("Robert"); + assertEquals(expected, performGet("/monoResponseEntity", JSON, Person.class).getBody()); } @Test public void serializeAsMono() throws Exception { - serializeAsPojo("http://localhost:" + port + "/mono"); + Person expected = new Person("Robert"); + assertEquals(expected, performGet("/mono", JSON, Person.class).getBody()); } @Test public void serializeAsSingle() throws Exception { - serializeAsPojo("http://localhost:" + port + "/single"); + Person expected = new Person("Robert"); + assertEquals(expected, performGet("/single", JSON, Person.class).getBody()); } @Test public void serializeAsList() throws Exception { - serializeAsCollection("http://localhost:" + port + "/list"); + List expected = asList(new Person("Robert"), new Person("Marie")); + assertEquals(expected, performGet("/list", JSON, PERSON_LIST).getBody()); } @Test public void serializeAsPublisher() throws Exception { - serializeAsCollection("http://localhost:" + port + "/publisher"); + List expected = asList(new Person("Robert"), new Person("Marie")); + assertEquals(expected, performGet("/publisher", JSON, PERSON_LIST).getBody()); } @Test public void serializeAsFlux() throws Exception { - serializeAsCollection("http://localhost:" + port + "/flux"); + List expected = asList(new Person("Robert"), new Person("Marie")); + assertEquals(expected, performGet("/flux", JSON, PERSON_LIST).getBody()); } @Test public void serializeAsObservable() throws Exception { - serializeAsCollection("http://localhost:" + port + "/observable"); + List expected = asList(new Person("Robert"), new Person("Marie")); + assertEquals(expected, performGet("/observable", JSON, PERSON_LIST).getBody()); } @Test public void serializeAsReactorStream() throws Exception { - serializeAsCollection("http://localhost:" + port + "/stream"); + List expected = asList(new Person("Robert"), new Person("Marie")); + assertEquals(expected, performGet("/stream", JSON, PERSON_LIST).getBody()); } @Test public void publisherCapitalize() throws Exception { - capitalizeCollection("http://localhost:" + port + "/publisher-capitalize"); + List req = asList(new Person("Robert"), new Person("Marie")); + List res = asList(new Person("ROBERT"), new Person("MARIE")); + assertEquals(res, performPost("/publisher-capitalize", JSON, req, JSON, PERSON_LIST).getBody()); } @Test public void fluxCapitalize() throws Exception { - capitalizeCollection("http://localhost:" + port + "/flux-capitalize"); + List req = asList(new Person("Robert"), new Person("Marie")); + List res = asList(new Person("ROBERT"), new Person("MARIE")); + assertEquals(res, performPost("/flux-capitalize", JSON, req, JSON, PERSON_LIST).getBody()); } @Test public void observableCapitalize() throws Exception { - capitalizeCollection("http://localhost:" + port + "/observable-capitalize"); + List req = asList(new Person("Robert"), new Person("Marie")); + List res = asList(new Person("ROBERT"), new Person("MARIE")); + assertEquals(res, performPost("/observable-capitalize", JSON, req, JSON, PERSON_LIST).getBody()); } @Test public void personCapitalize() throws Exception { - capitalizePojo("http://localhost:" + port + "/person-capitalize"); + assertEquals(new Person("ROBERT"), + performPost("/person-capitalize", JSON, new Person("Robert"), + JSON, Person.class).getBody()); } @Test public void completableFutureCapitalize() throws Exception { - capitalizePojo("http://localhost:" + port + "/completable-future-capitalize"); + assertEquals(new Person("ROBERT"), + performPost("/completable-future-capitalize", JSON, new Person("Robert"), + JSON, Person.class).getBody()); } @Test public void monoCapitalize() throws Exception { - capitalizePojo("http://localhost:" + port + "/mono-capitalize"); + assertEquals(new Person("ROBERT"), + performPost("/mono-capitalize", JSON, new Person("Robert"), + JSON, Person.class).getBody()); } @Test public void singleCapitalize() throws Exception { - capitalizePojo("http://localhost:" + port + "/single-capitalize"); + assertEquals(new Person("ROBERT"), + performPost("/single-capitalize", JSON, new Person("Robert"), + JSON, Person.class).getBody()); } @Test public void publisherCreate() throws Exception { - createJson("http://localhost:" + this.port + "/publisher-create"); + ResponseEntity entity = performPost("/publisher-create", JSON, + asList(new Person("Robert"), new Person("Marie")), null, Void.class); + + assertEquals(HttpStatus.OK, entity.getStatusCode()); + assertEquals(2, this.wac.getBean(TestRestController.class).persons.size()); } @Test public void publisherCreateXml() throws Exception { - createXml("http://localhost:" + this.port + "/publisher-create"); + People people = new People(new Person("Robert"), new Person("Marie")); + ResponseEntity response = performPost("/publisher-create", APPLICATION_XML, people, null, Void.class); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals(2, this.wac.getBean(TestRestController.class).persons.size()); } @Test public void fluxCreate() throws Exception { - createJson("http://localhost:" + this.port + "/flux-create"); + ResponseEntity entity = performPost("/flux-create", JSON, + asList(new Person("Robert"), new Person("Marie")), null, Void.class); + + assertEquals(HttpStatus.OK, entity.getStatusCode()); + assertEquals(2, this.wac.getBean(TestRestController.class).persons.size()); } @Test public void fluxCreateXml() throws Exception { - createXml("http://localhost:" + this.port + "/flux-create"); + People people = new People(new Person("Robert"), new Person("Marie")); + ResponseEntity response = performPost("/flux-create", APPLICATION_XML, people, null, Void.class); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals(2, this.wac.getBean(TestRestController.class).persons.size()); } @Test public void observableCreate() throws Exception { - createJson("http://localhost:" + this.port + "/observable-create"); + ResponseEntity entity = performPost("/observable-create", JSON, + asList(new Person("Robert"), new Person("Marie")), null, Void.class); + + assertEquals(HttpStatus.OK, entity.getStatusCode()); + assertEquals(2, this.wac.getBean(TestRestController.class).persons.size()); } @Test public void observableCreateXml() throws Exception { - createXml("http://localhost:" + this.port + "/observable-create"); + People people = new People(new Person("Robert"), new Person("Marie")); + ResponseEntity response = performPost("/observable-create", APPLICATION_XML, people, null, Void.class); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals(2, this.wac.getBean(TestRestController.class).persons.size()); } @Test public void html() throws Exception { - URI url = new URI("http://localhost:" + port + "/html?name=Jason"); - RequestEntity request = RequestEntity.get(url).accept(MediaType.TEXT_HTML).build(); - ResponseEntity response = restTemplate.exchange(request, String.class); - - assertEquals("Hello: Jason!", response.getBody()); + String expected = "Hello: Jason!"; + assertEquals(expected, performGet("/html?name=Jason", MediaType.TEXT_HTML, String.class).getBody()); } @Test public void resource() throws Exception { - URI url = new URI("http://localhost:" + port + "/resource"); - RequestEntity request = RequestEntity.get(url).build(); - ResponseEntity response = restTemplate.exchange(request, byte[].class); + ResponseEntity response = performGet("/resource", null, byte[].class); + assertEquals(HttpStatus.OK, response.getStatusCode()); assertTrue(response.hasBody()); assertEquals(951, response.getHeaders().getContentLength()); assertEquals(951, response.getBody().length); - assertEquals(new MediaType("image", "x-png"), - response.getHeaders().getContentType()); - } - - private void serializeAsPojo(String requestUrl) throws Exception { - RequestEntity request = RequestEntity.get(new URI(requestUrl)) - .accept(MediaType.APPLICATION_JSON) - .build(); - ResponseEntity response = restTemplate.exchange(request, Person.class); - - assertEquals(new Person("Robert"), response.getBody()); - } - - private void serializeAsCollection(String requestUrl) throws Exception { - RequestEntity request = RequestEntity.get(new URI(requestUrl)) - .accept(MediaType.APPLICATION_JSON) - .build(); - List results = restTemplate.exchange(request, - new ParameterizedTypeReference>(){}).getBody(); - - assertEquals(2, results.size()); - assertEquals(new Person("Robert"), results.get(0)); - assertEquals(new Person("Marie"), results.get(1)); + assertEquals(new MediaType("image", "x-png"), response.getHeaders().getContentType()); } - private void capitalizePojo(String requestUrl) throws Exception { - RequestEntity request = RequestEntity.post(new URI(requestUrl)) - .contentType(MediaType.APPLICATION_JSON) - .accept(MediaType.APPLICATION_JSON) - .body(new Person("Robert")); - ResponseEntity response = restTemplate.exchange(request, Person.class); + private ResponseEntity performGet(String url, MediaType acceptHeader, + Class type) throws Exception { - assertEquals(new Person("ROBERT"), response.getBody()); + return this.restTemplate.exchange(prepareGet(url, acceptHeader), type); } - private void capitalizeCollection(String requestUrl) throws Exception { - RequestEntity> request = RequestEntity.post(new URI(requestUrl)) - .contentType(MediaType.APPLICATION_JSON) - .accept(MediaType.APPLICATION_JSON) - .body(Arrays.asList(new Person("Robert"), new Person("Marie"))); - List results = restTemplate.exchange(request, - new ParameterizedTypeReference>(){}).getBody(); + private ResponseEntity performGet(String url, MediaType acceptHeader, + ParameterizedTypeReference type) throws Exception { - assertEquals(2, results.size()); - assertEquals("ROBERT", results.get(0).getName()); - assertEquals("MARIE", results.get(1).getName()); + return this.restTemplate.exchange(prepareGet(url, acceptHeader), type); } - private void createJson(String requestUrl) throws Exception { - URI url = new URI(requestUrl); - RequestEntity> request = RequestEntity.post(url) - .contentType(MediaType.APPLICATION_JSON) - .body(Arrays.asList(new Person("Robert"), new Person("Marie"))); - ResponseEntity response = restTemplate.exchange(request, Void.class); + private ResponseEntity performPost(String url, MediaType in, Object body, + MediaType out, Class type) throws Exception { - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertEquals(2, this.wac.getBean(TestRestController.class).persons.size()); + return this.restTemplate.exchange(preparePost(url, in, body, out), type); } - private void createXml(String requestUrl) throws Exception { - URI url = new URI(requestUrl); - People people = new People(); - people.getPerson().add(new Person("Robert")); - people.getPerson().add(new Person("Marie")); - RequestEntity request = - RequestEntity.post(url).contentType(MediaType.APPLICATION_XML) - .body(people); - ResponseEntity response = restTemplate.exchange(request, Void.class); + private ResponseEntity performPost(String url, MediaType in, Object body, + MediaType out, ParameterizedTypeReference type) throws Exception { - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertEquals(2, this.wac.getBean(TestRestController.class).persons.size()); + return this.restTemplate.exchange(preparePost(url, in, body, out), type); + } + + private RequestEntity prepareGet(String url, MediaType accept) throws Exception { + URI uri = new URI("http://localhost:" + this.port + url); + return (accept != null ? get(uri).accept(accept).build() : get(uri).build()); + } + + private RequestEntity preparePost(String url, MediaType in, Object body, MediaType out) throws Exception { + URI uri = new URI("http://localhost:" + this.port + url); + return (out != null ? + RequestEntity.post(uri).contentType(in).accept(out).body(body) : + RequestEntity.post(uri).contentType(in).body(body)); } @Configuration - @SuppressWarnings("unused") + @SuppressWarnings({"unused", "WeakerAccess"}) static class FrameworkConfig extends WebReactiveConfiguration { @Override @@ -385,7 +384,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati } @Configuration - @SuppressWarnings("unused") + @SuppressWarnings({"unused", "WeakerAccess"}) static class ApplicationConfig { @Bean @@ -426,7 +425,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory(); JacksonJsonEncoder encoder = new JacksonJsonEncoder(); return encoder.encode(Mono.just(new Person("Robert")), dataBufferFactory, - ResolvableType.forClass(Person.class), MediaType.APPLICATION_JSON).map(DataBuffer::asByteBuffer); + ResolvableType.forClass(Person.class), JSON).map(DataBuffer::asByteBuffer); } @RequestMapping("/stream-result") @@ -462,7 +461,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @RequestMapping("/list") public List listResponseBody() { - return Arrays.asList(new Person("Robert"), new Person("Marie")); + return asList(new Person("Robert"), new Person("Marie")); } @RequestMapping("/publisher") @@ -580,7 +579,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati } - @XmlRootElement + @XmlRootElement @SuppressWarnings("WeakerAccess") private static class Person { private String name; @@ -626,16 +625,23 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati } } - @XmlRootElement + @XmlRootElement @SuppressWarnings({"WeakerAccess", "unused"}) private static class People { private List persons = new ArrayList<>(); + public People() { + } + + public People(Person... persons) { + this.persons.addAll(Arrays.asList(persons)); + } + @XmlElement public List getPerson() { return this.persons; } + } - } From aa1397b8ba73d993d213b87b5a7f17d0fcf8776e Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 11 Jul 2016 13:56:00 -0400 Subject: [PATCH 485/498] Polish method order in RequestMappingIntegrationTests --- .../RequestMappingIntegrationTests.java | 229 +++++++++--------- 1 file changed, 116 insertions(+), 113 deletions(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java index d3a3e839d2..2126fe1469 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java @@ -128,24 +128,6 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati assertEquals(expected, performGet("/raw-observable", null, String.class).getBody()); } - @Test - public void handleWithThrownException() throws Exception { - String expected = "Recovered from error: Boo"; - assertEquals(expected, performGet("/thrown-exception", null, String.class).getBody()); - } - - @Test - public void handleWithErrorSignal() throws Exception { - String expected = "Recovered from error: Boo"; - assertEquals(expected, performGet("/error-signal", null, String.class).getBody()); - } - - @Test - public void streamResult() throws Exception { - String[] expected = {"0", "1", "2", "3", "4"}; - assertArrayEquals(expected, performGet("/stream-result", null, String[].class).getBody()); - } - @Test public void serializeAsPojo() throws Exception { Person expected = new Person("Robert"); @@ -158,12 +140,6 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati assertEquals(expected, performGet("/completable-future", JSON, Person.class).getBody()); } - @Test - public void serializeAsMonoResponseEntity() throws Exception { - Person expected = new Person("Robert"); - assertEquals(expected, performGet("/monoResponseEntity", JSON, Person.class).getBody()); - } - @Test public void serializeAsMono() throws Exception { Person expected = new Person("Robert"); @@ -176,6 +152,12 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati assertEquals(expected, performGet("/single", JSON, Person.class).getBody()); } + @Test + public void serializeAsMonoResponseEntity() throws Exception { + Person expected = new Person("Robert"); + assertEquals(expected, performGet("/monoResponseEntity", JSON, Person.class).getBody()); + } + @Test public void serializeAsList() throws Exception { List expected = asList(new Person("Robert"), new Person("Marie")); @@ -201,9 +183,42 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati } @Test - public void serializeAsReactorStream() throws Exception { - List expected = asList(new Person("Robert"), new Person("Marie")); - assertEquals(expected, performGet("/stream", JSON, PERSON_LIST).getBody()); + public void resource() throws Exception { + ResponseEntity response = performGet("/resource", null, byte[].class); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertTrue(response.hasBody()); + assertEquals(951, response.getHeaders().getContentLength()); + assertEquals(951, response.getBody().length); + assertEquals(new MediaType("image", "x-png"), response.getHeaders().getContentType()); + } + + @Test + public void personCapitalize() throws Exception { + assertEquals(new Person("ROBERT"), + performPost("/person-capitalize", JSON, new Person("Robert"), + JSON, Person.class).getBody()); + } + + @Test + public void completableFutureCapitalize() throws Exception { + assertEquals(new Person("ROBERT"), + performPost("/completable-future-capitalize", JSON, new Person("Robert"), + JSON, Person.class).getBody()); + } + + @Test + public void monoCapitalize() throws Exception { + assertEquals(new Person("ROBERT"), + performPost("/mono-capitalize", JSON, new Person("Robert"), + JSON, Person.class).getBody()); + } + + @Test + public void singleCapitalize() throws Exception { + assertEquals(new Person("ROBERT"), + performPost("/single-capitalize", JSON, new Person("Robert"), + JSON, Person.class).getBody()); } @Test @@ -227,34 +242,6 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati assertEquals(res, performPost("/observable-capitalize", JSON, req, JSON, PERSON_LIST).getBody()); } - @Test - public void personCapitalize() throws Exception { - assertEquals(new Person("ROBERT"), - performPost("/person-capitalize", JSON, new Person("Robert"), - JSON, Person.class).getBody()); - } - - @Test - public void completableFutureCapitalize() throws Exception { - assertEquals(new Person("ROBERT"), - performPost("/completable-future-capitalize", JSON, new Person("Robert"), - JSON, Person.class).getBody()); - } - - @Test - public void monoCapitalize() throws Exception { - assertEquals(new Person("ROBERT"), - performPost("/mono-capitalize", JSON, new Person("Robert"), - JSON, Person.class).getBody()); - } - - @Test - public void singleCapitalize() throws Exception { - assertEquals(new Person("ROBERT"), - performPost("/single-capitalize", JSON, new Person("Robert"), - JSON, Person.class).getBody()); - } - @Test public void publisherCreate() throws Exception { ResponseEntity entity = performPost("/publisher-create", JSON, @@ -310,20 +297,27 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati } @Test - public void html() throws Exception { - String expected = "Hello: Jason!"; - assertEquals(expected, performGet("/html?name=Jason", MediaType.TEXT_HTML, String.class).getBody()); + public void streamResult() throws Exception { + String[] expected = {"0", "1", "2", "3", "4"}; + assertArrayEquals(expected, performGet("/stream-result", null, String[].class).getBody()); } @Test - public void resource() throws Exception { - ResponseEntity response = performGet("/resource", null, byte[].class); + public void handleWithThrownException() throws Exception { + String expected = "Recovered from error: Boo"; + assertEquals(expected, performGet("/thrown-exception", null, String.class).getBody()); + } - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertTrue(response.hasBody()); - assertEquals(951, response.getHeaders().getContentLength()); - assertEquals(951, response.getBody().length); - assertEquals(new MediaType("image", "x-png"), response.getHeaders().getContentType()); + @Test + public void handleWithErrorSignal() throws Exception { + String expected = "Recovered from error: Boo"; + assertEquals(expected, performGet("/error-signal", null, String.class).getBody()); + } + + @Test + public void html() throws Exception { + String expected = "Hello: Jason!"; + assertEquals(expected, performGet("/html?name=Jason", MediaType.TEXT_HTML, String.class).getBody()); } @@ -410,15 +404,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati return Flux.just("Hello ", name, "!"); } - @RequestMapping("/person") - public Person personResponseBody() { - return new Person("Robert"); - } - - @RequestMapping("/completable-future") - public CompletableFuture completableFutureResponseBody() { - return CompletableFuture.completedFuture(new Person("Robert")); - } + // Response body with "raw" data (DataBuffer) @RequestMapping("/raw") public Publisher rawResponseBody() { @@ -428,11 +414,6 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati ResolvableType.forClass(Person.class), JSON).map(DataBuffer::asByteBuffer); } - @RequestMapping("/stream-result") - public Publisher stringStreamResponseBody() { - return Flux.interval(100).take(5); - } - @RequestMapping("/raw-flux") public Flux rawFluxResponseBody() { return Flux.just(ByteBuffer.wrap("Hello!".getBytes())); @@ -443,10 +424,16 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati return Observable.just(ByteBuffer.wrap("Hello!".getBytes())); } - @RequestMapping("/monoResponseEntity") - public ResponseEntity> monoResponseEntity() { - Mono body = Mono.just(new Person("Robert")); - return ResponseEntity.ok(body); + // Response body with Person Object(s) to "serialize" + + @RequestMapping("/person") + public Person personResponseBody() { + return new Person("Robert"); + } + + @RequestMapping("/completable-future") + public CompletableFuture completableFutureResponseBody() { + return CompletableFuture.completedFuture(new Person("Robert")); } @RequestMapping("/mono") @@ -459,6 +446,12 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati return Single.just(new Person("Robert")); } + @RequestMapping("/monoResponseEntity") + public ResponseEntity> monoResponseEntity() { + Mono body = Mono.just(new Person("Robert")); + return ResponseEntity.ok(body); + } + @RequestMapping("/list") public List listResponseBody() { return asList(new Person("Robert"), new Person("Marie")); @@ -479,9 +472,35 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati return Observable.just(new Person("Robert"), new Person("Marie")); } - @RequestMapping("/stream") - public Flux reactorStreamResponseBody() { - return Flux.just(new Person("Robert"), new Person("Marie")); + // ResponseBody with Resource + + @RequestMapping("/resource") + @ResponseBody + public Resource resource() { + return new ClassPathResource("spring.png", ZeroCopyIntegrationTests.class); + } + + // RequestBody -> ResponseBody with Person "capitalize" name transformation + + @RequestMapping("/person-capitalize") + public Person personCapitalize(@RequestBody Person person) { + return new Person(person.getName().toUpperCase()); + } + + @RequestMapping("/completable-future-capitalize") + public CompletableFuture completableFutureCapitalize( + @RequestBody CompletableFuture personFuture) { + return personFuture.thenApply(person -> new Person(person.getName().toUpperCase())); + } + + @RequestMapping("/mono-capitalize") + public Mono monoCapitalize(@RequestBody Mono personFuture) { + return personFuture.map(person -> new Person(person.getName().toUpperCase())); + } + + @RequestMapping("/single-capitalize") + public Single singleCapitalize(@RequestBody Single personFuture) { + return personFuture.map(person -> new Person(person.getName().toUpperCase())); } @RequestMapping("/publisher-capitalize") @@ -501,32 +520,13 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati return persons.map(person -> new Person(person.getName().toUpperCase())); } + // Request body with Objects to "create" + @RequestMapping("/stream-create") public Publisher streamCreate(@RequestBody Flux personStream) { return personStream.collectList().doOnSuccess(persons::addAll).then(); } - @RequestMapping("/person-capitalize") - public Person personCapitalize(@RequestBody Person person) { - return new Person(person.getName().toUpperCase()); - } - - @RequestMapping("/completable-future-capitalize") - public CompletableFuture completableFutureCapitalize( - @RequestBody CompletableFuture personFuture) { - return personFuture.thenApply(person -> new Person(person.getName().toUpperCase())); - } - - @RequestMapping("/mono-capitalize") - public Mono monoCapitalize(@RequestBody Mono personFuture) { - return personFuture.map(person -> new Person(person.getName().toUpperCase())); - } - - @RequestMapping("/single-capitalize") - public Single singleCapitalize(@RequestBody Single personFuture) { - return personFuture.map(person -> new Person(person.getName().toUpperCase())); - } - @RequestMapping("/publisher-create") public Publisher publisherCreate(@RequestBody Publisher personStream) { return Flux.from(personStream).doOnNext(persons::add).then(); @@ -542,6 +542,15 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati return personStream.toList().doOnNext(persons::addAll).flatMap(document -> Observable.empty()); } + // Async stream + + @RequestMapping("/stream-result") + public Publisher stringStreamResponseBody() { + return Flux.interval(100).take(5); + } + + // Error handling + @RequestMapping("/thrown-exception") public Publisher handleAndThrowException() { throw new IllegalStateException("Boo"); @@ -557,12 +566,6 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati return Mono.just("Recovered from error: " + ex.getMessage()); } - @RequestMapping("/resource") - @ResponseBody - public Resource resource() { - return new ClassPathResource("spring.png", ZeroCopyIntegrationTests.class); - } - //TODO add mixed and T request mappings tests } From 6fde86903d1bffaeb2259505ac626106a1e5019c Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 11 Jul 2016 14:07:12 -0400 Subject: [PATCH 486/498] Declare HTTP method mappings TestRestController --- .../RequestMappingIntegrationTests.java | 73 +++++++++---------- 1 file changed, 36 insertions(+), 37 deletions(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java index 2126fe1469..069ece27da 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java @@ -53,10 +53,10 @@ import org.springframework.http.server.reactive.ZeroCopyIntegrationTests; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; import org.springframework.web.reactive.DispatcherHandler; @@ -399,14 +399,14 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati final List persons = new ArrayList<>(); - @RequestMapping("/param") + @GetMapping("/param") public Publisher handleWithParam(@RequestParam String name) { return Flux.just("Hello ", name, "!"); } - // Response body with "raw" data (DataBuffer) + // GET with "raw" data (DataBuffer) response body - @RequestMapping("/raw") + @GetMapping("/raw") public Publisher rawResponseBody() { DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory(); JacksonJsonEncoder encoder = new JacksonJsonEncoder(); @@ -414,149 +414,148 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati ResolvableType.forClass(Person.class), JSON).map(DataBuffer::asByteBuffer); } - @RequestMapping("/raw-flux") + @GetMapping("/raw-flux") public Flux rawFluxResponseBody() { return Flux.just(ByteBuffer.wrap("Hello!".getBytes())); } - @RequestMapping("/raw-observable") + @GetMapping("/raw-observable") public Observable rawObservableResponseBody() { return Observable.just(ByteBuffer.wrap("Hello!".getBytes())); } - // Response body with Person Object(s) to "serialize" + // GET with Person Object(s) response body to "serialize" - @RequestMapping("/person") + @GetMapping("/person") public Person personResponseBody() { return new Person("Robert"); } - @RequestMapping("/completable-future") + @GetMapping("/completable-future") public CompletableFuture completableFutureResponseBody() { return CompletableFuture.completedFuture(new Person("Robert")); } - @RequestMapping("/mono") + @GetMapping("/mono") public Mono monoResponseBody() { return Mono.just(new Person("Robert")); } - @RequestMapping("/single") + @GetMapping("/single") public Single singleResponseBody() { return Single.just(new Person("Robert")); } - @RequestMapping("/monoResponseEntity") + @GetMapping("/monoResponseEntity") public ResponseEntity> monoResponseEntity() { Mono body = Mono.just(new Person("Robert")); return ResponseEntity.ok(body); } - @RequestMapping("/list") + @GetMapping("/list") public List listResponseBody() { return asList(new Person("Robert"), new Person("Marie")); } - @RequestMapping("/publisher") + @GetMapping("/publisher") public Publisher publisherResponseBody() { return Flux.just(new Person("Robert"), new Person("Marie")); } - @RequestMapping("/flux") + @GetMapping("/flux") public Flux fluxResponseBody() { return Flux.just(new Person("Robert"), new Person("Marie")); } - @RequestMapping("/observable") + @GetMapping("/observable") public Observable observableResponseBody() { return Observable.just(new Person("Robert"), new Person("Marie")); } - // ResponseBody with Resource + // GET with Resource response body - @RequestMapping("/resource") - @ResponseBody + @GetMapping("/resource") public Resource resource() { return new ClassPathResource("spring.png", ZeroCopyIntegrationTests.class); } - // RequestBody -> ResponseBody with Person "capitalize" name transformation + // POST with Person "capitalize" name transformation - @RequestMapping("/person-capitalize") + @PostMapping("/person-capitalize") public Person personCapitalize(@RequestBody Person person) { return new Person(person.getName().toUpperCase()); } - @RequestMapping("/completable-future-capitalize") + @PostMapping("/completable-future-capitalize") public CompletableFuture completableFutureCapitalize( @RequestBody CompletableFuture personFuture) { return personFuture.thenApply(person -> new Person(person.getName().toUpperCase())); } - @RequestMapping("/mono-capitalize") + @PostMapping("/mono-capitalize") public Mono monoCapitalize(@RequestBody Mono personFuture) { return personFuture.map(person -> new Person(person.getName().toUpperCase())); } - @RequestMapping("/single-capitalize") + @PostMapping("/single-capitalize") public Single singleCapitalize(@RequestBody Single personFuture) { return personFuture.map(person -> new Person(person.getName().toUpperCase())); } - @RequestMapping("/publisher-capitalize") + @PostMapping("/publisher-capitalize") public Publisher publisherCapitalize(@RequestBody Publisher persons) { return Flux .from(persons) .map(person -> new Person(person.getName().toUpperCase())); } - @RequestMapping("/flux-capitalize") + @PostMapping("/flux-capitalize") public Flux fluxCapitalize(@RequestBody Flux persons) { return persons.map(person -> new Person(person.getName().toUpperCase())); } - @RequestMapping("/observable-capitalize") + @PostMapping("/observable-capitalize") public Observable observableCapitalize(@RequestBody Observable persons) { return persons.map(person -> new Person(person.getName().toUpperCase())); } - // Request body with Objects to "create" + // POST with Objects to "create" - @RequestMapping("/stream-create") + @PostMapping("/stream-create") public Publisher streamCreate(@RequestBody Flux personStream) { return personStream.collectList().doOnSuccess(persons::addAll).then(); } - @RequestMapping("/publisher-create") + @PostMapping("/publisher-create") public Publisher publisherCreate(@RequestBody Publisher personStream) { return Flux.from(personStream).doOnNext(persons::add).then(); } - @RequestMapping("/flux-create") + @PostMapping("/flux-create") public Mono fluxCreate(@RequestBody Flux personStream) { return personStream.doOnNext(persons::add).then(); } - @RequestMapping("/observable-create") + @PostMapping("/observable-create") public Observable observableCreate(@RequestBody Observable personStream) { return personStream.toList().doOnNext(persons::addAll).flatMap(document -> Observable.empty()); } // Async stream - @RequestMapping("/stream-result") + @GetMapping("/stream-result") public Publisher stringStreamResponseBody() { return Flux.interval(100).take(5); } // Error handling - @RequestMapping("/thrown-exception") + @GetMapping("/thrown-exception") public Publisher handleAndThrowException() { throw new IllegalStateException("Boo"); } - @RequestMapping("/error-signal") + @GetMapping("/error-signal") public Publisher handleWithError() { return Mono.error(new IllegalStateException("Boo")); } @@ -574,7 +573,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati @SuppressWarnings("unused") private static class TestController { - @RequestMapping("/html") + @GetMapping("/html") public String getHtmlPage(@RequestParam String name, Model model) { model.addAttribute("hello", "Hello: " + name + "!"); return "test"; From 35f791acf8a89ab017b25ed98596d4366cd76ca0 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 11 Jul 2016 14:41:15 -0400 Subject: [PATCH 487/498] Extract base class for RequestMappingIntegrationTests --- ...bstractRequestMappingIntegrationTests.java | 95 +++++++++++++++++++ .../RequestMappingIntegrationTests.java | 71 +++----------- 2 files changed, 108 insertions(+), 58 deletions(-) create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/AbstractRequestMappingIntegrationTests.java diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/AbstractRequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/AbstractRequestMappingIntegrationTests.java new file mode 100644 index 0000000000..2beb97217f --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/AbstractRequestMappingIntegrationTests.java @@ -0,0 +1,95 @@ +/* + * 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.web.reactive.result.method.annotation; + +import java.net.URI; + +import org.springframework.context.ApplicationContext; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.MediaType; +import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.http.server.reactive.AbstractHttpHandlerIntegrationTests; +import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.reactive.DispatcherHandler; +import org.springframework.web.server.adapter.WebHttpHandlerBuilder; + +import static org.springframework.http.RequestEntity.get; + +/** + * + * @author Rossen Stoyanchev + */ +public abstract class AbstractRequestMappingIntegrationTests extends AbstractHttpHandlerIntegrationTests { + + private ApplicationContext applicationContext; + + private RestTemplate restTemplate = new RestTemplate(); + + + @Override + protected HttpHandler createHttpHandler() { + this.applicationContext = initApplicationContext(); + DispatcherHandler handler = new DispatcherHandler(); + handler.setApplicationContext(this.applicationContext); + return WebHttpHandlerBuilder.webHandler(handler).build(); + } + + protected abstract ApplicationContext initApplicationContext(); + + + ApplicationContext getApplicationContext() { + return this.applicationContext; + } + + + ResponseEntity performGet(String url, MediaType out, + Class type) throws Exception { + + return this.restTemplate.exchange(prepareGet(url, out), type); + } + + ResponseEntity performGet(String url, MediaType out, + ParameterizedTypeReference type) throws Exception { + + return this.restTemplate.exchange(prepareGet(url, out), type); + } + + ResponseEntity performPost(String url, MediaType in, Object body, MediaType out, + Class type) throws Exception { + + return this.restTemplate.exchange(preparePost(url, in, body, out), type); + } + + ResponseEntity performPost(String url, MediaType in, Object body, + MediaType out, ParameterizedTypeReference type) throws Exception { + + return this.restTemplate.exchange(preparePost(url, in, body, out), type); + } + + private RequestEntity prepareGet(String url, MediaType accept) throws Exception { + URI uri = new URI("http://localhost:" + this.port + url); + return (accept != null ? get(uri).accept(accept).build() : get(uri).build()); + } + + private RequestEntity preparePost(String url, MediaType in, Object body, MediaType out) throws Exception { + URI uri = new URI("http://localhost:" + this.port + url); + return (out != null ? + RequestEntity.post(uri).contentType(in).accept(out).body(body) : + RequestEntity.post(uri).contentType(in).body(body)); + } +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java index 069ece27da..61aa66e608 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java @@ -32,6 +32,7 @@ import reactor.core.publisher.Mono; import rx.Observable; import rx.Single; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -79,7 +80,7 @@ import static org.springframework.http.RequestEntity.get; * @author Sebastien Deleuze * @author Stephane Maldini */ -public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrationTests { +public class RequestMappingIntegrationTests extends AbstractRequestMappingIntegrationTests { private static final ParameterizedTypeReference> PERSON_LIST = new ParameterizedTypeReference>() {}; @@ -87,21 +88,12 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati private static final MediaType JSON = MediaType.APPLICATION_JSON; - private AnnotationConfigApplicationContext wac; - - private RestTemplate restTemplate = new RestTemplate(); - - @Override - protected HttpHandler createHttpHandler() { - this.wac = new AnnotationConfigApplicationContext(); - this.wac.register(FrameworkConfig.class, ApplicationConfig.class); - this.wac.refresh(); - - DispatcherHandler handler = new DispatcherHandler(); - handler.setApplicationContext(this.wac); - - return WebHttpHandlerBuilder.webHandler(handler).build(); + protected ApplicationContext initApplicationContext() { + AnnotationConfigApplicationContext wac = new AnnotationConfigApplicationContext(); + wac.register(FrameworkConfig.class, ApplicationConfig.class); + wac.refresh(); + return wac; } @Test @@ -248,7 +240,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati asList(new Person("Robert"), new Person("Marie")), null, Void.class); assertEquals(HttpStatus.OK, entity.getStatusCode()); - assertEquals(2, this.wac.getBean(TestRestController.class).persons.size()); + assertEquals(2, getApplicationContext().getBean(TestRestController.class).persons.size()); } @Test @@ -257,7 +249,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati ResponseEntity response = performPost("/publisher-create", APPLICATION_XML, people, null, Void.class); assertEquals(HttpStatus.OK, response.getStatusCode()); - assertEquals(2, this.wac.getBean(TestRestController.class).persons.size()); + assertEquals(2, getApplicationContext().getBean(TestRestController.class).persons.size()); } @Test @@ -266,7 +258,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati asList(new Person("Robert"), new Person("Marie")), null, Void.class); assertEquals(HttpStatus.OK, entity.getStatusCode()); - assertEquals(2, this.wac.getBean(TestRestController.class).persons.size()); + assertEquals(2, getApplicationContext().getBean(TestRestController.class).persons.size()); } @Test @@ -275,7 +267,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati ResponseEntity response = performPost("/flux-create", APPLICATION_XML, people, null, Void.class); assertEquals(HttpStatus.OK, response.getStatusCode()); - assertEquals(2, this.wac.getBean(TestRestController.class).persons.size()); + assertEquals(2, getApplicationContext().getBean(TestRestController.class).persons.size()); } @Test @@ -284,7 +276,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati asList(new Person("Robert"), new Person("Marie")), null, Void.class); assertEquals(HttpStatus.OK, entity.getStatusCode()); - assertEquals(2, this.wac.getBean(TestRestController.class).persons.size()); + assertEquals(2, getApplicationContext().getBean(TestRestController.class).persons.size()); } @Test @@ -293,7 +285,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati ResponseEntity response = performPost("/observable-create", APPLICATION_XML, people, null, Void.class); assertEquals(HttpStatus.OK, response.getStatusCode()); - assertEquals(2, this.wac.getBean(TestRestController.class).persons.size()); + assertEquals(2, getApplicationContext().getBean(TestRestController.class).persons.size()); } @Test @@ -321,43 +313,6 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati } - private ResponseEntity performGet(String url, MediaType acceptHeader, - Class type) throws Exception { - - return this.restTemplate.exchange(prepareGet(url, acceptHeader), type); - } - - private ResponseEntity performGet(String url, MediaType acceptHeader, - ParameterizedTypeReference type) throws Exception { - - return this.restTemplate.exchange(prepareGet(url, acceptHeader), type); - } - - private ResponseEntity performPost(String url, MediaType in, Object body, - MediaType out, Class type) throws Exception { - - return this.restTemplate.exchange(preparePost(url, in, body, out), type); - } - - private ResponseEntity performPost(String url, MediaType in, Object body, - MediaType out, ParameterizedTypeReference type) throws Exception { - - return this.restTemplate.exchange(preparePost(url, in, body, out), type); - } - - private RequestEntity prepareGet(String url, MediaType accept) throws Exception { - URI uri = new URI("http://localhost:" + this.port + url); - return (accept != null ? get(uri).accept(accept).build() : get(uri).build()); - } - - private RequestEntity preparePost(String url, MediaType in, Object body, MediaType out) throws Exception { - URI uri = new URI("http://localhost:" + this.port + url); - return (out != null ? - RequestEntity.post(uri).contentType(in).accept(out).body(body) : - RequestEntity.post(uri).contentType(in).body(body)); - } - - @Configuration @SuppressWarnings({"unused", "WeakerAccess"}) static class FrameworkConfig extends WebReactiveConfiguration { From 7e07fb16d8762c7e1fbd205a8bb44d366938bfe7 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 11 Jul 2016 16:45:18 -0400 Subject: [PATCH 488/498] Split RequestMappingIntegrationTests into sub-classes --- ...pingExceptionHandlingIntegrationTests.java | 94 ++++ .../RequestMappingIntegrationTests.java | 532 +----------------- ...pingMessageConversionIntegrationTests.java | 489 ++++++++++++++++ ...MappingViewResolutionIntegrationTests.java | 93 +++ 4 files changed, 685 insertions(+), 523 deletions(-) create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingExceptionHandlingIntegrationTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingMessageConversionIntegrationTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingViewResolutionIntegrationTests.java diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingExceptionHandlingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingExceptionHandlingIntegrationTests.java new file mode 100644 index 0000000000..c11d2d6ded --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingExceptionHandlingIntegrationTests.java @@ -0,0 +1,94 @@ +/* + * 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.web.reactive.result.method.annotation; + +import org.junit.Test; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Mono; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.reactive.config.WebReactiveConfiguration; + +import static org.junit.Assert.assertEquals; + + +/** + * {@code @RequestMapping} integration tests with exception handling scenarios. + * + * @author Rossen Stoyanchev + */ +public class RequestMappingExceptionHandlingIntegrationTests extends AbstractRequestMappingIntegrationTests { + + + @Override + protected ApplicationContext initApplicationContext() { + AnnotationConfigApplicationContext wac = new AnnotationConfigApplicationContext(); + wac.register(WebConfig.class); + wac.refresh(); + return wac; + } + + + @Test + public void controllerThrowingException() throws Exception { + String expected = "Recovered from error: Boo"; + assertEquals(expected, performGet("/thrown-exception", null, String.class).getBody()); + } + + @Test + public void controllerReturnsMonoError() throws Exception { + String expected = "Recovered from error: Boo"; + assertEquals(expected, performGet("/mono-error", null, String.class).getBody()); + } + + + @Configuration + @ComponentScan(resourcePattern = "**/RequestMappingExceptionHandlingIntegrationTests$*.class") + @SuppressWarnings({"unused", "WeakerAccess"}) + static class WebConfig extends WebReactiveConfiguration { + + } + + + @RestController + @SuppressWarnings("unused") + private static class TestController { + + @GetMapping("/thrown-exception") + public Publisher handleAndThrowException() { + throw new IllegalStateException("Boo"); + } + + @GetMapping("/mono-error") + public Publisher handleWithError() { + return Mono.error(new IllegalStateException("Boo")); + } + + @ExceptionHandler + public Publisher handleException(IllegalStateException ex) { + return Mono.just("Recovered from error: " + ex.getMessage()); + } + + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java index 61aa66e608..3b5a332319 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java @@ -16,82 +16,38 @@ package org.springframework.web.reactive.result.method.annotation; -import java.net.URI; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlRootElement; - import org.junit.Test; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import rx.Observable; -import rx.Single; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.core.ResolvableType; -import org.springframework.core.io.ClassPathResource; -import org.springframework.core.io.Resource; -import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferFactory; -import org.springframework.core.io.buffer.DefaultDataBufferFactory; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.http.RequestEntity; -import org.springframework.http.ResponseEntity; -import org.springframework.http.codec.json.JacksonJsonEncoder; -import org.springframework.http.server.reactive.AbstractHttpHandlerIntegrationTests; -import org.springframework.http.server.reactive.HttpHandler; -import org.springframework.http.server.reactive.ZeroCopyIntegrationTests; -import org.springframework.stereotype.Controller; -import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.client.RestTemplate; -import org.springframework.web.reactive.DispatcherHandler; -import org.springframework.web.reactive.config.ViewResolverRegistry; import org.springframework.web.reactive.config.WebReactiveConfiguration; -import org.springframework.web.reactive.result.view.freemarker.FreeMarkerConfigurer; -import org.springframework.web.server.adapter.WebHttpHandlerBuilder; -import static java.util.Arrays.asList; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.springframework.http.MediaType.APPLICATION_XML; -import static org.springframework.http.RequestEntity.get; /** - * Integration tests with {@code @RequestMapping} methods. + * Integration tests with {@code @RequestMapping} handler methods. + * + *

    Before adding tests here consider if they are a better fit for any of the + * other {@code RequestMapping*IntegrationTests}. + * * @author Rossen Stoyanchev - * @author Sebastien Deleuze * @author Stephane Maldini */ public class RequestMappingIntegrationTests extends AbstractRequestMappingIntegrationTests { - private static final ParameterizedTypeReference> PERSON_LIST = - new ParameterizedTypeReference>() {}; - - private static final MediaType JSON = MediaType.APPLICATION_JSON; - - @Override protected ApplicationContext initApplicationContext() { AnnotationConfigApplicationContext wac = new AnnotationConfigApplicationContext(); - wac.register(FrameworkConfig.class, ApplicationConfig.class); + wac.register(WebConfig.class); wac.refresh(); return wac; } @@ -102,503 +58,33 @@ public class RequestMappingIntegrationTests extends AbstractRequestMappingIntegr assertEquals(expected, performGet("/param?name=George", null, String.class).getBody()); } - @Test - public void rawPojoResponse() throws Exception { - Person expected = new Person("Robert"); - assertEquals(expected, performGet("/raw", JSON, Person.class).getBody()); - } - - @Test - public void rawFluxResponse() throws Exception { - String expected = "Hello!"; - assertEquals(expected, performGet("/raw-flux", null, String.class).getBody()); - } - - @Test - public void rawObservableResponse() throws Exception { - String expected = "Hello!"; - assertEquals(expected, performGet("/raw-observable", null, String.class).getBody()); - } - - @Test - public void serializeAsPojo() throws Exception { - Person expected = new Person("Robert"); - assertEquals(expected, performGet("/person", JSON, Person.class).getBody()); - } - - @Test - public void serializeAsCompletableFuture() throws Exception { - Person expected = new Person("Robert"); - assertEquals(expected, performGet("/completable-future", JSON, Person.class).getBody()); - } - - @Test - public void serializeAsMono() throws Exception { - Person expected = new Person("Robert"); - assertEquals(expected, performGet("/mono", JSON, Person.class).getBody()); - } - - @Test - public void serializeAsSingle() throws Exception { - Person expected = new Person("Robert"); - assertEquals(expected, performGet("/single", JSON, Person.class).getBody()); - } - - @Test - public void serializeAsMonoResponseEntity() throws Exception { - Person expected = new Person("Robert"); - assertEquals(expected, performGet("/monoResponseEntity", JSON, Person.class).getBody()); - } - - @Test - public void serializeAsList() throws Exception { - List expected = asList(new Person("Robert"), new Person("Marie")); - assertEquals(expected, performGet("/list", JSON, PERSON_LIST).getBody()); - } - - @Test - public void serializeAsPublisher() throws Exception { - List expected = asList(new Person("Robert"), new Person("Marie")); - assertEquals(expected, performGet("/publisher", JSON, PERSON_LIST).getBody()); - } - - @Test - public void serializeAsFlux() throws Exception { - List expected = asList(new Person("Robert"), new Person("Marie")); - assertEquals(expected, performGet("/flux", JSON, PERSON_LIST).getBody()); - } - - @Test - public void serializeAsObservable() throws Exception { - List expected = asList(new Person("Robert"), new Person("Marie")); - assertEquals(expected, performGet("/observable", JSON, PERSON_LIST).getBody()); - } - - @Test - public void resource() throws Exception { - ResponseEntity response = performGet("/resource", null, byte[].class); - - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertTrue(response.hasBody()); - assertEquals(951, response.getHeaders().getContentLength()); - assertEquals(951, response.getBody().length); - assertEquals(new MediaType("image", "x-png"), response.getHeaders().getContentType()); - } - - @Test - public void personCapitalize() throws Exception { - assertEquals(new Person("ROBERT"), - performPost("/person-capitalize", JSON, new Person("Robert"), - JSON, Person.class).getBody()); - } - - @Test - public void completableFutureCapitalize() throws Exception { - assertEquals(new Person("ROBERT"), - performPost("/completable-future-capitalize", JSON, new Person("Robert"), - JSON, Person.class).getBody()); - } - - @Test - public void monoCapitalize() throws Exception { - assertEquals(new Person("ROBERT"), - performPost("/mono-capitalize", JSON, new Person("Robert"), - JSON, Person.class).getBody()); - } - - @Test - public void singleCapitalize() throws Exception { - assertEquals(new Person("ROBERT"), - performPost("/single-capitalize", JSON, new Person("Robert"), - JSON, Person.class).getBody()); - } - - @Test - public void publisherCapitalize() throws Exception { - List req = asList(new Person("Robert"), new Person("Marie")); - List res = asList(new Person("ROBERT"), new Person("MARIE")); - assertEquals(res, performPost("/publisher-capitalize", JSON, req, JSON, PERSON_LIST).getBody()); - } - - @Test - public void fluxCapitalize() throws Exception { - List req = asList(new Person("Robert"), new Person("Marie")); - List res = asList(new Person("ROBERT"), new Person("MARIE")); - assertEquals(res, performPost("/flux-capitalize", JSON, req, JSON, PERSON_LIST).getBody()); - } - - @Test - public void observableCapitalize() throws Exception { - List req = asList(new Person("Robert"), new Person("Marie")); - List res = asList(new Person("ROBERT"), new Person("MARIE")); - assertEquals(res, performPost("/observable-capitalize", JSON, req, JSON, PERSON_LIST).getBody()); - } - - @Test - public void publisherCreate() throws Exception { - ResponseEntity entity = performPost("/publisher-create", JSON, - asList(new Person("Robert"), new Person("Marie")), null, Void.class); - - assertEquals(HttpStatus.OK, entity.getStatusCode()); - assertEquals(2, getApplicationContext().getBean(TestRestController.class).persons.size()); - } - - @Test - public void publisherCreateXml() throws Exception { - People people = new People(new Person("Robert"), new Person("Marie")); - ResponseEntity response = performPost("/publisher-create", APPLICATION_XML, people, null, Void.class); - - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertEquals(2, getApplicationContext().getBean(TestRestController.class).persons.size()); - } - - @Test - public void fluxCreate() throws Exception { - ResponseEntity entity = performPost("/flux-create", JSON, - asList(new Person("Robert"), new Person("Marie")), null, Void.class); - - assertEquals(HttpStatus.OK, entity.getStatusCode()); - assertEquals(2, getApplicationContext().getBean(TestRestController.class).persons.size()); - } - - @Test - public void fluxCreateXml() throws Exception { - People people = new People(new Person("Robert"), new Person("Marie")); - ResponseEntity response = performPost("/flux-create", APPLICATION_XML, people, null, Void.class); - - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertEquals(2, getApplicationContext().getBean(TestRestController.class).persons.size()); - } - - @Test - public void observableCreate() throws Exception { - ResponseEntity entity = performPost("/observable-create", JSON, - asList(new Person("Robert"), new Person("Marie")), null, Void.class); - - assertEquals(HttpStatus.OK, entity.getStatusCode()); - assertEquals(2, getApplicationContext().getBean(TestRestController.class).persons.size()); - } - - @Test - public void observableCreateXml() throws Exception { - People people = new People(new Person("Robert"), new Person("Marie")); - ResponseEntity response = performPost("/observable-create", APPLICATION_XML, people, null, Void.class); - - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertEquals(2, getApplicationContext().getBean(TestRestController.class).persons.size()); - } - @Test public void streamResult() throws Exception { String[] expected = {"0", "1", "2", "3", "4"}; assertArrayEquals(expected, performGet("/stream-result", null, String[].class).getBody()); } - @Test - public void handleWithThrownException() throws Exception { - String expected = "Recovered from error: Boo"; - assertEquals(expected, performGet("/thrown-exception", null, String.class).getBody()); - } - - @Test - public void handleWithErrorSignal() throws Exception { - String expected = "Recovered from error: Boo"; - assertEquals(expected, performGet("/error-signal", null, String.class).getBody()); - } - - @Test - public void html() throws Exception { - String expected = "Hello: Jason!"; - assertEquals(expected, performGet("/html?name=Jason", MediaType.TEXT_HTML, String.class).getBody()); - } - @Configuration + @ComponentScan(resourcePattern = "**/RequestMappingIntegrationTests$*.class") @SuppressWarnings({"unused", "WeakerAccess"}) - static class FrameworkConfig extends WebReactiveConfiguration { - - @Override - protected void configureViewResolvers(ViewResolverRegistry registry) { - registry.freeMarker(); - } - - @Bean - public FreeMarkerConfigurer freeMarkerConfig() { - FreeMarkerConfigurer configurer = new FreeMarkerConfigurer(); - configurer.setPreferFileSystemAccess(false); - configurer.setTemplateLoaderPath("classpath*:org/springframework/web/reactive/view/freemarker/"); - return configurer; - } - + static class WebConfig extends WebReactiveConfiguration { } - @Configuration - @SuppressWarnings({"unused", "WeakerAccess"}) - static class ApplicationConfig { - - @Bean - public TestRestController testRestController() { - return new TestRestController(); - } - - @Bean - public TestController testController() { - return new TestController(); - } - } - - @RestController @SuppressWarnings("unused") private static class TestRestController { - final List persons = new ArrayList<>(); - @GetMapping("/param") public Publisher handleWithParam(@RequestParam String name) { return Flux.just("Hello ", name, "!"); } - // GET with "raw" data (DataBuffer) response body - - @GetMapping("/raw") - public Publisher rawResponseBody() { - DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory(); - JacksonJsonEncoder encoder = new JacksonJsonEncoder(); - return encoder.encode(Mono.just(new Person("Robert")), dataBufferFactory, - ResolvableType.forClass(Person.class), JSON).map(DataBuffer::asByteBuffer); - } - - @GetMapping("/raw-flux") - public Flux rawFluxResponseBody() { - return Flux.just(ByteBuffer.wrap("Hello!".getBytes())); - } - - @GetMapping("/raw-observable") - public Observable rawObservableResponseBody() { - return Observable.just(ByteBuffer.wrap("Hello!".getBytes())); - } - - // GET with Person Object(s) response body to "serialize" - - @GetMapping("/person") - public Person personResponseBody() { - return new Person("Robert"); - } - - @GetMapping("/completable-future") - public CompletableFuture completableFutureResponseBody() { - return CompletableFuture.completedFuture(new Person("Robert")); - } - - @GetMapping("/mono") - public Mono monoResponseBody() { - return Mono.just(new Person("Robert")); - } - - @GetMapping("/single") - public Single singleResponseBody() { - return Single.just(new Person("Robert")); - } - - @GetMapping("/monoResponseEntity") - public ResponseEntity> monoResponseEntity() { - Mono body = Mono.just(new Person("Robert")); - return ResponseEntity.ok(body); - } - - @GetMapping("/list") - public List listResponseBody() { - return asList(new Person("Robert"), new Person("Marie")); - } - - @GetMapping("/publisher") - public Publisher publisherResponseBody() { - return Flux.just(new Person("Robert"), new Person("Marie")); - } - - @GetMapping("/flux") - public Flux fluxResponseBody() { - return Flux.just(new Person("Robert"), new Person("Marie")); - } - - @GetMapping("/observable") - public Observable observableResponseBody() { - return Observable.just(new Person("Robert"), new Person("Marie")); - } - - // GET with Resource response body - - @GetMapping("/resource") - public Resource resource() { - return new ClassPathResource("spring.png", ZeroCopyIntegrationTests.class); - } - - // POST with Person "capitalize" name transformation - - @PostMapping("/person-capitalize") - public Person personCapitalize(@RequestBody Person person) { - return new Person(person.getName().toUpperCase()); - } - - @PostMapping("/completable-future-capitalize") - public CompletableFuture completableFutureCapitalize( - @RequestBody CompletableFuture personFuture) { - return personFuture.thenApply(person -> new Person(person.getName().toUpperCase())); - } - - @PostMapping("/mono-capitalize") - public Mono monoCapitalize(@RequestBody Mono personFuture) { - return personFuture.map(person -> new Person(person.getName().toUpperCase())); - } - - @PostMapping("/single-capitalize") - public Single singleCapitalize(@RequestBody Single personFuture) { - return personFuture.map(person -> new Person(person.getName().toUpperCase())); - } - - @PostMapping("/publisher-capitalize") - public Publisher publisherCapitalize(@RequestBody Publisher persons) { - return Flux - .from(persons) - .map(person -> new Person(person.getName().toUpperCase())); - } - - @PostMapping("/flux-capitalize") - public Flux fluxCapitalize(@RequestBody Flux persons) { - return persons.map(person -> new Person(person.getName().toUpperCase())); - } - - @PostMapping("/observable-capitalize") - public Observable observableCapitalize(@RequestBody Observable persons) { - return persons.map(person -> new Person(person.getName().toUpperCase())); - } - - // POST with Objects to "create" - - @PostMapping("/stream-create") - public Publisher streamCreate(@RequestBody Flux personStream) { - return personStream.collectList().doOnSuccess(persons::addAll).then(); - } - - @PostMapping("/publisher-create") - public Publisher publisherCreate(@RequestBody Publisher personStream) { - return Flux.from(personStream).doOnNext(persons::add).then(); - } - - @PostMapping("/flux-create") - public Mono fluxCreate(@RequestBody Flux personStream) { - return personStream.doOnNext(persons::add).then(); - } - - @PostMapping("/observable-create") - public Observable observableCreate(@RequestBody Observable personStream) { - return personStream.toList().doOnNext(persons::addAll).flatMap(document -> Observable.empty()); - } - - // Async stream - @GetMapping("/stream-result") public Publisher stringStreamResponseBody() { return Flux.interval(100).take(5); } - // Error handling - - @GetMapping("/thrown-exception") - public Publisher handleAndThrowException() { - throw new IllegalStateException("Boo"); - } - - @GetMapping("/error-signal") - public Publisher handleWithError() { - return Mono.error(new IllegalStateException("Boo")); - } - - @ExceptionHandler - public Publisher handleException(IllegalStateException ex) { - return Mono.just("Recovered from error: " + ex.getMessage()); - } - - //TODO add mixed and T request mappings tests - - } - - @Controller - @SuppressWarnings("unused") - private static class TestController { - - @GetMapping("/html") - public String getHtmlPage(@RequestParam String name, Model model) { - model.addAttribute("hello", "Hello: " + name + "!"); - return "test"; - } - - } - - @XmlRootElement @SuppressWarnings("WeakerAccess") - private static class Person { - - private String name; - - @SuppressWarnings("unused") - public Person() { - } - - public Person(String name) { - this.name = name; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - Person person = (Person) o; - return !(this.name != null ? !this.name.equals(person.name) : person.name != null); - } - - @Override - public int hashCode() { - return this.name != null ? this.name.hashCode() : 0; - } - - @Override - public String toString() { - return "Person{" + - "name='" + name + '\'' + - '}'; - } - } - - @XmlRootElement @SuppressWarnings({"WeakerAccess", "unused"}) - private static class People { - - private List persons = new ArrayList<>(); - - public People() { - } - - public People(Person... persons) { - this.persons.addAll(Arrays.asList(persons)); - } - - @XmlElement - public List getPerson() { - return this.persons; - } - } } diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingMessageConversionIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingMessageConversionIntegrationTests.java new file mode 100644 index 0000000000..c98fce1eeb --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingMessageConversionIntegrationTests.java @@ -0,0 +1,489 @@ +/* + * 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.web.reactive.result.method.annotation; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +import org.junit.Test; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import rx.Observable; +import rx.Single; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.ResolvableType; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.codec.json.JacksonJsonEncoder; +import org.springframework.http.server.reactive.ZeroCopyIntegrationTests; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.reactive.config.WebReactiveConfiguration; + +import static java.util.Arrays.asList; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.springframework.http.MediaType.APPLICATION_XML; + + +/** + * {@code @RequestMapping} integration tests focusing on serialization and + * deserialization of the request and response body. + * + * @author Rossen Stoyanchev + * @author Sebastien Deleuze + */ +public class RequestMappingMessageConversionIntegrationTests extends AbstractRequestMappingIntegrationTests { + + private static final ParameterizedTypeReference> PERSON_LIST = + new ParameterizedTypeReference>() {}; + + private static final MediaType JSON = MediaType.APPLICATION_JSON; + + + @Override + protected ApplicationContext initApplicationContext() { + AnnotationConfigApplicationContext wac = new AnnotationConfigApplicationContext(); + wac.register(WebConfig.class); + wac.refresh(); + return wac; + } + + + @Test + public void rawPojoResponse() throws Exception { + Person expected = new Person("Robert"); + assertEquals(expected, performGet("/raw", JSON, Person.class).getBody()); + } + + @Test + public void rawFluxResponse() throws Exception { + String expected = "Hello!"; + assertEquals(expected, performGet("/raw-flux", null, String.class).getBody()); + } + + @Test + public void rawObservableResponse() throws Exception { + String expected = "Hello!"; + assertEquals(expected, performGet("/raw-observable", null, String.class).getBody()); + } + + @Test + public void serializeAsPojo() throws Exception { + Person expected = new Person("Robert"); + assertEquals(expected, performGet("/person", JSON, Person.class).getBody()); + } + + @Test + public void serializeAsCompletableFuture() throws Exception { + Person expected = new Person("Robert"); + assertEquals(expected, performGet("/completable-future", JSON, Person.class).getBody()); + } + + @Test + public void serializeAsMono() throws Exception { + Person expected = new Person("Robert"); + assertEquals(expected, performGet("/mono", JSON, Person.class).getBody()); + } + + @Test + public void serializeAsSingle() throws Exception { + Person expected = new Person("Robert"); + assertEquals(expected, performGet("/single", JSON, Person.class).getBody()); + } + + @Test + public void serializeAsMonoResponseEntity() throws Exception { + Person expected = new Person("Robert"); + assertEquals(expected, performGet("/monoResponseEntity", JSON, Person.class).getBody()); + } + + @Test + public void serializeAsList() throws Exception { + List expected = asList(new Person("Robert"), new Person("Marie")); + assertEquals(expected, performGet("/list", JSON, PERSON_LIST).getBody()); + } + + @Test + public void serializeAsPublisher() throws Exception { + List expected = asList(new Person("Robert"), new Person("Marie")); + assertEquals(expected, performGet("/publisher", JSON, PERSON_LIST).getBody()); + } + + @Test + public void serializeAsFlux() throws Exception { + List expected = asList(new Person("Robert"), new Person("Marie")); + assertEquals(expected, performGet("/flux", JSON, PERSON_LIST).getBody()); + } + + @Test + public void serializeAsObservable() throws Exception { + List expected = asList(new Person("Robert"), new Person("Marie")); + assertEquals(expected, performGet("/observable", JSON, PERSON_LIST).getBody()); + } + + @Test + public void resource() throws Exception { + ResponseEntity response = performGet("/resource", null, byte[].class); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertTrue(response.hasBody()); + assertEquals(951, response.getHeaders().getContentLength()); + assertEquals(951, response.getBody().length); + assertEquals(new MediaType("image", "x-png"), response.getHeaders().getContentType()); + } + + @Test + public void personCapitalize() throws Exception { + assertEquals(new Person("ROBERT"), + performPost("/person-capitalize", JSON, new Person("Robert"), + JSON, Person.class).getBody()); + } + + @Test + public void completableFutureCapitalize() throws Exception { + assertEquals(new Person("ROBERT"), + performPost("/completable-future-capitalize", JSON, new Person("Robert"), + JSON, Person.class).getBody()); + } + + @Test + public void monoCapitalize() throws Exception { + assertEquals(new Person("ROBERT"), + performPost("/mono-capitalize", JSON, new Person("Robert"), + JSON, Person.class).getBody()); + } + + @Test + public void singleCapitalize() throws Exception { + assertEquals(new Person("ROBERT"), + performPost("/single-capitalize", JSON, new Person("Robert"), + JSON, Person.class).getBody()); + } + + @Test + public void publisherCapitalize() throws Exception { + List req = asList(new Person("Robert"), new Person("Marie")); + List res = asList(new Person("ROBERT"), new Person("MARIE")); + assertEquals(res, performPost("/publisher-capitalize", JSON, req, JSON, PERSON_LIST).getBody()); + } + + @Test + public void fluxCapitalize() throws Exception { + List req = asList(new Person("Robert"), new Person("Marie")); + List res = asList(new Person("ROBERT"), new Person("MARIE")); + assertEquals(res, performPost("/flux-capitalize", JSON, req, JSON, PERSON_LIST).getBody()); + } + + @Test + public void observableCapitalize() throws Exception { + List req = asList(new Person("Robert"), new Person("Marie")); + List res = asList(new Person("ROBERT"), new Person("MARIE")); + assertEquals(res, performPost("/observable-capitalize", JSON, req, JSON, PERSON_LIST).getBody()); + } + + @Test + public void publisherCreate() throws Exception { + ResponseEntity entity = performPost("/publisher-create", JSON, + asList(new Person("Robert"), new Person("Marie")), null, Void.class); + + assertEquals(HttpStatus.OK, entity.getStatusCode()); + assertEquals(2, getApplicationContext().getBean(TestRestController.class).persons.size()); + } + + @Test + public void publisherCreateXml() throws Exception { + People people = new People(new Person("Robert"), new Person("Marie")); + ResponseEntity response = performPost("/publisher-create", APPLICATION_XML, people, null, Void.class); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals(2, getApplicationContext().getBean(TestRestController.class).persons.size()); + } + + @Test + public void fluxCreate() throws Exception { + ResponseEntity entity = performPost("/flux-create", JSON, + asList(new Person("Robert"), new Person("Marie")), null, Void.class); + + assertEquals(HttpStatus.OK, entity.getStatusCode()); + assertEquals(2, getApplicationContext().getBean(TestRestController.class).persons.size()); + } + + @Test + public void fluxCreateXml() throws Exception { + People people = new People(new Person("Robert"), new Person("Marie")); + ResponseEntity response = performPost("/flux-create", APPLICATION_XML, people, null, Void.class); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals(2, getApplicationContext().getBean(TestRestController.class).persons.size()); + } + + @Test + public void observableCreate() throws Exception { + ResponseEntity entity = performPost("/observable-create", JSON, + asList(new Person("Robert"), new Person("Marie")), null, Void.class); + + assertEquals(HttpStatus.OK, entity.getStatusCode()); + assertEquals(2, getApplicationContext().getBean(TestRestController.class).persons.size()); + } + + @Test + public void observableCreateXml() throws Exception { + People people = new People(new Person("Robert"), new Person("Marie")); + ResponseEntity response = performPost("/observable-create", APPLICATION_XML, people, null, Void.class); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals(2, getApplicationContext().getBean(TestRestController.class).persons.size()); + } + + + @Configuration + @ComponentScan(resourcePattern = "**/RequestMappingMessageConversionIntegrationTests$*.class") + @SuppressWarnings({"unused", "WeakerAccess"}) + static class WebConfig extends WebReactiveConfiguration { + } + + + @RestController + @SuppressWarnings("unused") + private static class TestRestController { + + final List persons = new ArrayList<>(); + + // GET with "raw" data (DataBuffer) response body + + @GetMapping("/raw") + public Publisher rawResponseBody() { + DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory(); + JacksonJsonEncoder encoder = new JacksonJsonEncoder(); + return encoder.encode(Mono.just(new Person("Robert")), dataBufferFactory, + ResolvableType.forClass(Person.class), JSON).map(DataBuffer::asByteBuffer); + } + + @GetMapping("/raw-flux") + public Flux rawFluxResponseBody() { + return Flux.just(ByteBuffer.wrap("Hello!".getBytes())); + } + + @GetMapping("/raw-observable") + public Observable rawObservableResponseBody() { + return Observable.just(ByteBuffer.wrap("Hello!".getBytes())); + } + + // GET with Person Object(s) response body to "serialize" + + @GetMapping("/person") + public Person personResponseBody() { + return new Person("Robert"); + } + + @GetMapping("/completable-future") + public CompletableFuture completableFutureResponseBody() { + return CompletableFuture.completedFuture(new Person("Robert")); + } + + @GetMapping("/mono") + public Mono monoResponseBody() { + return Mono.just(new Person("Robert")); + } + + @GetMapping("/single") + public Single singleResponseBody() { + return Single.just(new Person("Robert")); + } + + @GetMapping("/monoResponseEntity") + public ResponseEntity> monoResponseEntity() { + Mono body = Mono.just(new Person("Robert")); + return ResponseEntity.ok(body); + } + + @GetMapping("/list") + public List listResponseBody() { + return asList(new Person("Robert"), new Person("Marie")); + } + + @GetMapping("/publisher") + public Publisher publisherResponseBody() { + return Flux.just(new Person("Robert"), new Person("Marie")); + } + + @GetMapping("/flux") + public Flux fluxResponseBody() { + return Flux.just(new Person("Robert"), new Person("Marie")); + } + + @GetMapping("/observable") + public Observable observableResponseBody() { + return Observable.just(new Person("Robert"), new Person("Marie")); + } + + // GET with Resource response body + + @GetMapping("/resource") + public Resource resource() { + return new ClassPathResource("spring.png", ZeroCopyIntegrationTests.class); + } + + // POST with Person "capitalize" name transformation + + @PostMapping("/person-capitalize") + public Person personCapitalize(@RequestBody Person person) { + return new Person(person.getName().toUpperCase()); + } + + @PostMapping("/completable-future-capitalize") + public CompletableFuture completableFutureCapitalize( + @RequestBody CompletableFuture personFuture) { + return personFuture.thenApply(person -> new Person(person.getName().toUpperCase())); + } + + @PostMapping("/mono-capitalize") + public Mono monoCapitalize(@RequestBody Mono personFuture) { + return personFuture.map(person -> new Person(person.getName().toUpperCase())); + } + + @PostMapping("/single-capitalize") + public Single singleCapitalize(@RequestBody Single personFuture) { + return personFuture.map(person -> new Person(person.getName().toUpperCase())); + } + + @PostMapping("/publisher-capitalize") + public Publisher publisherCapitalize(@RequestBody Publisher persons) { + return Flux + .from(persons) + .map(person -> new Person(person.getName().toUpperCase())); + } + + @PostMapping("/flux-capitalize") + public Flux fluxCapitalize(@RequestBody Flux persons) { + return persons.map(person -> new Person(person.getName().toUpperCase())); + } + + @PostMapping("/observable-capitalize") + public Observable observableCapitalize(@RequestBody Observable persons) { + return persons.map(person -> new Person(person.getName().toUpperCase())); + } + + // POST with Objects to "create" + + @PostMapping("/stream-create") + public Publisher streamCreate(@RequestBody Flux personStream) { + return personStream.collectList().doOnSuccess(persons::addAll).then(); + } + + @PostMapping("/publisher-create") + public Publisher publisherCreate(@RequestBody Publisher personStream) { + return Flux.from(personStream).doOnNext(persons::add).then(); + } + + @PostMapping("/flux-create") + public Mono fluxCreate(@RequestBody Flux personStream) { + return personStream.doOnNext(persons::add).then(); + } + + @PostMapping("/observable-create") + public Observable observableCreate(@RequestBody Observable personStream) { + return personStream.toList().doOnNext(persons::addAll).flatMap(document -> Observable.empty()); + } + } + + @XmlRootElement @SuppressWarnings("WeakerAccess") + private static class Person { + + private String name; + + @SuppressWarnings("unused") + public Person() { + } + + public Person(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Person person = (Person) o; + return !(this.name != null ? !this.name.equals(person.name) : person.name != null); + } + + @Override + public int hashCode() { + return this.name != null ? this.name.hashCode() : 0; + } + + @Override + public String toString() { + return "Person{" + + "name='" + name + '\'' + + '}'; + } + } + + @XmlRootElement @SuppressWarnings({"WeakerAccess", "unused"}) + private static class People { + + private List persons = new ArrayList<>(); + + public People() { + } + + public People(Person... persons) { + this.persons.addAll(Arrays.asList(persons)); + } + + @XmlElement + public List getPerson() { + return this.persons; + } + + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingViewResolutionIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingViewResolutionIntegrationTests.java new file mode 100644 index 0000000000..9ea276300a --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingViewResolutionIntegrationTests.java @@ -0,0 +1,93 @@ +/* + * 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.web.reactive.result.method.annotation; + +import org.junit.Test; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.reactive.config.ViewResolverRegistry; +import org.springframework.web.reactive.config.WebReactiveConfiguration; +import org.springframework.web.reactive.result.view.freemarker.FreeMarkerConfigurer; + +import static org.junit.Assert.assertEquals; + + +/** + * {@code @RequestMapping} integration tests with view resolution scenarios. + * + * @author Rossen Stoyanchev + */ +public class RequestMappingViewResolutionIntegrationTests extends AbstractRequestMappingIntegrationTests { + + + @Override + protected ApplicationContext initApplicationContext() { + AnnotationConfigApplicationContext wac = new AnnotationConfigApplicationContext(); + wac.register(WebConfig.class); + wac.refresh(); + return wac; + } + + + @Test + public void html() throws Exception { + String expected = "Hello: Jason!"; + assertEquals(expected, performGet("/html?name=Jason", MediaType.TEXT_HTML, String.class).getBody()); + } + + + @Configuration + @ComponentScan(resourcePattern = "**/RequestMappingViewResolutionIntegrationTests$*.class") + @SuppressWarnings({"unused", "WeakerAccess"}) + static class WebConfig extends WebReactiveConfiguration { + + @Override + protected void configureViewResolvers(ViewResolverRegistry registry) { + registry.freeMarker(); + } + + @Bean + public FreeMarkerConfigurer freeMarkerConfig() { + FreeMarkerConfigurer configurer = new FreeMarkerConfigurer(); + configurer.setPreferFileSystemAccess(false); + configurer.setTemplateLoaderPath("classpath*:org/springframework/web/reactive/view/freemarker/"); + return configurer; + } + + } + + @Controller + @SuppressWarnings("unused") + private static class TestController { + + @GetMapping("/html") + public String getHtmlPage(@RequestParam String name, Model model) { + model.addAttribute("hello", "Hello: " + name + "!"); + return "test"; + } + } + +} From aa47616be231133236e87cef7af1a569677a7792 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 11 Jul 2016 18:02:34 -0400 Subject: [PATCH 489/498] Split TestRestController into use-case sub-classes --- ...pingMessageConversionIntegrationTests.java | 220 +++++++++--------- 1 file changed, 115 insertions(+), 105 deletions(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingMessageConversionIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingMessageConversionIntegrationTests.java index c98fce1eeb..7991dd14e6 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingMessageConversionIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingMessageConversionIntegrationTests.java @@ -50,6 +50,7 @@ import org.springframework.http.server.reactive.ZeroCopyIntegrationTests; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.reactive.config.WebReactiveConfiguration; @@ -84,75 +85,75 @@ public class RequestMappingMessageConversionIntegrationTests extends AbstractReq @Test - public void rawPojoResponse() throws Exception { + public void byteBufferResponseBodyWithPublisher() throws Exception { Person expected = new Person("Robert"); - assertEquals(expected, performGet("/raw", JSON, Person.class).getBody()); + assertEquals(expected, performGet("/raw-response/publisher", JSON, Person.class).getBody()); } @Test - public void rawFluxResponse() throws Exception { + public void byteBufferResponseBodyWithFlux() throws Exception { String expected = "Hello!"; - assertEquals(expected, performGet("/raw-flux", null, String.class).getBody()); + assertEquals(expected, performGet("/raw-response/flux", null, String.class).getBody()); } @Test - public void rawObservableResponse() throws Exception { + public void byteBufferResponseBodyWithObservable() throws Exception { String expected = "Hello!"; - assertEquals(expected, performGet("/raw-observable", null, String.class).getBody()); + assertEquals(expected, performGet("/raw-response/observable", null, String.class).getBody()); } @Test - public void serializeAsPojo() throws Exception { + public void personResponseBody() throws Exception { Person expected = new Person("Robert"); - assertEquals(expected, performGet("/person", JSON, Person.class).getBody()); + assertEquals(expected, performGet("/person-response/person", JSON, Person.class).getBody()); } @Test - public void serializeAsCompletableFuture() throws Exception { + public void personResponseBodyWithCompletableFuture() throws Exception { Person expected = new Person("Robert"); - assertEquals(expected, performGet("/completable-future", JSON, Person.class).getBody()); + assertEquals(expected, performGet("/person-response/completable-future", JSON, Person.class).getBody()); } @Test - public void serializeAsMono() throws Exception { + public void personResponseBodyWithMono() throws Exception { Person expected = new Person("Robert"); - assertEquals(expected, performGet("/mono", JSON, Person.class).getBody()); + assertEquals(expected, performGet("/person-response/mono", JSON, Person.class).getBody()); } @Test - public void serializeAsSingle() throws Exception { + public void personResponseBodyWithSingle() throws Exception { Person expected = new Person("Robert"); - assertEquals(expected, performGet("/single", JSON, Person.class).getBody()); + assertEquals(expected, performGet("/person-response/single", JSON, Person.class).getBody()); } @Test - public void serializeAsMonoResponseEntity() throws Exception { + public void personResponseBodyWithMonoResponseEntity() throws Exception { Person expected = new Person("Robert"); - assertEquals(expected, performGet("/monoResponseEntity", JSON, Person.class).getBody()); + assertEquals(expected, performGet("/person-response/mono-response-entity", JSON, Person.class).getBody()); } @Test - public void serializeAsList() throws Exception { + public void personResponseBodyWithList() throws Exception { List expected = asList(new Person("Robert"), new Person("Marie")); - assertEquals(expected, performGet("/list", JSON, PERSON_LIST).getBody()); + assertEquals(expected, performGet("/person-response/list", JSON, PERSON_LIST).getBody()); } @Test - public void serializeAsPublisher() throws Exception { + public void personResponseBodyWithPublisher() throws Exception { List expected = asList(new Person("Robert"), new Person("Marie")); - assertEquals(expected, performGet("/publisher", JSON, PERSON_LIST).getBody()); + assertEquals(expected, performGet("/person-response/publisher", JSON, PERSON_LIST).getBody()); } @Test - public void serializeAsFlux() throws Exception { + public void personResponseBodyWithFlux() throws Exception { List expected = asList(new Person("Robert"), new Person("Marie")); - assertEquals(expected, performGet("/flux", JSON, PERSON_LIST).getBody()); + assertEquals(expected, performGet("/person-response/flux", JSON, PERSON_LIST).getBody()); } @Test - public void serializeAsObservable() throws Exception { + public void personResponseBodyWithObservable() throws Exception { List expected = asList(new Person("Robert"), new Person("Marie")); - assertEquals(expected, performGet("/observable", JSON, PERSON_LIST).getBody()); + assertEquals(expected, performGet("/person-response/observable", JSON, PERSON_LIST).getBody()); } @Test @@ -167,106 +168,106 @@ public class RequestMappingMessageConversionIntegrationTests extends AbstractReq } @Test - public void personCapitalize() throws Exception { + public void personTransform() throws Exception { assertEquals(new Person("ROBERT"), - performPost("/person-capitalize", JSON, new Person("Robert"), + performPost("/person-transform/person", JSON, new Person("Robert"), JSON, Person.class).getBody()); } @Test - public void completableFutureCapitalize() throws Exception { + public void personTransformWithCompletableFuture() throws Exception { assertEquals(new Person("ROBERT"), - performPost("/completable-future-capitalize", JSON, new Person("Robert"), + performPost("/person-transform/completable-future", JSON, new Person("Robert"), JSON, Person.class).getBody()); } @Test - public void monoCapitalize() throws Exception { + public void personTransformWithMono() throws Exception { assertEquals(new Person("ROBERT"), - performPost("/mono-capitalize", JSON, new Person("Robert"), + performPost("/person-transform/mono", JSON, new Person("Robert"), JSON, Person.class).getBody()); } @Test - public void singleCapitalize() throws Exception { + public void personTransformWithSingle() throws Exception { assertEquals(new Person("ROBERT"), - performPost("/single-capitalize", JSON, new Person("Robert"), + performPost("/person-transform/single", JSON, new Person("Robert"), JSON, Person.class).getBody()); } @Test - public void publisherCapitalize() throws Exception { + public void personTransformWithPublisher() throws Exception { List req = asList(new Person("Robert"), new Person("Marie")); List res = asList(new Person("ROBERT"), new Person("MARIE")); - assertEquals(res, performPost("/publisher-capitalize", JSON, req, JSON, PERSON_LIST).getBody()); + assertEquals(res, performPost("/person-transform/publisher", JSON, req, JSON, PERSON_LIST).getBody()); } @Test - public void fluxCapitalize() throws Exception { + public void personTransformWithFlux() throws Exception { List req = asList(new Person("Robert"), new Person("Marie")); List res = asList(new Person("ROBERT"), new Person("MARIE")); - assertEquals(res, performPost("/flux-capitalize", JSON, req, JSON, PERSON_LIST).getBody()); + assertEquals(res, performPost("/person-transform/flux", JSON, req, JSON, PERSON_LIST).getBody()); } @Test - public void observableCapitalize() throws Exception { + public void personTransformWithObservable() throws Exception { List req = asList(new Person("Robert"), new Person("Marie")); List res = asList(new Person("ROBERT"), new Person("MARIE")); - assertEquals(res, performPost("/observable-capitalize", JSON, req, JSON, PERSON_LIST).getBody()); + assertEquals(res, performPost("/person-transform/observable", JSON, req, JSON, PERSON_LIST).getBody()); } @Test - public void publisherCreate() throws Exception { - ResponseEntity entity = performPost("/publisher-create", JSON, + public void personCreateWithPublisherJson() throws Exception { + ResponseEntity entity = performPost("/person-create/publisher", JSON, asList(new Person("Robert"), new Person("Marie")), null, Void.class); assertEquals(HttpStatus.OK, entity.getStatusCode()); - assertEquals(2, getApplicationContext().getBean(TestRestController.class).persons.size()); + assertEquals(2, getApplicationContext().getBean(PersonCreateController.class).persons.size()); } @Test - public void publisherCreateXml() throws Exception { + public void personCreateWithPublisherXml() throws Exception { People people = new People(new Person("Robert"), new Person("Marie")); - ResponseEntity response = performPost("/publisher-create", APPLICATION_XML, people, null, Void.class); + ResponseEntity response = performPost("/person-create/publisher", APPLICATION_XML, people, null, Void.class); assertEquals(HttpStatus.OK, response.getStatusCode()); - assertEquals(2, getApplicationContext().getBean(TestRestController.class).persons.size()); + assertEquals(2, getApplicationContext().getBean(PersonCreateController.class).persons.size()); } @Test - public void fluxCreate() throws Exception { - ResponseEntity entity = performPost("/flux-create", JSON, + public void personCreateWithFluxJson() throws Exception { + ResponseEntity entity = performPost("/person-create/flux", JSON, asList(new Person("Robert"), new Person("Marie")), null, Void.class); assertEquals(HttpStatus.OK, entity.getStatusCode()); - assertEquals(2, getApplicationContext().getBean(TestRestController.class).persons.size()); + assertEquals(2, getApplicationContext().getBean(PersonCreateController.class).persons.size()); } @Test - public void fluxCreateXml() throws Exception { + public void personCreateWithFluxXml() throws Exception { People people = new People(new Person("Robert"), new Person("Marie")); - ResponseEntity response = performPost("/flux-create", APPLICATION_XML, people, null, Void.class); + ResponseEntity response = performPost("/person-create/flux", APPLICATION_XML, people, null, Void.class); assertEquals(HttpStatus.OK, response.getStatusCode()); - assertEquals(2, getApplicationContext().getBean(TestRestController.class).persons.size()); + assertEquals(2, getApplicationContext().getBean(PersonCreateController.class).persons.size()); } @Test - public void observableCreate() throws Exception { - ResponseEntity entity = performPost("/observable-create", JSON, + public void personCreateWithObservableJson() throws Exception { + ResponseEntity entity = performPost("/person-create/observable", JSON, asList(new Person("Robert"), new Person("Marie")), null, Void.class); assertEquals(HttpStatus.OK, entity.getStatusCode()); - assertEquals(2, getApplicationContext().getBean(TestRestController.class).persons.size()); + assertEquals(2, getApplicationContext().getBean(PersonCreateController.class).persons.size()); } @Test - public void observableCreateXml() throws Exception { + public void personCreateWithObservableXml() throws Exception { People people = new People(new Person("Robert"), new Person("Marie")); - ResponseEntity response = performPost("/observable-create", APPLICATION_XML, people, null, Void.class); + ResponseEntity response = performPost("/person-create/observable", APPLICATION_XML, people, null, Void.class); assertEquals(HttpStatus.OK, response.getStatusCode()); - assertEquals(2, getApplicationContext().getBean(TestRestController.class).persons.size()); + assertEquals(2, getApplicationContext().getBean(PersonCreateController.class).persons.size()); } @@ -278,145 +279,154 @@ public class RequestMappingMessageConversionIntegrationTests extends AbstractReq @RestController + @RequestMapping("/raw-response") @SuppressWarnings("unused") - private static class TestRestController { + private static class RawResponseBodyController { - final List persons = new ArrayList<>(); - - // GET with "raw" data (DataBuffer) response body - - @GetMapping("/raw") - public Publisher rawResponseBody() { + @GetMapping("/publisher") + public Publisher getPublisher() { DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory(); JacksonJsonEncoder encoder = new JacksonJsonEncoder(); return encoder.encode(Mono.just(new Person("Robert")), dataBufferFactory, ResolvableType.forClass(Person.class), JSON).map(DataBuffer::asByteBuffer); } - @GetMapping("/raw-flux") - public Flux rawFluxResponseBody() { + @GetMapping("/flux") + public Flux getFlux() { return Flux.just(ByteBuffer.wrap("Hello!".getBytes())); } - @GetMapping("/raw-observable") - public Observable rawObservableResponseBody() { + @GetMapping("/observable") + public Observable getObservable() { return Observable.just(ByteBuffer.wrap("Hello!".getBytes())); } + } - // GET with Person Object(s) response body to "serialize" + @RestController + @RequestMapping("/person-response") + @SuppressWarnings("unused") + private static class PersonResponseBodyController { @GetMapping("/person") - public Person personResponseBody() { + public Person getPerson() { return new Person("Robert"); } @GetMapping("/completable-future") - public CompletableFuture completableFutureResponseBody() { + public CompletableFuture getCompletableFuture() { return CompletableFuture.completedFuture(new Person("Robert")); } @GetMapping("/mono") - public Mono monoResponseBody() { + public Mono getMono() { return Mono.just(new Person("Robert")); } @GetMapping("/single") - public Single singleResponseBody() { + public Single getSingle() { return Single.just(new Person("Robert")); } - @GetMapping("/monoResponseEntity") - public ResponseEntity> monoResponseEntity() { + @GetMapping("/mono-response-entity") + public ResponseEntity> getMonoResponseEntity() { Mono body = Mono.just(new Person("Robert")); return ResponseEntity.ok(body); } @GetMapping("/list") - public List listResponseBody() { + public List getList() { return asList(new Person("Robert"), new Person("Marie")); } @GetMapping("/publisher") - public Publisher publisherResponseBody() { + public Publisher getPublisher() { return Flux.just(new Person("Robert"), new Person("Marie")); } @GetMapping("/flux") - public Flux fluxResponseBody() { + public Flux getFlux() { return Flux.just(new Person("Robert"), new Person("Marie")); } @GetMapping("/observable") - public Observable observableResponseBody() { + public Observable getObservable() { return Observable.just(new Person("Robert"), new Person("Marie")); } + } - // GET with Resource response body + @RestController + @SuppressWarnings("unused") + private static class ResourceController { @GetMapping("/resource") public Resource resource() { return new ClassPathResource("spring.png", ZeroCopyIntegrationTests.class); } + } - // POST with Person "capitalize" name transformation + @RestController + @RequestMapping("/person-transform") + @SuppressWarnings("unused") + private static class PersonTransformationController { - @PostMapping("/person-capitalize") - public Person personCapitalize(@RequestBody Person person) { + @PostMapping("/person") + public Person transformPerson(@RequestBody Person person) { return new Person(person.getName().toUpperCase()); } - @PostMapping("/completable-future-capitalize") - public CompletableFuture completableFutureCapitalize( + @PostMapping("/completable-future") + public CompletableFuture transformCompletableFuture( @RequestBody CompletableFuture personFuture) { return personFuture.thenApply(person -> new Person(person.getName().toUpperCase())); } - @PostMapping("/mono-capitalize") - public Mono monoCapitalize(@RequestBody Mono personFuture) { + @PostMapping("/mono") + public Mono transformMono(@RequestBody Mono personFuture) { return personFuture.map(person -> new Person(person.getName().toUpperCase())); } - @PostMapping("/single-capitalize") - public Single singleCapitalize(@RequestBody Single personFuture) { + @PostMapping("/single") + public Single transformSingle(@RequestBody Single personFuture) { return personFuture.map(person -> new Person(person.getName().toUpperCase())); } - @PostMapping("/publisher-capitalize") - public Publisher publisherCapitalize(@RequestBody Publisher persons) { + @PostMapping("/publisher") + public Publisher transformPublisher(@RequestBody Publisher persons) { return Flux .from(persons) .map(person -> new Person(person.getName().toUpperCase())); } - @PostMapping("/flux-capitalize") - public Flux fluxCapitalize(@RequestBody Flux persons) { + @PostMapping("/flux") + public Flux transformFlux(@RequestBody Flux persons) { return persons.map(person -> new Person(person.getName().toUpperCase())); } - @PostMapping("/observable-capitalize") - public Observable observableCapitalize(@RequestBody Observable persons) { + @PostMapping("/observable") + public Observable transformObservable(@RequestBody Observable persons) { return persons.map(person -> new Person(person.getName().toUpperCase())); } + } - // POST with Objects to "create" + @RestController + @RequestMapping("/person-create") + @SuppressWarnings("unused") + private static class PersonCreateController { - @PostMapping("/stream-create") - public Publisher streamCreate(@RequestBody Flux personStream) { - return personStream.collectList().doOnSuccess(persons::addAll).then(); - } + final List persons = new ArrayList<>(); - @PostMapping("/publisher-create") - public Publisher publisherCreate(@RequestBody Publisher personStream) { + @PostMapping("/publisher") + public Publisher createWithPublisher(@RequestBody Publisher personStream) { return Flux.from(personStream).doOnNext(persons::add).then(); } - @PostMapping("/flux-create") - public Mono fluxCreate(@RequestBody Flux personStream) { + @PostMapping("/flux") + public Mono createWithFlux(@RequestBody Flux personStream) { return personStream.doOnNext(persons::add).then(); } - @PostMapping("/observable-create") - public Observable observableCreate(@RequestBody Observable personStream) { + @PostMapping("/observable") + public Observable createWithObservable(@RequestBody Observable personStream) { return personStream.toList().doOnNext(persons::addAll).flatMap(document -> Observable.empty()); } } From c2c7e3b596a6551d948a87f03a9b6f49ffec6496 Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Tue, 12 Jul 2016 12:13:50 +0100 Subject: [PATCH 490/498] Polish timed operators and remove useTimer() --- .../http/server/reactive/AsyncIntegrationTests.java | 1 - 1 file changed, 1 deletion(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java index 9405c7f656..ecfaacb4b2 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncIntegrationTests.java @@ -65,7 +65,6 @@ public class AsyncIntegrationTests extends AbstractHttpHandlerIntegrationTests { @Override public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { return response.writeWith(Flux.just("h", "e", "l", "l", "o") - .useTimer(Schedulers.timer()) .delay(Duration.ofMillis(100)) .publishOn(asyncGroup) .collect(dataBufferFactory::allocateBuffer, (buffer, str) -> buffer.write(str.getBytes()))); From d736245f28e71b0a9d462c8197b61f417acace5b Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Tue, 12 Jul 2016 13:25:18 +0200 Subject: [PATCH 491/498] Changed from interval() to intervalMillis() --- .../http/server/reactive/FlushingIntegrationTests.java | 2 +- .../method/annotation/RequestMappingIntegrationTests.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/FlushingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/FlushingIntegrationTests.java index 831e5a55ad..16b608e60e 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/FlushingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/FlushingIntegrationTests.java @@ -72,7 +72,7 @@ public class FlushingIntegrationTests extends AbstractHttpHandlerIntegrationTest @Override public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { Flux responseBody = Flux - .interval(50) + .intervalMillis(50) .map(l -> { byte[] data = ("data" + l).getBytes(); DataBuffer buffer = response.bufferFactory().allocateBuffer(data.length); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java index 3b5a332319..b5fc3627b2 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java @@ -82,7 +82,7 @@ public class RequestMappingIntegrationTests extends AbstractRequestMappingIntegr @GetMapping("/stream-result") public Publisher stringStreamResponseBody() { - return Flux.interval(100).take(5); + return Flux.intervalMillis(100).take(5); } } From e906a78e0fafda26c24520189a1802bc5bc0877d Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Tue, 12 Jul 2016 17:36:30 +0200 Subject: [PATCH 492/498] Move server testing infrastructure to test sources --- spring-web-reactive/.gitignore | 2 +- .../AbstractHttpHandlerIntegrationTests.java | 12 ++++++------ .../reactive/ErrorHandlerIntegrationTests.java | 2 +- .../server/reactive/ZeroCopyIntegrationTests.java | 4 ++-- .../http/server/reactive/bootstrap}/HttpServer.java | 2 +- .../reactive/bootstrap}/HttpServerSupport.java | 2 +- .../server/reactive/bootstrap}/JettyHttpServer.java | 2 +- .../reactive/bootstrap}/ReactorHttpServer.java | 2 +- .../reactive/bootstrap}/RxNettyHttpServer.java | 2 +- .../server/reactive/bootstrap}/TomcatHttpServer.java | 2 +- .../reactive/bootstrap}/UndertowHttpServer.java | 2 +- .../server/reactive/bootstrap}/package-info.java | 2 +- 12 files changed, 18 insertions(+), 18 deletions(-) rename spring-web-reactive/src/{main/java/org/springframework/http/server/reactive/boot => test/java/org/springframework/http/server/reactive/bootstrap}/HttpServer.java (94%) rename spring-web-reactive/src/{main/java/org/springframework/http/server/reactive/boot => test/java/org/springframework/http/server/reactive/bootstrap}/HttpServerSupport.java (95%) rename spring-web-reactive/src/{main/java/org/springframework/http/server/reactive/boot => test/java/org/springframework/http/server/reactive/bootstrap}/JettyHttpServer.java (97%) rename spring-web-reactive/src/{main/java/org/springframework/http/server/reactive/boot => test/java/org/springframework/http/server/reactive/bootstrap}/ReactorHttpServer.java (97%) rename spring-web-reactive/src/{main/java/org/springframework/http/server/reactive/boot => test/java/org/springframework/http/server/reactive/bootstrap}/RxNettyHttpServer.java (96%) rename spring-web-reactive/src/{main/java/org/springframework/http/server/reactive/boot => test/java/org/springframework/http/server/reactive/bootstrap}/TomcatHttpServer.java (97%) rename spring-web-reactive/src/{main/java/org/springframework/http/server/reactive/boot => test/java/org/springframework/http/server/reactive/bootstrap}/UndertowHttpServer.java (96%) rename spring-web-reactive/src/{main/java/org/springframework/http/server/reactive/boot => test/java/org/springframework/http/server/reactive/bootstrap}/package-info.java (73%) diff --git a/spring-web-reactive/.gitignore b/spring-web-reactive/.gitignore index 207211d54c..0b81cf52be 100644 --- a/spring-web-reactive/.gitignore +++ b/spring-web-reactive/.gitignore @@ -6,5 +6,5 @@ target /.idea/ bin .gradle -tomcat* +/tomcat.*/ build diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AbstractHttpHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AbstractHttpHandlerIntegrationTests.java index 4a2b94e2db..1b705ed5f5 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AbstractHttpHandlerIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AbstractHttpHandlerIntegrationTests.java @@ -23,12 +23,12 @@ import org.junit.Before; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; -import org.springframework.http.server.reactive.boot.HttpServer; -import org.springframework.http.server.reactive.boot.JettyHttpServer; -import org.springframework.http.server.reactive.boot.ReactorHttpServer; -import org.springframework.http.server.reactive.boot.RxNettyHttpServer; -import org.springframework.http.server.reactive.boot.TomcatHttpServer; -import org.springframework.http.server.reactive.boot.UndertowHttpServer; +import org.springframework.http.server.reactive.bootstrap.HttpServer; +import org.springframework.http.server.reactive.bootstrap.JettyHttpServer; +import org.springframework.http.server.reactive.bootstrap.ReactorHttpServer; +import org.springframework.http.server.reactive.bootstrap.RxNettyHttpServer; +import org.springframework.http.server.reactive.bootstrap.TomcatHttpServer; +import org.springframework.http.server.reactive.bootstrap.UndertowHttpServer; import org.springframework.util.SocketUtils; diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ErrorHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ErrorHandlerIntegrationTests.java index c7dfa83f73..8f8b1d6320 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ErrorHandlerIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ErrorHandlerIntegrationTests.java @@ -25,7 +25,7 @@ import reactor.core.publisher.Mono; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.http.client.ClientHttpResponse; -import org.springframework.http.server.reactive.boot.ReactorHttpServer; +import org.springframework.http.server.reactive.bootstrap.ReactorHttpServer; import org.springframework.web.client.ResponseErrorHandler; import org.springframework.web.client.RestTemplate; diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ZeroCopyIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ZeroCopyIntegrationTests.java index 1b1e8c7aa0..09d5a22160 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ZeroCopyIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ZeroCopyIntegrationTests.java @@ -28,8 +28,8 @@ import org.springframework.http.MediaType; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; import org.springframework.http.ZeroCopyHttpOutputMessage; -import org.springframework.http.server.reactive.boot.ReactorHttpServer; -import org.springframework.http.server.reactive.boot.UndertowHttpServer; +import org.springframework.http.server.reactive.bootstrap.ReactorHttpServer; +import org.springframework.http.server.reactive.bootstrap.UndertowHttpServer; import org.springframework.web.client.RestTemplate; import static org.junit.Assert.assertEquals; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/HttpServer.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/bootstrap/HttpServer.java similarity index 94% rename from spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/HttpServer.java rename to spring-web-reactive/src/test/java/org/springframework/http/server/reactive/bootstrap/HttpServer.java index e706861852..8bc8ae12d4 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/HttpServer.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/bootstrap/HttpServer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.http.server.reactive.boot; +package org.springframework.http.server.reactive.bootstrap; import org.springframework.beans.factory.InitializingBean; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/HttpServerSupport.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/bootstrap/HttpServerSupport.java similarity index 95% rename from spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/HttpServerSupport.java rename to spring-web-reactive/src/test/java/org/springframework/http/server/reactive/bootstrap/HttpServerSupport.java index c238f60e55..4c22291b91 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/HttpServerSupport.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/bootstrap/HttpServerSupport.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.http.server.reactive.boot; +package org.springframework.http.server.reactive.bootstrap; import org.springframework.http.server.reactive.HttpHandler; import org.springframework.util.SocketUtils; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/JettyHttpServer.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/bootstrap/JettyHttpServer.java similarity index 97% rename from spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/JettyHttpServer.java rename to spring-web-reactive/src/test/java/org/springframework/http/server/reactive/bootstrap/JettyHttpServer.java index 51837739ce..485af7594b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/JettyHttpServer.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/bootstrap/JettyHttpServer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.http.server.reactive.boot; +package org.springframework.http.server.reactive.bootstrap; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/bootstrap/ReactorHttpServer.java similarity index 97% rename from spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java rename to spring-web-reactive/src/test/java/org/springframework/http/server/reactive/bootstrap/ReactorHttpServer.java index 8bb845a534..1bba2c3c85 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/ReactorHttpServer.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/bootstrap/ReactorHttpServer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.http.server.reactive.boot; +package org.springframework.http.server.reactive.bootstrap; import reactor.core.flow.Loopback; import reactor.core.state.Completable; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/RxNettyHttpServer.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/bootstrap/RxNettyHttpServer.java similarity index 96% rename from spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/RxNettyHttpServer.java rename to spring-web-reactive/src/test/java/org/springframework/http/server/reactive/bootstrap/RxNettyHttpServer.java index 1117199684..051ba64e66 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/RxNettyHttpServer.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/bootstrap/RxNettyHttpServer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.http.server.reactive.boot; +package org.springframework.http.server.reactive.bootstrap; import java.net.InetSocketAddress; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/TomcatHttpServer.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/bootstrap/TomcatHttpServer.java similarity index 97% rename from spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/TomcatHttpServer.java rename to spring-web-reactive/src/test/java/org/springframework/http/server/reactive/bootstrap/TomcatHttpServer.java index acbb4568d3..1a47951590 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/TomcatHttpServer.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/bootstrap/TomcatHttpServer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.http.server.reactive.boot; +package org.springframework.http.server.reactive.bootstrap; import java.io.File; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/UndertowHttpServer.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/bootstrap/UndertowHttpServer.java similarity index 96% rename from spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/UndertowHttpServer.java rename to spring-web-reactive/src/test/java/org/springframework/http/server/reactive/bootstrap/UndertowHttpServer.java index 5c6048353c..ca9555a816 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/UndertowHttpServer.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/bootstrap/UndertowHttpServer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.http.server.reactive.boot; +package org.springframework.http.server.reactive.bootstrap; import io.undertow.Undertow; import io.undertow.server.HttpHandler; diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/package-info.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/bootstrap/package-info.java similarity index 73% rename from spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/package-info.java rename to spring-web-reactive/src/test/java/org/springframework/http/server/reactive/bootstrap/package-info.java index 56e9fbd187..93e5dac549 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/boot/package-info.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/bootstrap/package-info.java @@ -2,4 +2,4 @@ * This package contains temporary interfaces and classes for running embedded servers. * They are expected to be replaced by an upcoming Spring Boot support. */ -package org.springframework.http.server.reactive.boot; +package org.springframework.http.server.reactive.bootstrap; From d2fce24eb4d685e89ce6e818ac584cfafecbd703 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Tue, 12 Jul 2016 13:08:21 +0200 Subject: [PATCH 493/498] Introduced ResponseBodyWriteResultPublisher Refactored Publisher in AbstractResponseBodyProcessor into separate ResponseBodyWriteResultPublisher. --- .../AbstractResponseBodyProcessor.java | 251 ++++-------------- .../ResponseBodyWriteResultPublisher.java | 211 +++++++++++++++ .../reactive/ServletHttpHandlerAdapter.java | 12 +- .../reactive/UndertowServerHttpResponse.java | 19 -- 4 files changed, 261 insertions(+), 232 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ResponseBodyWriteResultPublisher.java diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractResponseBodyProcessor.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractResponseBodyProcessor.java index c187a5c49d..e4463fb6a7 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractResponseBodyProcessor.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractResponseBodyProcessor.java @@ -27,7 +27,6 @@ import org.apache.commons.logging.LogFactory; import org.reactivestreams.Processor; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; -import reactor.core.util.BackpressureUtils; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.FlushingDataBuffer; @@ -46,80 +45,57 @@ abstract class AbstractResponseBodyProcessor implements Processor subscriberState = - new AtomicReference<>(SubscriberState.UNSUBSCRIBED); + private final ResponseBodyWriteResultPublisher publisherDelegate = + new ResponseBodyWriteResultPublisher(); - private final AtomicReference publisherState = - new AtomicReference<>(PublisherState.UNSUBSCRIBED); + private final AtomicReference state = + new AtomicReference<>(State.UNSUBSCRIBED); private volatile DataBuffer currentBuffer; private volatile boolean subscriberCompleted; - private volatile boolean publisherCompleted; - - private volatile Throwable publisherError; - private Subscription subscription; - private Subscriber subscriber; - // Subscriber @Override public final void onSubscribe(Subscription subscription) { if (logger.isTraceEnabled()) { - logger.trace("SUB " + this.subscriberState + " onSubscribe: " + subscription); + logger.trace(this.state + " onSubscribe: " + subscription); } - this.subscriberState.get().onSubscribe(this, subscription); + this.state.get().onSubscribe(this, subscription); } @Override public final void onNext(DataBuffer dataBuffer) { if (logger.isTraceEnabled()) { - logger.trace("SUB " + this.subscriberState + " onNext: " + dataBuffer); + logger.trace(this.state + " onNext: " + dataBuffer); } - this.subscriberState.get().onNext(this, dataBuffer); + this.state.get().onNext(this, dataBuffer); } @Override public final void onError(Throwable t) { if (logger.isErrorEnabled()) { - logger.error("SUB " + this.subscriberState + " publishError: " + t, t); + logger.error(this.state + " onError: " + t, t); } - this.subscriberState.get().onError(this, t); + this.state.get().onError(this, t); } @Override public final void onComplete() { if (logger.isTraceEnabled()) { - logger.trace("SUB " + this.subscriberState + " onComplete"); + logger.trace(this.state + " onComplete"); } - this.subscriberState.get().onComplete(this); + this.state.get().onComplete(this); } // Publisher @Override public final void subscribe(Subscriber subscriber) { - if (logger.isTraceEnabled()) { - logger.trace("PUB " + this.publisherState + " subscribe: " + subscriber); - } - this.publisherState.get().subscribe(this, subscriber); - } - - private void publishComplete() { - if (logger.isTraceEnabled()) { - logger.trace("PUB " + this.publisherState + " publishComplete"); - } - this.publisherState.get().publishComplete(this); - } - - private void publishError(Throwable t) { - if (logger.isTraceEnabled()) { - logger.trace("PUB " + this.publisherState + " publishError: " + t); - } - this.publisherState.get().publishError(this, t); + this.publisherDelegate.subscribe(subscriber); } // listener methods @@ -130,7 +106,7 @@ abstract class AbstractResponseBodyProcessor implements Processor * Refer to the individual states for more information. */ - private enum SubscriberState { + private enum State { /** * The initial unsubscribed state. Will respond to {@code onSubscribe} by @@ -253,7 +195,7 @@ abstract class AbstractResponseBodyProcessor implements Processor subscriber) { - Objects.requireNonNull(subscriber); - if (processor.changePublisherState(this, SUBSCRIBED)) { - Subscription subscription = new ResponseBodySubscription(processor); - processor.subscriber = subscriber; - subscriber.onSubscribe(subscription); - if (processor.publisherCompleted) { - processor.publishComplete(); - } - else if (processor.publisherError != null) { - processor.publishError(processor.publisherError); - } - } - else { - throw new IllegalStateException(toString()); - } - } - - @Override - void publishComplete(AbstractResponseBodyProcessor processor) { - processor.publisherCompleted = true; - } - - @Override - void publishError(AbstractResponseBodyProcessor processor, Throwable t) { - processor.publisherError = t; - } - }, - SUBSCRIBED { - @Override - void request(AbstractResponseBodyProcessor processor, long n) { - BackpressureUtils.checkRequest(n, processor.subscriber); - } - - @Override - void publishComplete(AbstractResponseBodyProcessor processor) { - if (processor.changePublisherState(this, COMPLETED)) { - processor.subscriber.onComplete(); - } - } - - @Override - void publishError(AbstractResponseBodyProcessor processor, Throwable t) { - if (processor.changePublisherState(this, COMPLETED)) { - processor.subscriber.onError(t); - } - } - - }, - COMPLETED { - @Override - void request(AbstractResponseBodyProcessor processor, long n) { - // ignore - } - - @Override - void cancel(AbstractResponseBodyProcessor processor) { - // ignore - } - - @Override - void publishComplete(AbstractResponseBodyProcessor processor) { - // ignore - } - - @Override - void publishError(AbstractResponseBodyProcessor processor, Throwable t) { - // ignore - } - }; - - void subscribe(AbstractResponseBodyProcessor processor, - Subscriber subscriber) { - throw new IllegalStateException(toString()); - } - - void request(AbstractResponseBodyProcessor processor, long n) { - throw new IllegalStateException(toString()); - } - - void cancel(AbstractResponseBodyProcessor processor) { - processor.changePublisherState(this, COMPLETED); - } - - void publishComplete(AbstractResponseBodyProcessor processor) { - throw new IllegalStateException(toString()); - } - - void publishError(AbstractResponseBodyProcessor processor, Throwable t) { - throw new IllegalStateException(toString()); - } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ResponseBodyWriteResultPublisher.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ResponseBodyWriteResultPublisher.java new file mode 100644 index 0000000000..eeaab20ed0 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ResponseBodyWriteResultPublisher.java @@ -0,0 +1,211 @@ +/* + * 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.http.server.reactive; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import reactor.core.util.BackpressureUtils; + +/** + * Publisher returned from {@link ServerHttpResponse#writeWith(Publisher)}. + * @author Arjen Poutsma + */ +class ResponseBodyWriteResultPublisher implements Publisher { + + private static final Log logger = + LogFactory.getLog(ResponseBodyWriteResultPublisher.class); + + private final AtomicReference state = + new AtomicReference<>(State.UNSUBSCRIBED); + + private Subscriber subscriber; + + private volatile boolean publisherCompleted; + + private volatile Throwable publisherError; + + @Override + public final void subscribe(Subscriber subscriber) { + if (logger.isTraceEnabled()) { + logger.trace(this.state + " subscribe: " + subscriber); + } + this.state.get().subscribe(this, subscriber); + } + + private boolean changeState(State oldState, State newState) { + return this.state.compareAndSet(oldState, newState); + } + + /** + * Publishes the complete signal to the subscriber of this publisher. + */ + public void publishComplete() { + if (logger.isTraceEnabled()) { + logger.trace(this.state + " publishComplete"); + } + this.state.get().publishComplete(this); + } + + /** + * Publishes the given error signal to the subscriber of this publisher. + */ + public void publishError(Throwable t) { + if (logger.isTraceEnabled()) { + logger.trace(this.state + " publishError: " + t); + } + this.state.get().publishError(this, t); + } + + private static final class ResponseBodyWriteResultSubscription + implements Subscription { + + private final ResponseBodyWriteResultPublisher publisher; + + public ResponseBodyWriteResultSubscription( + ResponseBodyWriteResultPublisher publisher) { + this.publisher = publisher; + } + + @Override + public final void request(long n) { + if (logger.isTraceEnabled()) { + logger.trace(state() + " request: " + n); + } + state().request(this.publisher, n); + } + + @Override + public final void cancel() { + if (logger.isTraceEnabled()) { + logger.trace(state() + " cancel"); + } + state().cancel(this.publisher); + } + + private State state() { + return this.publisher.state.get(); + } + + } + + private enum State { + UNSUBSCRIBED { + @Override + void subscribe(ResponseBodyWriteResultPublisher publisher, + Subscriber subscriber) { + Objects.requireNonNull(subscriber); + if (publisher.changeState(this, SUBSCRIBED)) { + Subscription subscription = + new ResponseBodyWriteResultSubscription(publisher); + publisher.subscriber = subscriber; + subscriber.onSubscribe(subscription); + if (publisher.publisherCompleted) { + publisher.publishComplete(); + } + else if (publisher.publisherError != null) { + publisher.publishError(publisher.publisherError); + } + } + else { + throw new IllegalStateException(toString()); + } + } + + @Override + void publishComplete(ResponseBodyWriteResultPublisher publisher) { + publisher.publisherCompleted = true; + } + + @Override + void publishError(ResponseBodyWriteResultPublisher publisher, Throwable t) { + publisher.publisherError = t; + } + }, + SUBSCRIBED { + @Override + void request(ResponseBodyWriteResultPublisher publisher, long n) { + BackpressureUtils.checkRequest(n, publisher.subscriber); + } + + @Override + void publishComplete(ResponseBodyWriteResultPublisher publisher) { + if (publisher.changeState(this, COMPLETED)) { + publisher.subscriber.onComplete(); + } + } + + @Override + void publishError(ResponseBodyWriteResultPublisher publisher, Throwable t) { + if (publisher.changeState(this, COMPLETED)) { + publisher.subscriber.onError(t); + } + } + + }, + COMPLETED { + @Override + void request(ResponseBodyWriteResultPublisher publisher, long n) { + // ignore + } + + @Override + void cancel(ResponseBodyWriteResultPublisher publisher) { + // ignore + } + + @Override + void publishComplete(ResponseBodyWriteResultPublisher publisher) { + // ignore + } + + @Override + void publishError(ResponseBodyWriteResultPublisher publisher, Throwable t) { + // ignore + } + }; + + void subscribe(ResponseBodyWriteResultPublisher publisher, + Subscriber subscriber) { + throw new IllegalStateException(toString()); + } + + void request(ResponseBodyWriteResultPublisher publisher, long n) { + throw new IllegalStateException(toString()); + } + + void cancel(ResponseBodyWriteResultPublisher publisher) { + publisher.changeState(this, COMPLETED); + } + + void publishComplete(ResponseBodyWriteResultPublisher publisher) { + throw new IllegalStateException(toString()); + } + + void publishError(ResponseBodyWriteResultPublisher publisher, Throwable t) { + throw new IllegalStateException(toString()); + } + + } + + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java index c8b03c4c50..e95f39afe3 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java @@ -248,14 +248,13 @@ public class ServletHttpHandlerAdapter extends HttpServlet { } @Override - protected void checkOnWritePossible() { + protected boolean isWritePossible() { try { - if (outputStream().isReady()) { - onWritePossible(); - } + return outputStream().isReady(); } catch (IOException ex) { onError(ex); + return false; } } @@ -307,11 +306,6 @@ public class ServletHttpHandlerAdapter extends HttpServlet { } - @Override - protected void close() { - this.synchronizer.writeComplete(); - } - private int writeDataBuffer(DataBuffer dataBuffer) throws IOException { InputStream input = dataBuffer.asInputStream(); ServletOutputStream output = outputStream(); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java index 0910723ea8..52f77cbcd3 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java @@ -30,8 +30,6 @@ import io.undertow.server.handlers.CookieImpl; import io.undertow.util.HttpString; import org.reactivestreams.Publisher; import org.xnio.ChannelListener; -import org.xnio.ChannelListeners; -import org.xnio.IoUtils; import org.xnio.channels.StreamSinkChannel; import reactor.core.publisher.Mono; @@ -206,23 +204,6 @@ public class UndertowServerHttpResponse extends AbstractServerHttpResponse this.byteBuffer = null; } - @Override - protected void close() { - try { - this.responseChannel.shutdownWrites(); - - if (!this.responseChannel.flush()) { - this.responseChannel.getWriteSetter().set(ChannelListeners - .flushingChannelListener( - o -> IoUtils.safeClose(this.responseChannel), - ChannelListeners.closingChannelExceptionHandler())); - this.responseChannel.resumeWrites(); - } - } - catch (IOException ignored) { - } - } - private class WriteListener implements ChannelListener { @Override From e64907eed8b4accdda53c6be0145087017eae820 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Tue, 12 Jul 2016 15:30:16 +0200 Subject: [PATCH 494/498] Removed ServletAsyncContextSynchronizer --- .../AbstractRequestBodyPublisher.java | 11 +- .../ServletAsyncContextSynchronizer.java | 105 ------------------ .../reactive/ServletHttpHandlerAdapter.java | 98 ++++++---------- .../reactive/UndertowServerHttpRequest.java | 11 -- .../AsyncContextSynchronizerTests.java | 57 ---------- 5 files changed, 33 insertions(+), 249 deletions(-) delete mode 100644 spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletAsyncContextSynchronizer.java delete mode 100644 spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncContextSynchronizerTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractRequestBodyPublisher.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractRequestBodyPublisher.java index 2e0a7359c0..19307e24cf 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractRequestBodyPublisher.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/AbstractRequestBodyPublisher.java @@ -124,11 +124,6 @@ abstract class AbstractRequestBodyPublisher implements Publisher { */ protected abstract DataBuffer read() throws IOException; - /** - * Closes the input. - */ - protected abstract void close(); - private boolean hasDemand() { return this.demand.get() > 0; } @@ -294,9 +289,7 @@ abstract class AbstractRequestBodyPublisher implements Publisher { } void cancel(AbstractRequestBodyPublisher publisher) { - if (publisher.changeState(this, COMPLETED)) { - publisher.close(); - } + publisher.changeState(this, COMPLETED); } void onDataAvailable(AbstractRequestBodyPublisher publisher) { @@ -305,7 +298,6 @@ abstract class AbstractRequestBodyPublisher implements Publisher { void onAllDataRead(AbstractRequestBodyPublisher publisher) { if (publisher.changeState(this, COMPLETED)) { - publisher.close(); if (publisher.subscriber != null) { publisher.subscriber.onComplete(); } @@ -314,7 +306,6 @@ abstract class AbstractRequestBodyPublisher implements Publisher { void onError(AbstractRequestBodyPublisher publisher, Throwable t) { if (publisher.changeState(this, COMPLETED)) { - publisher.close(); if (publisher.subscriber != null) { publisher.subscriber.onError(t); } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletAsyncContextSynchronizer.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletAsyncContextSynchronizer.java deleted file mode 100644 index ce507c9729..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletAsyncContextSynchronizer.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * 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.http.server.reactive; - -import java.util.concurrent.atomic.AtomicInteger; -import javax.servlet.AsyncContext; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; - -/** - * Utility class for synchronizing between the reading and writing side of an - * {@link AsyncContext}. This class will simply call {@link AsyncContext#complete()} when - * both {@link #readComplete()} and {@link #writeComplete()} have been called. - * - * @author Arjen Poutsma - * @see AsyncContext - */ -final class ServletAsyncContextSynchronizer { - - private static final int NONE_COMPLETE = 0; - - private static final int READ_COMPLETE = 1; - - private static final int WRITE_COMPLETE = 1 << 1; - - private static final int COMPLETE = READ_COMPLETE | WRITE_COMPLETE; - - - private final AsyncContext asyncContext; - - private final AtomicInteger complete = new AtomicInteger(NONE_COMPLETE); - - - /** - * Creates a new {@code AsyncContextSynchronizer} based on the given context. - * @param asyncContext the context to base this synchronizer on - */ - public ServletAsyncContextSynchronizer(AsyncContext asyncContext) { - this.asyncContext = asyncContext; - } - - /** - * Returns the request of this synchronizer. - */ - public ServletRequest getRequest() { - return this.asyncContext.getRequest(); - } - - /** - * Returns the response of this synchronizer. - */ - public ServletResponse getResponse() { - return this.asyncContext.getResponse(); - } - - /** - * Completes the reading side of the asynchronous operation. When both this method and - * {@link #writeComplete()} have been called, the {@code AsyncContext} will be - * {@linkplain AsyncContext#complete() fully completed}. - */ - public void readComplete() { - if (complete.compareAndSet(WRITE_COMPLETE, COMPLETE)) { - this.asyncContext.complete(); - } - else { - this.complete.compareAndSet(NONE_COMPLETE, READ_COMPLETE); - } - } - - /** - * Completes the writing side of the asynchronous operation. When both this method and - * {@link #readComplete()} have been called, the {@code AsyncContext} will be - * {@linkplain AsyncContext#complete() fully completed}. - */ - public void writeComplete() { - if (complete.compareAndSet(READ_COMPLETE, COMPLETE)) { - this.asyncContext.complete(); - } - else { - this.complete.compareAndSet(NONE_COMPLETE, WRITE_COMPLETE); - } - } - - /** - * Completes both the reading and writing side of the asynchronous operation. - */ - public void complete() { - readComplete(); - writeComplete(); - } -} diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java index e95f39afe3..ce6e49a7b5 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java @@ -51,7 +51,6 @@ public class ServletHttpHandlerAdapter extends HttpServlet { private static Log logger = LogFactory.getLog(ServletHttpHandlerAdapter.class); - private HttpHandler handler; // Servlet is based on blocking I/O, hence the usage of non-direct, heap-based buffers @@ -60,7 +59,6 @@ public class ServletHttpHandlerAdapter extends HttpServlet { private int bufferSize = DEFAULT_BUFFER_SIZE; - public void setHandler(HttpHandler handler) { Assert.notNull(handler, "'handler' must not be null"); this.handler = handler; @@ -77,21 +75,21 @@ public class ServletHttpHandlerAdapter extends HttpServlet { } @Override - protected void service(HttpServletRequest servletRequest, HttpServletResponse servletResponse) - throws ServletException, IOException { + protected void service(HttpServletRequest servletRequest, + HttpServletResponse servletResponse) throws ServletException, IOException { - AsyncContext context = servletRequest.startAsync(); - ServletAsyncContextSynchronizer synchronizer = new ServletAsyncContextSynchronizer(context); + AsyncContext asyncContext = servletRequest.startAsync(); RequestBodyPublisher requestBody = - new RequestBodyPublisher(synchronizer, this.dataBufferFactory, - this.bufferSize); + new RequestBodyPublisher(servletRequest.getInputStream(), + this.dataBufferFactory, this.bufferSize); requestBody.registerListener(); ServletServerHttpRequest request = new ServletServerHttpRequest(servletRequest, requestBody); ResponseBodyProcessor responseBody = - new ResponseBodyProcessor(synchronizer, this.bufferSize); + new ResponseBodyProcessor(servletResponse.getOutputStream(), + this.bufferSize); responseBody.registerListener(); ServletServerHttpResponse response = new ServletServerHttpResponse(servletResponse, this.dataBufferFactory, @@ -101,20 +99,19 @@ public class ServletHttpHandlerAdapter extends HttpServlet { })); HandlerResultSubscriber resultSubscriber = - new HandlerResultSubscriber(synchronizer); + new HandlerResultSubscriber(asyncContext); this.handler.handle(request, response).subscribe(resultSubscriber); } private static class HandlerResultSubscriber implements Subscriber { - private final ServletAsyncContextSynchronizer synchronizer; + private final AsyncContext asyncContext; - public HandlerResultSubscriber(ServletAsyncContextSynchronizer synchronizer) { - this.synchronizer = synchronizer; + public HandlerResultSubscriber(AsyncContext asyncContext) { + this.asyncContext = asyncContext; } - @Override public void onSubscribe(Subscription subscription) { subscription.request(Long.MAX_VALUE); @@ -129,14 +126,14 @@ public class ServletHttpHandlerAdapter extends HttpServlet { public void onError(Throwable ex) { logger.error("Error from request handling. Completing the request.", ex); HttpServletResponse response = - (HttpServletResponse) this.synchronizer.getResponse(); + (HttpServletResponse) this.asyncContext.getResponse(); response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); - this.synchronizer.complete(); + this.asyncContext.complete(); } @Override public void onComplete() { - this.synchronizer.complete(); + this.asyncContext.complete(); } } @@ -145,44 +142,34 @@ public class ServletHttpHandlerAdapter extends HttpServlet { private final RequestBodyPublisher.RequestBodyReadListener readListener = new RequestBodyPublisher.RequestBodyReadListener(); - private final ServletAsyncContextSynchronizer synchronizer; + private final ServletInputStream inputStream; private final DataBufferFactory dataBufferFactory; private final byte[] buffer; - public RequestBodyPublisher(ServletAsyncContextSynchronizer synchronizer, + public RequestBodyPublisher(ServletInputStream inputStream, DataBufferFactory dataBufferFactory, int bufferSize) { - this.synchronizer = synchronizer; + this.inputStream = inputStream; this.dataBufferFactory = dataBufferFactory; this.buffer = new byte[bufferSize]; } public void registerListener() throws IOException { - inputStream().setReadListener(this.readListener); - } - - private ServletInputStream inputStream() throws IOException { - return this.synchronizer.getRequest().getInputStream(); + inputStream.setReadListener(this.readListener); } @Override protected void checkOnDataAvailable() { - try { - if (!inputStream().isFinished() && inputStream().isReady()) { - onDataAvailable(); - } - } - catch (IOException ex) { - onError(ex); + if (!inputStream.isFinished() && inputStream.isReady()) { + onDataAvailable(); } } @Override protected DataBuffer read() throws IOException { - ServletInputStream input = inputStream(); - if (input.isReady()) { - int read = input.read(this.buffer); + if (inputStream.isReady()) { + int read = inputStream.read(this.buffer); if (logger.isTraceEnabled()) { logger.trace("read:" + read); } @@ -196,12 +183,6 @@ public class ServletHttpHandlerAdapter extends HttpServlet { return null; } - @Override - protected void close() { - this.synchronizer.readComplete(); - - } - private class RequestBodyReadListener implements ReadListener { @Override @@ -227,46 +208,33 @@ public class ServletHttpHandlerAdapter extends HttpServlet { private final ResponseBodyWriteListener writeListener = new ResponseBodyWriteListener(); - private final ServletAsyncContextSynchronizer synchronizer; + private final ServletOutputStream outputStream; private final int bufferSize; private volatile boolean flushOnNext; - public ResponseBodyProcessor(ServletAsyncContextSynchronizer synchronizer, - int bufferSize) { - this.synchronizer = synchronizer; + public ResponseBodyProcessor(ServletOutputStream outputStream, int bufferSize) { + this.outputStream = outputStream; this.bufferSize = bufferSize; } public void registerListener() throws IOException { - outputStream().setWriteListener(this.writeListener); - } - - private ServletOutputStream outputStream() throws IOException { - return this.synchronizer.getResponse().getOutputStream(); + outputStream.setWriteListener(this.writeListener); } @Override protected boolean isWritePossible() { - try { - return outputStream().isReady(); - } - catch (IOException ex) { - onError(ex); - return false; - } + return outputStream.isReady(); } @Override protected boolean write(DataBuffer dataBuffer) throws IOException { - ServletOutputStream output = outputStream(); - if (this.flushOnNext) { flush(); } - boolean ready = output.isReady(); + boolean ready = outputStream.isReady(); if (this.logger.isTraceEnabled()) { this.logger.trace("write: " + dataBuffer + " ready: " + ready); @@ -288,13 +256,12 @@ public class ServletHttpHandlerAdapter extends HttpServlet { @Override protected void flush() throws IOException { - ServletOutputStream output = outputStream(); - if (output.isReady()) { + if (outputStream.isReady()) { if (logger.isTraceEnabled()) { this.logger.trace("flush"); } try { - output.flush(); + outputStream.flush(); this.flushOnNext = false; } catch (IOException ignored) { @@ -308,14 +275,13 @@ public class ServletHttpHandlerAdapter extends HttpServlet { private int writeDataBuffer(DataBuffer dataBuffer) throws IOException { InputStream input = dataBuffer.asInputStream(); - ServletOutputStream output = outputStream(); int bytesWritten = 0; byte[] buffer = new byte[this.bufferSize]; int bytesRead = -1; - while (output.isReady() && (bytesRead = input.read(buffer)) != -1) { - output.write(buffer, 0, bytesRead); + while (outputStream.isReady() && (bytesRead = input.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); bytesWritten += bytesRead; } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java index 1597a3af2c..1649a25145 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java @@ -26,7 +26,6 @@ import io.undertow.server.HttpServerExchange; import io.undertow.server.handlers.Cookie; import io.undertow.util.HeaderValues; import org.xnio.ChannelListener; -import org.xnio.IoUtils; import org.xnio.channels.StreamSourceChannel; import reactor.core.publisher.Flux; @@ -152,16 +151,6 @@ public class UndertowServerHttpRequest extends AbstractServerHttpRequest { return null; } - @Override - protected void close() { - if (this.pooledByteBuffer != null) { - IoUtils.safeClose(this.pooledByteBuffer); - } - if (this.requestChannel != null) { - IoUtils.safeClose(this.requestChannel); - } - } - private class ReadListener implements ChannelListener { @Override diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncContextSynchronizerTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncContextSynchronizerTests.java deleted file mode 100644 index 158c7e022e..0000000000 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AsyncContextSynchronizerTests.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2002-2015 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.server.reactive; - -import javax.servlet.AsyncContext; - -import org.junit.Before; -import org.junit.Test; - -import static org.mockito.BDDMockito.mock; -import static org.mockito.BDDMockito.verify; - -/** - * @author Arjen Poutsma - */ -public class AsyncContextSynchronizerTests { - - private AsyncContext asyncContext; - - private ServletAsyncContextSynchronizer synchronizer; - - @Before - public void setUp() throws Exception { - asyncContext = mock(AsyncContext.class); - synchronizer = new ServletAsyncContextSynchronizer(asyncContext); - } - - @Test - public void readThenWrite() { - synchronizer.readComplete(); - synchronizer.writeComplete(); - - verify(asyncContext).complete(); - } - - @Test - public void writeThenRead() { - synchronizer.writeComplete(); - synchronizer.readComplete(); - - verify(asyncContext).complete(); - } -} \ No newline at end of file From 392a1660a4fcc1e2eab734a21beda71f9fd8ba6b Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Tue, 12 Jul 2016 15:39:17 +0200 Subject: [PATCH 495/498] Polishing --- .../reactive/ServletHttpHandlerAdapter.java | 30 +++++++++---------- .../reactive/UndertowServerHttpResponse.java | 3 -- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java index ce6e49a7b5..8726036ab2 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java @@ -156,20 +156,20 @@ public class ServletHttpHandlerAdapter extends HttpServlet { } public void registerListener() throws IOException { - inputStream.setReadListener(this.readListener); + this.inputStream.setReadListener(this.readListener); } @Override protected void checkOnDataAvailable() { - if (!inputStream.isFinished() && inputStream.isReady()) { + if (!this.inputStream.isFinished() && this.inputStream.isReady()) { onDataAvailable(); } } @Override protected DataBuffer read() throws IOException { - if (inputStream.isReady()) { - int read = inputStream.read(this.buffer); + if (this.inputStream.isReady()) { + int read = this.inputStream.read(this.buffer); if (logger.isTraceEnabled()) { logger.trace("read:" + read); } @@ -220,12 +220,12 @@ public class ServletHttpHandlerAdapter extends HttpServlet { } public void registerListener() throws IOException { - outputStream.setWriteListener(this.writeListener); + this.outputStream.setWriteListener(this.writeListener); } @Override protected boolean isWritePossible() { - return outputStream.isReady(); + return this.outputStream.isReady(); } @Override @@ -234,7 +234,7 @@ public class ServletHttpHandlerAdapter extends HttpServlet { flush(); } - boolean ready = outputStream.isReady(); + boolean ready = this.outputStream.isReady(); if (this.logger.isTraceEnabled()) { this.logger.trace("write: " + dataBuffer + " ready: " + ready); @@ -256,20 +256,19 @@ public class ServletHttpHandlerAdapter extends HttpServlet { @Override protected void flush() throws IOException { - if (outputStream.isReady()) { + if (this.outputStream.isReady()) { if (logger.isTraceEnabled()) { - this.logger.trace("flush"); + logger.trace("flush"); } try { - outputStream.flush(); + this.outputStream.flush(); this.flushOnNext = false; + return; } catch (IOException ignored) { } } - else { - this.flushOnNext = true; - } + this.flushOnNext = true; } @@ -280,8 +279,9 @@ public class ServletHttpHandlerAdapter extends HttpServlet { byte[] buffer = new byte[this.bufferSize]; int bytesRead = -1; - while (outputStream.isReady() && (bytesRead = input.read(buffer)) != -1) { - outputStream.write(buffer, 0, bytesRead); + while (this.outputStream.isReady() && + (bytesRead = input.read(buffer)) != -1) { + this.outputStream.write(buffer, 0, bytesRead); bytesWritten += bytesRead; } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java index 52f77cbcd3..fd105378a0 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java @@ -140,14 +140,11 @@ public class UndertowServerHttpResponse extends AbstractServerHttpResponse private final ChannelListener listener = new WriteListener(); - private final HttpServerExchange exchange; - private final StreamSinkChannel responseChannel; private volatile ByteBuffer byteBuffer; public ResponseBodyProcessor(HttpServerExchange exchange) { - this.exchange = exchange; this.responseChannel = exchange.getResponseChannel(); } From f82f17d7f762bb0fd10cd813c334ac9a0b62355e Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Tue, 12 Jul 2016 17:20:39 +0200 Subject: [PATCH 496/498] Moved Servlet Publisher and Processor In preparation of supporting both Publisher and Publisher> as response body, moved RequestBodyPublisher and ResponseBodyProcessor into ServletServerHttpRequest and ServletServerHttpResponse respectively. --- .../reactive/ServletHttpHandlerAdapter.java | 193 +----------------- .../reactive/ServletServerHttpRequest.java | 98 ++++++++- .../reactive/ServletServerHttpResponse.java | 128 +++++++++++- .../reactive/ServerHttpRequestTests.java | 9 +- 4 files changed, 214 insertions(+), 214 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java index 8726036ab2..ea64176d0a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java @@ -17,13 +17,8 @@ package org.springframework.http.server.reactive; import java.io.IOException; -import java.io.InputStream; import javax.servlet.AsyncContext; -import javax.servlet.ReadListener; import javax.servlet.ServletException; -import javax.servlet.ServletInputStream; -import javax.servlet.ServletOutputStream; -import javax.servlet.WriteListener; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; @@ -33,9 +28,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; -import reactor.core.publisher.Mono; -import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.util.Assert; @@ -80,23 +73,15 @@ public class ServletHttpHandlerAdapter extends HttpServlet { AsyncContext asyncContext = servletRequest.startAsync(); - RequestBodyPublisher requestBody = - new RequestBodyPublisher(servletRequest.getInputStream(), - this.dataBufferFactory, this.bufferSize); - requestBody.registerListener(); ServletServerHttpRequest request = - new ServletServerHttpRequest(servletRequest, requestBody); - - ResponseBodyProcessor responseBody = - new ResponseBodyProcessor(servletResponse.getOutputStream(), + new ServletServerHttpRequest(servletRequest, this.dataBufferFactory, this.bufferSize); - responseBody.registerListener(); + request.registerListener(); + ServletServerHttpResponse response = new ServletServerHttpResponse(servletResponse, this.dataBufferFactory, - publisher -> Mono.from(subscriber -> { - publisher.subscribe(responseBody); - responseBody.subscribe(subscriber); - })); + this.bufferSize); + response.registerListener(); HandlerResultSubscriber resultSubscriber = new HandlerResultSubscriber(asyncContext); @@ -137,173 +122,5 @@ public class ServletHttpHandlerAdapter extends HttpServlet { } } - private static class RequestBodyPublisher extends AbstractRequestBodyPublisher { - - private final RequestBodyPublisher.RequestBodyReadListener readListener = - new RequestBodyPublisher.RequestBodyReadListener(); - - private final ServletInputStream inputStream; - - private final DataBufferFactory dataBufferFactory; - - private final byte[] buffer; - - public RequestBodyPublisher(ServletInputStream inputStream, - DataBufferFactory dataBufferFactory, int bufferSize) { - this.inputStream = inputStream; - this.dataBufferFactory = dataBufferFactory; - this.buffer = new byte[bufferSize]; - } - - public void registerListener() throws IOException { - this.inputStream.setReadListener(this.readListener); - } - - @Override - protected void checkOnDataAvailable() { - if (!this.inputStream.isFinished() && this.inputStream.isReady()) { - onDataAvailable(); - } - } - - @Override - protected DataBuffer read() throws IOException { - if (this.inputStream.isReady()) { - int read = this.inputStream.read(this.buffer); - if (logger.isTraceEnabled()) { - logger.trace("read:" + read); - } - - if (read > 0) { - DataBuffer dataBuffer = this.dataBufferFactory.allocateBuffer(read); - dataBuffer.write(this.buffer, 0, read); - return dataBuffer; - } - } - return null; - } - - private class RequestBodyReadListener implements ReadListener { - - @Override - public void onDataAvailable() throws IOException { - RequestBodyPublisher.this.onDataAvailable(); - } - - @Override - public void onAllDataRead() throws IOException { - RequestBodyPublisher.this.onAllDataRead(); - } - - @Override - public void onError(Throwable throwable) { - RequestBodyPublisher.this.onError(throwable); - - } - } - } - - private static class ResponseBodyProcessor extends AbstractResponseBodyProcessor { - - private final ResponseBodyWriteListener writeListener = - new ResponseBodyWriteListener(); - - private final ServletOutputStream outputStream; - - private final int bufferSize; - - private volatile boolean flushOnNext; - - public ResponseBodyProcessor(ServletOutputStream outputStream, int bufferSize) { - this.outputStream = outputStream; - this.bufferSize = bufferSize; - } - - public void registerListener() throws IOException { - this.outputStream.setWriteListener(this.writeListener); - } - - @Override - protected boolean isWritePossible() { - return this.outputStream.isReady(); - } - - @Override - protected boolean write(DataBuffer dataBuffer) throws IOException { - if (this.flushOnNext) { - flush(); - } - - boolean ready = this.outputStream.isReady(); - - if (this.logger.isTraceEnabled()) { - this.logger.trace("write: " + dataBuffer + " ready: " + ready); - } - - if (ready) { - int total = dataBuffer.readableByteCount(); - int written = writeDataBuffer(dataBuffer); - - if (this.logger.isTraceEnabled()) { - this.logger.trace("written: " + written + " total: " + total); - } - return written == total; - } - else { - return false; - } - } - - @Override - protected void flush() throws IOException { - if (this.outputStream.isReady()) { - if (logger.isTraceEnabled()) { - logger.trace("flush"); - } - try { - this.outputStream.flush(); - this.flushOnNext = false; - return; - } - catch (IOException ignored) { - } - } - this.flushOnNext = true; - - } - - private int writeDataBuffer(DataBuffer dataBuffer) throws IOException { - InputStream input = dataBuffer.asInputStream(); - - int bytesWritten = 0; - byte[] buffer = new byte[this.bufferSize]; - int bytesRead = -1; - - while (this.outputStream.isReady() && - (bytesRead = input.read(buffer)) != -1) { - this.outputStream.write(buffer, 0, bytesRead); - bytesWritten += bytesRead; - } - - return bytesWritten; - } - - private class ResponseBodyWriteListener implements WriteListener { - - @Override - public void onWritePossible() throws IOException { - ResponseBodyProcessor.this.onWritePossible(); - } - - @Override - public void onError(Throwable ex) { - // Error on writing to the HTTP stream, so any further writes will probably - // fail. Let's log instead of calling {@link #writeError}. - ResponseBodyProcessor.this.logger - .error("ResponseBodyWriteListener error", ex); - } - } - } - } \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java index 568942bde9..3027dea310 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java @@ -16,18 +16,21 @@ package org.springframework.http.server.reactive; +import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.Charset; import java.util.Enumeration; import java.util.Map; +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; -import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -40,24 +43,24 @@ import org.springframework.util.StringUtils; /** * Adapt {@link ServerHttpRequest} to the Servlet {@link HttpServletRequest}. - * * @author Rossen Stoyanchev */ public class ServletServerHttpRequest extends AbstractServerHttpRequest { private final HttpServletRequest request; - private final Flux requestBodyPublisher; + private final RequestBodyPublisher bodyPublisher; public ServletServerHttpRequest(HttpServletRequest request, - Publisher body) { + DataBufferFactory dataBufferFactory, int bufferSize) throws IOException { Assert.notNull(request, "'request' must not be null."); - Assert.notNull(body, "'body' must not be null."); + Assert.notNull(dataBufferFactory, "'dataBufferFactory' must not be null"); this.request = request; - this.requestBodyPublisher = Flux.from(body); + this.bodyPublisher = + new RequestBodyPublisher(request.getInputStream(), dataBufferFactory, + bufferSize); } - public HttpServletRequest getServletRequest() { return this.request; } @@ -80,9 +83,11 @@ public class ServletServerHttpRequest extends AbstractServerHttpRequest { @Override protected HttpHeaders initHeaders() { HttpHeaders headers = new HttpHeaders(); - for (Enumeration names = getServletRequest().getHeaderNames(); names.hasMoreElements(); ) { + for (Enumeration names = getServletRequest().getHeaderNames(); + names.hasMoreElements(); ) { String name = (String) names.nextElement(); - for (Enumeration values = getServletRequest().getHeaders(name); values.hasMoreElements(); ) { + for (Enumeration values = getServletRequest().getHeaders(name); + values.hasMoreElements(); ) { headers.add(name, (String) values.nextElement()); } } @@ -101,7 +106,9 @@ public class ServletServerHttpRequest extends AbstractServerHttpRequest { Map params = new LinkedCaseInsensitiveMap<>(); params.putAll(contentType.getParameters()); params.put("charset", charset.toString()); - headers.setContentType(new MediaType(contentType.getType(), contentType.getSubtype(), params)); + headers.setContentType( + new MediaType(contentType.getType(), contentType.getSubtype(), + params)); } } if (headers.getContentLength() == -1) { @@ -129,7 +136,76 @@ public class ServletServerHttpRequest extends AbstractServerHttpRequest { @Override public Flux getBody() { - return this.requestBodyPublisher; + return Flux.from(this.bodyPublisher); } + public void registerListener() throws IOException { + this.bodyPublisher.registerListener(); + } + + private static class RequestBodyPublisher extends AbstractRequestBodyPublisher { + + private final RequestBodyPublisher.RequestBodyReadListener readListener = + new RequestBodyPublisher.RequestBodyReadListener(); + + private final ServletInputStream inputStream; + + private final DataBufferFactory dataBufferFactory; + + private final byte[] buffer; + + public RequestBodyPublisher(ServletInputStream inputStream, + DataBufferFactory dataBufferFactory, int bufferSize) { + this.inputStream = inputStream; + this.dataBufferFactory = dataBufferFactory; + this.buffer = new byte[bufferSize]; + } + + public void registerListener() throws IOException { + this.inputStream.setReadListener(this.readListener); + } + + @Override + protected void checkOnDataAvailable() { + if (!this.inputStream.isFinished() && this.inputStream.isReady()) { + onDataAvailable(); + } + } + + @Override + protected DataBuffer read() throws IOException { + if (this.inputStream.isReady()) { + int read = this.inputStream.read(this.buffer); + if (logger.isTraceEnabled()) { + logger.trace("read:" + read); + } + + if (read > 0) { + DataBuffer dataBuffer = this.dataBufferFactory.allocateBuffer(read); + dataBuffer.write(this.buffer, 0, read); + return dataBuffer; + } + } + return null; + } + + private class RequestBodyReadListener implements ReadListener { + + @Override + public void onDataAvailable() throws IOException { + RequestBodyPublisher.this.onDataAvailable(); + } + + @Override + public void onAllDataRead() throws IOException { + RequestBodyPublisher.this.onAllDataRead(); + } + + @Override + public void onError(Throwable throwable) { + RequestBodyPublisher.this.onError(throwable); + + } + } + } } diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java index 7cdad1512a..a54f2b0808 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java @@ -16,10 +16,13 @@ package org.springframework.http.server.reactive; +import java.io.IOException; +import java.io.InputStream; import java.nio.charset.Charset; import java.util.List; import java.util.Map; -import java.util.function.Function; +import javax.servlet.ServletOutputStream; +import javax.servlet.WriteListener; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletResponse; @@ -35,32 +38,27 @@ import org.springframework.util.Assert; /** * Adapt {@link ServerHttpResponse} to the Servlet {@link HttpServletResponse}. - * * @author Rossen Stoyanchev */ public class ServletServerHttpResponse extends AbstractServerHttpResponse { private final HttpServletResponse response; - private final Function, Mono> responseBodyWriter; - + private ResponseBodyProcessor bodyProcessor; public ServletServerHttpResponse(HttpServletResponse response, - DataBufferFactory dataBufferFactory, - Function, Mono> responseBodyWriter) { + DataBufferFactory dataBufferFactory, int bufferSize) throws IOException { super(dataBufferFactory); Assert.notNull(response, "'response' must not be null"); - Assert.notNull(responseBodyWriter, "'responseBodyWriter' must not be null"); this.response = response; - this.responseBodyWriter = responseBodyWriter; + this.bodyProcessor = + new ResponseBodyProcessor(response.getOutputStream(), bufferSize); } - public HttpServletResponse getServletResponse() { return this.response; } - @Override protected void writeStatusCode() { HttpStatus statusCode = this.getStatusCode(); @@ -71,7 +69,10 @@ public class ServletServerHttpResponse extends AbstractServerHttpResponse { @Override protected Mono writeWithInternal(Publisher publisher) { - return this.responseBodyWriter.apply(publisher); + return Mono.from(subscriber -> { + publisher.subscribe(this.bodyProcessor); + this.bodyProcessor.subscribe(subscriber); + }); } @Override @@ -109,4 +110,109 @@ public class ServletServerHttpResponse extends AbstractServerHttpResponse { } } + public void registerListener() throws IOException { + this.bodyProcessor.registerListener(); + } + + private static class ResponseBodyProcessor extends AbstractResponseBodyProcessor { + + private final ResponseBodyWriteListener writeListener = + new ResponseBodyWriteListener(); + + private final ServletOutputStream outputStream; + + private final int bufferSize; + + private volatile boolean flushOnNext; + + public ResponseBodyProcessor(ServletOutputStream outputStream, int bufferSize) { + this.outputStream = outputStream; + this.bufferSize = bufferSize; + } + + public void registerListener() throws IOException { + this.outputStream.setWriteListener(this.writeListener); + } + + @Override + protected boolean isWritePossible() { + return this.outputStream.isReady(); + } + + @Override + protected boolean write(DataBuffer dataBuffer) throws IOException { + if (this.flushOnNext) { + flush(); + } + + boolean ready = this.outputStream.isReady(); + + if (this.logger.isTraceEnabled()) { + this.logger.trace("write: " + dataBuffer + " ready: " + ready); + } + + if (ready) { + int total = dataBuffer.readableByteCount(); + int written = writeDataBuffer(dataBuffer); + + if (this.logger.isTraceEnabled()) { + this.logger.trace("written: " + written + " total: " + total); + } + return written == total; + } + else { + return false; + } + } + + @Override + protected void flush() throws IOException { + if (this.outputStream.isReady()) { + if (logger.isTraceEnabled()) { + logger.trace("flush"); + } + try { + this.outputStream.flush(); + this.flushOnNext = false; + return; + } + catch (IOException ignored) { + } + } + this.flushOnNext = true; + + } + + private int writeDataBuffer(DataBuffer dataBuffer) throws IOException { + InputStream input = dataBuffer.asInputStream(); + + int bytesWritten = 0; + byte[] buffer = new byte[this.bufferSize]; + int bytesRead = -1; + + while (this.outputStream.isReady() && + (bytesRead = input.read(buffer)) != -1) { + this.outputStream.write(buffer, 0, bytesRead); + bytesWritten += bytesRead; + } + + return bytesWritten; + } + + private class ResponseBodyWriteListener implements WriteListener { + + @Override + public void onWritePossible() throws IOException { + ResponseBodyProcessor.this.onWritePossible(); + } + + @Override + public void onError(Throwable ex) { + // Error on writing to the HTTP stream, so any further writes will probably + // fail. Let's log instead of calling {@link #writeError}. + ResponseBodyProcessor.this.logger + .error("ResponseBodyWriteListener error", ex); + } + } + } } \ No newline at end of file diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ServerHttpRequestTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ServerHttpRequestTests.java index 6de90e0cff..b6743cd77e 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ServerHttpRequestTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/ServerHttpRequestTests.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.http.server.reactive; import java.util.Arrays; @@ -20,8 +21,8 @@ import java.util.Collections; import javax.servlet.http.HttpServletRequest; import org.junit.Test; -import reactor.core.publisher.Flux; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.util.MultiValueMap; @@ -70,10 +71,10 @@ public class ServerHttpRequestTests { assertEquals(Collections.singletonList(null), params.get("a")); } - - private ServerHttpRequest createHttpRequest(String path) { + private ServerHttpRequest createHttpRequest(String path) throws Exception { HttpServletRequest servletRequest = new MockHttpServletRequest("GET", path); - return new ServletServerHttpRequest(servletRequest, Flux.empty()); + return new ServletServerHttpRequest(servletRequest, + new DefaultDataBufferFactory(), 1024); } } From 7519d1de41d806074d49caf0eee155fccf41134b Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Wed, 13 Jul 2016 09:56:45 +0200 Subject: [PATCH 497/498] Moved log4j config to test tree Moved log4j.properties from the main source tree to the test tree. --- spring-web-reactive/src/{main => test}/resources/log4j.properties | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename spring-web-reactive/src/{main => test}/resources/log4j.properties (100%) diff --git a/spring-web-reactive/src/main/resources/log4j.properties b/spring-web-reactive/src/test/resources/log4j.properties similarity index 100% rename from spring-web-reactive/src/main/resources/log4j.properties rename to spring-web-reactive/src/test/resources/log4j.properties From 56e50d6d6811d4f921f7e79f4ae8ce0823bd6d25 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Wed, 13 Jul 2016 10:21:02 +0200 Subject: [PATCH 498/498] Moved body processor to writeWithInternal() Moved ResponseBodyProcessor creation from constructor to writeWithInternal(), in preparation of supporting both Publisher as well as Publisher>. --- .../reactive/ServletHttpHandlerAdapter.java | 2 - .../reactive/ServletServerHttpRequest.java | 41 +++++++++++++--- .../reactive/ServletServerHttpResponse.java | 48 +++++++++++++----- .../reactive/UndertowServerHttpResponse.java | 49 +++++++++++++------ 4 files changed, 103 insertions(+), 37 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java index ea64176d0a..e25a03cc5e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java @@ -76,12 +76,10 @@ public class ServletHttpHandlerAdapter extends HttpServlet { ServletServerHttpRequest request = new ServletServerHttpRequest(servletRequest, this.dataBufferFactory, this.bufferSize); - request.registerListener(); ServletServerHttpResponse response = new ServletServerHttpResponse(servletResponse, this.dataBufferFactory, this.bufferSize); - response.registerListener(); HandlerResultSubscriber resultSubscriber = new HandlerResultSubscriber(asyncContext); diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java index 3027dea310..c9c7a1d1b0 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java @@ -47,18 +47,25 @@ import org.springframework.util.StringUtils; */ public class ServletServerHttpRequest extends AbstractServerHttpRequest { + private final Object bodyPublisherMonitor = new Object(); + + private volatile RequestBodyPublisher bodyPublisher; + private final HttpServletRequest request; - private final RequestBodyPublisher bodyPublisher; + private final DataBufferFactory dataBufferFactory; + + private final int bufferSize; public ServletServerHttpRequest(HttpServletRequest request, - DataBufferFactory dataBufferFactory, int bufferSize) throws IOException { + DataBufferFactory dataBufferFactory, int bufferSize) { Assert.notNull(request, "'request' must not be null."); Assert.notNull(dataBufferFactory, "'dataBufferFactory' must not be null"); + Assert.isTrue(bufferSize > 0); + this.request = request; - this.bodyPublisher = - new RequestBodyPublisher(request.getInputStream(), dataBufferFactory, - bufferSize); + this.dataBufferFactory = dataBufferFactory; + this.bufferSize = bufferSize; } public HttpServletRequest getServletRequest() { @@ -136,11 +143,29 @@ public class ServletServerHttpRequest extends AbstractServerHttpRequest { @Override public Flux getBody() { - return Flux.from(this.bodyPublisher); + try { + RequestBodyPublisher bodyPublisher = this.bodyPublisher; + if (bodyPublisher == null) { + synchronized (this.bodyPublisherMonitor) { + bodyPublisher = this.bodyPublisher; + if (bodyPublisher == null) { + this.bodyPublisher = bodyPublisher = createBodyPublisher(); + } + } + } + return Flux.from(bodyPublisher); + } + catch (IOException ex) { + return Flux.error(ex); + } } - public void registerListener() throws IOException { - this.bodyPublisher.registerListener(); + private RequestBodyPublisher createBodyPublisher() throws IOException { + RequestBodyPublisher bodyPublisher = + new RequestBodyPublisher(request.getInputStream(), this.dataBufferFactory, + this.bufferSize); + bodyPublisher.registerListener(); + return bodyPublisher; } private static class RequestBodyPublisher extends AbstractRequestBodyPublisher { diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java index a54f2b0808..1535eccdbd 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java @@ -42,17 +42,23 @@ import org.springframework.util.Assert; */ public class ServletServerHttpResponse extends AbstractServerHttpResponse { + private final Object bodyProcessorMonitor = new Object(); + + private volatile ResponseBodyProcessor bodyProcessor; + private final HttpServletResponse response; - private ResponseBodyProcessor bodyProcessor; + private final int bufferSize; public ServletServerHttpResponse(HttpServletResponse response, DataBufferFactory dataBufferFactory, int bufferSize) throws IOException { super(dataBufferFactory); Assert.notNull(response, "'response' must not be null"); + Assert.notNull(dataBufferFactory, "'dataBufferFactory' must not be null"); + Assert.isTrue(bufferSize > 0); + this.response = response; - this.bodyProcessor = - new ResponseBodyProcessor(response.getOutputStream(), bufferSize); + this.bufferSize = bufferSize; } public HttpServletResponse getServletResponse() { @@ -69,10 +75,34 @@ public class ServletServerHttpResponse extends AbstractServerHttpResponse { @Override protected Mono writeWithInternal(Publisher publisher) { - return Mono.from(subscriber -> { - publisher.subscribe(this.bodyProcessor); - this.bodyProcessor.subscribe(subscriber); - }); + Assert.state(this.bodyProcessor == null, + "Response body publisher is already provided"); + try { + synchronized (this.bodyProcessorMonitor) { + if (this.bodyProcessor == null) { + this.bodyProcessor = createBodyProcessor(); + } + else { + throw new IllegalStateException( + "Response body publisher is already provided"); + } + } + return Mono.from(subscriber -> { + publisher.subscribe(this.bodyProcessor); + this.bodyProcessor.subscribe(subscriber); + }); + } + catch (IOException ex) { + return Mono.error(ex); + } + } + + private ResponseBodyProcessor createBodyProcessor() throws IOException { + ResponseBodyProcessor bodyProcessor = + new ResponseBodyProcessor(this.response.getOutputStream(), + this.bufferSize); + bodyProcessor.registerListener(); + return bodyProcessor; } @Override @@ -110,10 +140,6 @@ public class ServletServerHttpResponse extends AbstractServerHttpResponse { } } - public void registerListener() throws IOException { - this.bodyProcessor.registerListener(); - } - private static class ResponseBodyProcessor extends AbstractResponseBodyProcessor { private final ResponseBodyWriteListener writeListener = diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java index fd105378a0..8d04fc99bb 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java @@ -42,7 +42,6 @@ import org.springframework.util.Assert; /** * Adapt {@link ServerHttpResponse} to the Undertow {@link HttpServerExchange}. - * * @author Marek Hawrylczak * @author Rossen Stoyanchev * @author Arjen Poutsma @@ -50,8 +49,11 @@ import org.springframework.util.Assert; public class UndertowServerHttpResponse extends AbstractServerHttpResponse implements ZeroCopyHttpOutputMessage { - private final HttpServerExchange exchange; + private final Object bodyProcessorMonitor = new Object(); + private volatile ResponseBodyProcessor bodyProcessor; + + private final HttpServerExchange exchange; public UndertowServerHttpResponse(HttpServerExchange exchange, DataBufferFactory dataBufferFactory) { @@ -60,12 +62,10 @@ public class UndertowServerHttpResponse extends AbstractServerHttpResponse this.exchange = exchange; } - public HttpServerExchange getUndertowExchange() { return this.exchange; } - @Override protected void writeStatusCode() { HttpStatus statusCode = this.getStatusCode(); @@ -74,20 +74,37 @@ public class UndertowServerHttpResponse extends AbstractServerHttpResponse } } - @Override protected Mono writeWithInternal(Publisher publisher) { - // lazily create Subscriber, since calling - // {@link HttpServerExchange#getResponseChannel} as done in the - // ResponseBodyProcessor constructor commits the response status and headers - return Mono.from(subscriber -> { - ResponseBodyProcessor processor = new ResponseBodyProcessor(this.exchange); - processor.registerListener(); - publisher.subscribe(processor); - processor.subscribe(subscriber); - }); + Assert.state(this.bodyProcessor == null, + "Response body publisher is already provided"); + try { + synchronized (this.bodyProcessorMonitor) { + if (this.bodyProcessor == null) { + this.bodyProcessor = createBodyProcessor(); + } + else { + throw new IllegalStateException( + "Response body publisher is already provided"); + } + } + return Mono.from(subscriber -> { + publisher.subscribe(this.bodyProcessor); + this.bodyProcessor.subscribe(subscriber); + }); + } + catch (IOException ex) { + return Mono.error(ex); + } } + private ResponseBodyProcessor createBodyProcessor() throws IOException { + ResponseBodyProcessor bodyProcessor = new ResponseBodyProcessor(this.exchange); + bodyProcessor.registerListener(); + return bodyProcessor; + } + + @Override public Mono writeWith(File file, long position, long count) { writeHeaders(); @@ -99,8 +116,8 @@ public class UndertowServerHttpResponse extends AbstractServerHttpResponse FileChannel in = new FileInputStream(file).getChannel(); long result = responseChannel.transferFrom(in, position, count); if (result < count) { - return Mono.error(new IOException("Could only write " + result + - " out of " + count + " bytes")); + return Mono.error(new IOException( + "Could only write " + result + " out of " + count + " bytes")); } else { return Mono.empty();