Merge spring-reactive

This commit is contained in:
Rob Winch 2016-07-14 11:03:32 -05:00
commit 934bc4f953
336 changed files with 39930 additions and 0 deletions

10
spring-web-reactive/.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
target
.project
.classpath
.settings
*.iml
/.idea/
bin
.gradle
/tomcat.*/
build

View File

@ -0,0 +1,56 @@
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
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`
## Documentation
See the current [Javadoc][].
## 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
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][].
[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
[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
[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

View File

@ -0,0 +1,135 @@
buildscript {
repositories {
maven { url 'https://repo.spring.io/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'
jar {
baseName = 'spring-reactive'
}
group = 'org.springframework.reactive'
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
}
ext {
springVersion = '5.0.0.BUILD-SNAPSHOT'
reactorVersion = '2.5.0.BUILD-SNAPSHOT'
reactorNettyVersion = '2.5.0.BUILD-SNAPSHOT'
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/",
"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[]
}
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 {
uniqueVersion = false
}
}
}
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(project.ext.javadocLinks)
}
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}"
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}"
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}"
optional "org.apache.tomcat:tomcat-util:${tomcatVersion}"
optional "org.apache.tomcat.embed:tomcat-embed-core:${tomcatVersion}"
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")
optional("com.fasterxml:aalto-xml:1.0.0")
optional("javax.validation:validation-api:1.1.0.Final")
provided "javax.servlet:javax.servlet-api:3.1.0"
testCompile "junit:junit:4.12"
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'
}
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"
}

View File

@ -0,0 +1,2 @@
version=0.1.0.BUILD-SNAPSHOT

Binary file not shown.

View File

@ -0,0 +1,6 @@
#Fri Aug 07 17:32:01 EDT 2015
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.12-all.zip

164
spring-web-reactive/gradlew vendored Executable file
View File

@ -0,0 +1,164 @@
#!/usr/bin/env bash
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn ( ) {
echo "$*"
}
die ( ) {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
esac
# For Cygwin, ensure paths are in UNIX format before anything is touched.
if $cygwin ; then
[ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
fi
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
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 "$@"

90
spring-web-reactive/gradlew.bat vendored Normal file
View File

@ -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

View File

@ -0,0 +1 @@
rootProject.name = "spring-reactive"

View File

@ -0,0 +1,61 @@
/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.core.codec;
import java.util.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.io.buffer.DataBuffer;
import org.springframework.util.MimeType;
/**
* @author Sebastien Deleuze
* @author Arjen Poutsma
*/
public abstract class AbstractDecoder<T> implements Decoder<T> {
private List<MimeType> decodableMimeTypes = Collections.emptyList();
protected AbstractDecoder(MimeType... supportedMimeTypes) {
this.decodableMimeTypes = Arrays.asList(supportedMimeTypes);
}
@Override
public List<MimeType> getDecodableMimeTypes() {
return this.decodableMimeTypes;
}
@Override
public boolean canDecode(ResolvableType elementType, MimeType mimeType, Object... hints) {
if (mimeType == null) {
return true;
}
return this.decodableMimeTypes.stream().
anyMatch(mt -> mt.isCompatibleWith(mimeType));
}
@Override
public Mono<T> decodeToMono(Publisher<DataBuffer> inputStream, ResolvableType elementType, MimeType mimeType, Object... hints) {
throw new UnsupportedOperationException();
}
}

View File

@ -0,0 +1,53 @@
/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.core.codec;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.springframework.core.ResolvableType;
import org.springframework.util.MimeType;
/**
* @author Sebastien Deleuze
* @author Arjen Poutsma
*/
public abstract class AbstractEncoder<T> implements Encoder<T> {
private List<MimeType> encodableMimeTypes = Collections.emptyList();
protected AbstractEncoder(MimeType... supportedMimeTypes) {
this.encodableMimeTypes = Arrays.asList(supportedMimeTypes);
}
@Override
public List<MimeType> getEncodableMimeTypes() {
return this.encodableMimeTypes;
}
@Override
public boolean canEncode(ResolvableType elementType, MimeType mimeType, Object... hints) {
if (mimeType == null) {
return true;
}
return this.encodableMimeTypes.stream().
anyMatch(mt -> mt.isCompatibleWith(mimeType));
}
}

View File

@ -0,0 +1,68 @@
/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.core.codec;
import 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;
/**
* 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<T> extends AbstractEncoder<T> {
public AbstractSingleValueEncoder(MimeType... supportedMimeTypes) {
super(supportedMimeTypes);
}
@Override
public final Flux<DataBuffer> encode(Publisher<? extends T> inputStream,
DataBufferFactory bufferFactory, ResolvableType elementType, MimeType mimeType,
Object... hints) {
return Flux.from(inputStream).
take(1).
concatMap(t -> {
try {
return encode(t, bufferFactory, elementType, mimeType);
}
catch (Exception ex) {
return Flux.error(ex);
}
});
}
/**
* Encodes {@code T} to an output {@link DataBuffer} stream.
* @param t the value 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
* @return the output stream
* @throws Exception in case of errors
*/
protected abstract Flux<DataBuffer> encode(T t, DataBufferFactory dataBufferFactory,
ResolvableType type, MimeType mimeType, Object... hints) throws Exception;
}

View File

@ -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.core.codec;
import java.nio.ByteBuffer;
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.support.DataBufferUtils;
import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils;
/**
* @author Sebastien Deleuze
* @author Arjen Poutsma
*/
public class ByteBufferDecoder extends AbstractDecoder<ByteBuffer> {
public ByteBufferDecoder() {
super(MimeTypeUtils.ALL);
}
@Override
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<ByteBuffer> decode(Publisher<DataBuffer> inputStream, ResolvableType elementType,
MimeType mimeType, Object... hints) {
return Flux.from(inputStream).map((dataBuffer) -> {
ByteBuffer copy = ByteBuffer.allocate(dataBuffer.readableByteCount());
copy.put(dataBuffer.asByteBuffer());
copy.flip();
DataBufferUtils.release(dataBuffer);
return copy;
});
}
}

View File

@ -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.core.codec;
import java.nio.ByteBuffer;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import org.springframework.core.ResolvableType;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils;
/**
* @author Sebastien Deleuze
*/
public class ByteBufferEncoder extends AbstractEncoder<ByteBuffer> {
public ByteBufferEncoder() {
super(MimeTypeUtils.ALL);
}
@Override
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<DataBuffer> encode(Publisher<? extends ByteBuffer> inputStream,
DataBufferFactory bufferFactory, ResolvableType elementType, MimeType mimeType,
Object... hints) {
return Flux.from(inputStream).map(bufferFactory::wrap);
}
}

View File

@ -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.core.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);
}
}

View File

@ -0,0 +1,83 @@
/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.core.codec;
import java.util.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;
import org.springframework.util.MimeType;
/**
* Strategy for decoding a {@link DataBuffer} input stream into an output stream
* of elements of type {@code <T>}.
*
* @author Sebastien Deleuze
* @author Rossen Stoyanchev
* @param <T> the type of elements in the output stream
*/
public interface Decoder<T> {
/**
* 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 elementType, MimeType mimeType, Object... hints);
/**
* 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<T> decode(Publisher<DataBuffer> 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<T> decodeToMono(Publisher<DataBuffer> inputStream, ResolvableType elementType,
MimeType mimeType, Object... hints);
/**
* Return the list of MIME types this decoder supports.
*/
List<MimeType> getDecodableMimeTypes();
}

View File

@ -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;
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.core.io.buffer.DataBufferFactory;
import org.springframework.util.MimeType;
/**
* Strategy to encode a stream of Objects of type {@code <T>} into an output
* stream of bytes.
*
* @author Sebastien Deleuze
* @author Rossen Stoyanchev
* @param <T> the type of elements in the input stream
*/
public interface Encoder<T> {
/**
* 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 elementType, MimeType mimeType, Object... hints);
/**
* 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<DataBuffer> encode(Publisher<? extends T> inputStream, DataBufferFactory bufferFactory,
ResolvableType elementType, MimeType mimeType, Object... hints);
/**
* Return the list of mime types this encoder supports.
*/
List<MimeType> getEncodableMimeTypes();
}

View File

@ -0,0 +1,82 @@
/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.core.codec;
import java.io.ByteArrayInputStream;
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<Resource> {
public ResourceDecoder() {
super(MimeTypeUtils.ALL);
}
@Override
public boolean canDecode(ResolvableType elementType, MimeType mimeType, Object... hints) {
Class<?> clazz = elementType.getRawClass();
return (InputStreamResource.class.equals(clazz) ||
clazz.isAssignableFrom(ByteArrayResource.class)) &&
super.canDecode(elementType, mimeType, hints);
}
@Override
public Flux<Resource> decode(Publisher<DataBuffer> inputStream, ResolvableType elementType,
MimeType mimeType, Object... hints) {
Class<?> clazz = elementType.getRawClass();
Mono<byte[]> 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)) {
return Flux.from(byteArray.
map(ByteArrayInputStream::new).
map(InputStreamResource::new));
}
else if (clazz.isAssignableFrom(ByteArrayResource.class)) {
return Flux.from(byteArray.
map(ByteArrayResource::new));
}
else {
return Flux.error(new IllegalStateException(
"Unsupported resource class: " + clazz));
}
}
}

View File

@ -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.core.codec;
import java.io.IOException;
import java.io.InputStream;
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.DataBufferFactory;
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 AbstractSingleValueEncoder<Resource> {
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 elementType, MimeType mimeType, Object... hints) {
Class<?> clazz = elementType.getRawClass();
return (super.canEncode(elementType, mimeType, hints) &&
Resource.class.isAssignableFrom(clazz));
}
@Override
protected Flux<DataBuffer> encode(Resource resource,
DataBufferFactory dataBufferFactory,
ResolvableType type, MimeType mimeType, Object... hints) throws IOException {
InputStream is = resource.getInputStream();
return DataBufferUtils.read(is, dataBufferFactory, bufferSize);
}
}

View File

@ -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.core.codec;
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;
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.MimeType;
import org.springframework.util.MimeTypeUtils;
/**
* Decode from a bytes stream to a String stream.
*
* <p>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
* @author Arjen Poutsma
* @author Mark Paluch
* @see StringEncoder
*/
public class StringDecoder extends AbstractDecoder<String> {
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
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
*
* <p>By default, this decoder will split along new lines.
*/
public StringDecoder() {
this(true);
}
/**
* Create a {@code StringDecoder} that decodes a bytes stream to a String stream
*
* @param splitOnNewline whether this decoder should split the received data buffers
* along newline characters
*/
public StringDecoder(boolean splitOnNewline) {
super(new MimeType("text", "*", DEFAULT_CHARSET), MimeTypeUtils.ALL);
this.splitOnNewline = splitOnNewline;
}
@Override
public boolean canDecode(ResolvableType elementType, MimeType mimeType, Object... hints) {
return super.canDecode(elementType, mimeType, hints) &&
String.class.equals(elementType.getRawClass());
}
@Override
public Flux<String> decode(Publisher<DataBuffer> inputStream, ResolvableType elementType,
MimeType mimeType, Object... hints) {
Flux<DataBuffer> inputFlux = Flux.from(inputStream);
if (this.splitOnNewline) {
inputFlux = Flux.from(inputStream).flatMap(StringDecoder::splitOnNewline);
}
return inputFlux.map(buffer -> decodeDataBuffer(buffer, mimeType));
}
@Override
public Mono<String> decodeToMono(Publisher<DataBuffer> inputStream, ResolvableType elementType,
MimeType mimeType, Object... hints) {
return Flux.from(inputStream)
.reduce(DataBuffer::write)
.map(buffer -> decodeDataBuffer(buffer, mimeType));
}
private static Flux<DataBuffer> splitOnNewline(DataBuffer dataBuffer) {
List<DataBuffer> results = new ArrayList<>();
int startIdx = 0;
int endIdx;
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 String decodeDataBuffer(DataBuffer dataBuffer, MimeType mimeType) {
Charset charset = getCharset(mimeType);
CharBuffer charBuffer = charset.decode(dataBuffer.asByteBuffer());
DataBufferUtils.release(dataBuffer);
return charBuffer.toString();
}
private Charset getCharset(MimeType mimeType) {
if (mimeType != null && mimeType.getCharset() != null) {
return mimeType.getCharset();
}
else {
return DEFAULT_CHARSET;
}
}
}

View File

@ -0,0 +1,70 @@
/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.core.codec;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
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;
/**
* Encode from a String stream to a bytes stream.
*
* @author Sebastien Deleuze
* @see StringDecoder
*/
public class StringEncoder extends AbstractEncoder<String> {
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
public StringEncoder() {
super(new MimeType("text", "plain", DEFAULT_CHARSET));
}
@Override
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<DataBuffer> encode(Publisher<? extends String> inputStream,
DataBufferFactory bufferFactory, ResolvableType elementType, MimeType mimeType,
Object... hints) {
Charset charset;
if (mimeType != null && mimeType.getCharset() != null) {
charset = mimeType.getCharset();
}
else {
charset = DEFAULT_CHARSET;
}
return Flux.from(inputStream).map(s -> {
byte[] bytes = s.getBytes(charset);
DataBuffer dataBuffer = bufferFactory.allocateBuffer(bytes.length);
dataBuffer.write(bytes);
return dataBuffer;
});
}
}

View File

@ -0,0 +1,6 @@
/**
* 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;

View File

@ -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.core.convert.support;
import java.util.LinkedHashSet;
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;
/**
* @author Sebastien Deleuze
*/
public class MonoToCompletableFutureConverter implements GenericConverter {
@Override
public Set<ConvertiblePair> getConvertibleTypes() {
Set<GenericConverter.ConvertiblePair> pairs = new LinkedHashSet<>();
pairs.add(new GenericConverter.ConvertiblePair(Mono.class, CompletableFuture.class));
pairs.add(new GenericConverter.ConvertiblePair(CompletableFuture.class, Mono.class));
return pairs;
}
@Override
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
if (source == null) {
return null;
}
else if (CompletableFuture.class.isAssignableFrom(sourceType.getType())) {
return Mono.fromFuture((CompletableFuture) source);
}
else if (CompletableFuture.class.isAssignableFrom(targetType.getType())) {
return Mono.from((Publisher) source).toFuture();
}
return null;
}
}

View File

@ -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.core.convert.support;
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;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.GenericConverter;
/**
* @author Stephane Maldini
* @author Sebastien Deleuze
*/
public final class ReactorToRxJava1Converter implements GenericConverter {
@Override
public Set<GenericConverter.ConvertiblePair> getConvertibleTypes() {
Set<GenericConverter.ConvertiblePair> pairs = new LinkedHashSet<>();
pairs.add(new GenericConverter.ConvertiblePair(Flux.class, Observable.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, Mono.class));
pairs.add(new GenericConverter.ConvertiblePair(Mono.class, Completable.class));
pairs.add(new GenericConverter.ConvertiblePair(Completable.class, Mono.class));
return pairs;
}
@Override
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
if (source == null) {
return null;
}
if (Observable.class.isAssignableFrom(sourceType.getType())) {
return RxJava1ObservableConverter.toPublisher((Observable) source);
}
else if (Observable.class.isAssignableFrom(targetType.getType())) {
return RxJava1ObservableConverter.fromPublisher((Publisher) source);
}
else if (Single.class.isAssignableFrom(sourceType.getType())) {
return RxJava1SingleConverter.toPublisher((Single) source);
}
else if (Single.class.isAssignableFrom(targetType.getType())) {
return RxJava1SingleConverter.fromPublisher((Publisher) source);
}
else if (Completable.class.isAssignableFrom(sourceType.getType())) {
return RxJava1CompletableConverter.toPublisher((Completable) source);
}
else if (Completable.class.isAssignableFrom(targetType.getType())) {
return RxJava1CompletableConverter.fromPublisher((Publisher) source);
}
return null;
}
}

View File

@ -0,0 +1,4 @@
/**
* Default implementation of the type conversion system.
*/
package org.springframework.core.convert.support;

View File

@ -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.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.
*
* @author Arjen Poutsma
*/
public interface DataBuffer {
/**
* Returns the {@link DataBufferFactory} that created this buffer.
* @return the creating buffer factory
*/
DataBufferFactory factory();
/**
* 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 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 fromIndex);
/**
* 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 {@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
*/
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);
/**
* 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 reading nor writing position 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();
}

View File

@ -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.core.io.buffer;
import java.nio.ByteBuffer;
/**
* A factory for {@link DataBuffer}s, allowing for allocation and wrapping of data
* buffers.
*
* @author Arjen Poutsma
* @see DataBuffer
*/
public interface DataBufferFactory {
/**
* 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);
/**
* 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);
}

View File

@ -0,0 +1,361 @@
/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS 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 java.util.function.IntPredicate;
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. Constructed
* using the {@link DefaultDataBufferFactory}.
*
* @author Arjen Poutsma
* @see DefaultDataBufferFactory
*/
public class DefaultDataBuffer implements DataBuffer {
private final DefaultDataBufferFactory dataBufferFactory;
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, DefaultDataBufferFactory dataBufferFactory) {
this(byteBuffer, byteBuffer.position(), byteBuffer.position(), dataBufferFactory);
}
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(dataBufferFactory, "'dataBufferFactory' must not be null");
this.byteBuffer = byteBuffer;
this.readPosition = readPosition;
this.writePosition = writePosition;
this.dataBufferFactory = dataBufferFactory;
}
@Override
public DefaultDataBufferFactory factory() {
return this.dataBufferFactory;
}
/**
* 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 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;
}
}
return -1;
}
@Override
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;
}
}
return -1;
}
@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> T readInternal(Function<ByteBuffer, T> function) {
this.byteBuffer.position(this.readPosition);
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));
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} before and
* after applying the given function on {@link #byteBuffer}.
*/
private <T> T writeInternal(Function<ByteBuffer, T> function) {
this.byteBuffer.position(this.writePosition);
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.dataBufferFactory);
}
finally {
this.byteBuffer.position(oldPosition);
}
}
@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);
}
}
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.readPosition == other.readPosition &&
this.writePosition == other.writePosition &&
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 readableByteCount();
}
@Override
public int read() {
return readInternal(
buffer -> readableByteCount() > 0 ? buffer.get() & 0xFF : -1);
}
@Override
public int read(byte[] bytes, int off, int len) throws IOException {
return readInternal(buffer -> {
int count = readableByteCount();
if (count > 0) {
int minLen = Math.min(len, count);
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));
}
}
private static class SlicedDefaultDataBuffer extends DefaultDataBuffer {
SlicedDefaultDataBuffer(ByteBuffer byteBuffer, int readPosition,
int writePosition, DefaultDataBufferFactory dataBufferFactory) {
super(byteBuffer, readPosition, writePosition, dataBufferFactory);
}
@Override
void grow(int minCapacity) {
throw new UnsupportedOperationException(
"Growing the capacity of a sliced buffer is not supported");
}
}
}

View File

@ -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.core.io.buffer;
import java.nio.ByteBuffer;
import org.springframework.util.Assert;
/**
* 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 DefaultDataBufferFactory implements DataBufferFactory {
/**
* The default capacity when none is specified.
* @see #DefaultDataBufferFactory()
* @see #DefaultDataBufferFactory(boolean)
*/
public static final int DEFAULT_INITIAL_CAPACITY = 256;
private final boolean preferDirect;
private final int defaultInitialCapacity;
/**
* Creates a new {@code DefaultDataBufferFactory} with default settings.
*/
public DefaultDataBufferFactory() {
this(false);
}
/**
* 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 DefaultDataBufferFactory(boolean preferDirect) {
this(preferDirect, DEFAULT_INITIAL_CAPACITY);
}
/**
* 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 DefaultDataBufferFactory(boolean preferDirect, int defaultInitialCapacity) {
Assert.isTrue(defaultInitialCapacity > 0,
"'defaultInitialCapacity' should be larger than 0");
this.preferDirect = preferDirect;
this.defaultInitialCapacity = defaultInitialCapacity;
}
@Override
public DefaultDataBuffer allocateBuffer() {
return allocateBuffer(this.defaultInitialCapacity);
}
@Override
public DefaultDataBuffer allocateBuffer(int initialCapacity) {
return this.preferDirect ?
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(), this);
}
@Override
public String toString() {
return "DefaultDataBufferFactory - preferDirect: " + this.preferDirect;
}
}

View File

@ -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.core.io.buffer;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.util.function.IntPredicate;
/**
* 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;
private FlushingDataBuffer() {
this.buffer = new DefaultDataBufferFactory().allocateBuffer(0);
}
@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.buffer.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();
}
}

View File

@ -0,0 +1,242 @@
/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS 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 java.util.function.IntPredicate;
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 NettyDataBufferFactory}.
*
* @author Arjen Poutsma
*/
public class NettyDataBuffer implements PooledDataBuffer {
private final NettyDataBufferFactory dataBufferFactory;
private ByteBuf byteBuf;
/**
* Creates a new {@code NettyDataBuffer} based on the given {@code ByteBuff}.
* @param byteBuf the buffer to base this buffer on
*/
NettyDataBuffer(ByteBuf byteBuf, NettyDataBufferFactory dataBufferFactory) {
Assert.notNull(byteBuf, "'byteBuf' must not be null");
Assert.notNull(dataBufferFactory, "'dataBufferFactory' must not be null");
this.byteBuf = byteBuf;
this.dataBufferFactory = dataBufferFactory;
}
@Override
public NettyDataBufferFactory factory() {
return this.dataBufferFactory;
}
/**
* 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 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, 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
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) {
ByteBuf[] nativeBuffers = Arrays.stream(buffers)
.map(b -> ((NettyDataBuffer) b).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);
composite.addComponents(byteBufs);
int writerIndex = this.byteBuf.readableBytes() +
Arrays.stream(byteBufs).mapToInt(ByteBuf::readableBytes).sum();
composite.writerIndex(writerIndex);
this.byteBuf = composite;
return this;
}
@Override
public DataBuffer slice(int index, int length) {
ByteBuf slice = this.byteBuf.slice(index, length);
return new NettyDataBuffer(slice, this.dataBufferFactory);
}
@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 PooledDataBuffer retain() {
return new NettyDataBuffer(this.byteBuf.retain(), dataBufferFactory);
}
@Override
public boolean release() {
return this.byteBuf.release();
}
@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();
}
}

View File

@ -0,0 +1,82 @@
/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.core.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;
/**
* 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 NettyDataBufferFactory implements DataBufferFactory {
private final ByteBufAllocator byteBufAllocator;
/**
* 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 NettyDataBufferFactory(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, this);
}
@Override
public NettyDataBuffer allocateBuffer(int initialCapacity) {
ByteBuf byteBuf = this.byteBufAllocator.buffer(initialCapacity);
return new NettyDataBuffer(byteBuf, this);
}
@Override
public NettyDataBuffer wrap(ByteBuffer byteBuffer) {
ByteBuf byteBuf = Unpooled.wrappedBuffer(byteBuffer);
return new NettyDataBuffer(byteBuf, this);
}
/**
* 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, this);
}
@Override
public String toString() {
return "NettyDataBufferFactory (" + this.byteBufAllocator + ")";
}
}

View File

@ -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();
}

View File

@ -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.core.io.buffer.support;
import java.io.IOException;
import java.io.InputStream;
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;
import reactor.core.publisher.Flux;
import reactor.core.subscriber.SignalEmitter;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.PooledDataBuffer;
import org.springframework.util.Assert;
/**i
* Utility class for working with {@link DataBuffer}s.
*
* @author Arjen Poutsma
*/
public abstract class DataBufferUtils {
private static final Consumer<ReadableByteChannel> CLOSE_CONSUMER = channel -> {
try {
if (channel != null) {
channel.close();
}
}
catch (IOException ignored) {
}
};
/**
* Reads the given {@code InputStream} into a {@code Flux} of
* {@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
* @return a flux of data buffers read from the given channel
*/
public static Flux<DataBuffer> read(InputStream inputStream,
DataBufferFactory dataBufferFactory, int bufferSize) {
Assert.notNull(inputStream, "'inputStream' must not be null");
Assert.notNull(dataBufferFactory, "'dataBufferFactory' must not be null");
ReadableByteChannel channel = Channels.newChannel(inputStream);
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 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<DataBuffer> read(ReadableByteChannel channel,
DataBufferFactory dataBufferFactory, int bufferSize) {
Assert.notNull(channel, "'channel' must not be null");
Assert.notNull(dataBufferFactory, "'dataBufferFactory' must not be null");
return Flux.generate(() -> channel,
new ReadableByteChannelGenerator(dataBufferFactory, bufferSize),
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<DataBuffer> takeUntilByteCount(Publisher<DataBuffer> publisher,
long maxByteCount) {
Assert.notNull(publisher, "'publisher' must not be null");
Assert.isTrue(maxByteCount >= 0, "'maxByteCount' must be a positive number");
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);
}
});
}
/**
* 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 extends DataBuffer> T retain(T dataBuffer) {
if (dataBuffer instanceof PooledDataBuffer) {
return (T) ((PooledDataBuffer) dataBuffer).retain();
}
else {
return dataBuffer;
}
}
/**
* 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 ReadableByteChannelGenerator
implements BiFunction<ReadableByteChannel, SignalEmitter<DataBuffer>,
ReadableByteChannel> {
private final DataBufferFactory dataBufferFactory;
private final int chunkSize;
public ReadableByteChannelGenerator(DataBufferFactory dataBufferFactory,
int chunkSize) {
this.dataBufferFactory = dataBufferFactory;
this.chunkSize = chunkSize;
}
@Override
public ReadableByteChannel apply(ReadableByteChannel
channel, SignalEmitter<DataBuffer> sub) {
try {
ByteBuffer byteBuffer = ByteBuffer.allocate(chunkSize);
int read;
if ((read = channel.read(byteBuffer)) > 0) {
byteBuffer.flip();
boolean release = true;
DataBuffer dataBuffer = this.dataBufferFactory.allocateBuffer(read);
try {
dataBuffer.write(byteBuffer);
release = false;
sub.next(dataBuffer);
}
finally {
if (release) {
release(dataBuffer);
}
}
}
else {
sub.complete();
}
}
catch (IOException ex) {
sub.fail(ex);
}
return channel;
}
}
}

View File

@ -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;
}
}

View File

@ -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;
import org.springframework.util.Assert;
/**
* 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 <a href="https://tools.ietf.org/html/rfc6265">RFC 6265</a>
*/
public class HttpCookie {
private final String name;
private final String value;
public HttpCookie(String name, String value) {
Assert.hasLength(name, "'name' is required and must not be empty.");
this.name = name;
this.value = (value != null ? value : "");
}
/**
* Return the cookie name.
*/
public String getName() {
return this.name;
}
/**
* Return the cookie value or an empty string, never {@code null}.
*/
public String getValue() {
return this.value;
}
@Override
public int hashCode() {
return this.name.hashCode();
}
@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()));
}
}

View File

@ -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.http;
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}.
*
* <p>Typically implemented by an HTTP request on the server-side or a response
* on the client-side.
*
* @author Arjen Poutsma
*/
public interface ReactiveHttpInputMessage extends HttpMessage {
/**
* Return the body of the message as a {@link Publisher}.
* @return the body content publisher
*/
Flux<DataBuffer> getBody();
}

View File

@ -0,0 +1,76 @@
/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS 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.function.Supplier;
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.core.io.buffer.FlushingDataBuffer;
/**
* A "reactive" HTTP output message that accepts output as a {@link Publisher}.
*
* <p>Typically implemented by an HTTP request on the client-side or a response
* on the server-side.
*
* @author Arjen Poutsma
* @author Sebastien Deleuze
*/
public interface ReactiveHttpOutputMessage extends HttpMessage {
/**
* Register an action to be applied just before the message is committed.
* @param action the action
*/
void beforeCommit(Supplier<? extends Mono<Void>> action);
/**
* 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).
*
* <p>Each {@link FlushingDataBuffer} element will trigger a flush.
*
* @param body the body content publisher
* @return a publisher that indicates completion or error.
*/
Mono<Void> writeWith(Publisher<DataBuffer> body);
/**
* Returns a {@link DataBufferFactory} that can be used for creating the body.
* @return a buffer factory
* @see #writeWith(Publisher)
*/
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).
* <p>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<Void> setComplete();
}

View File

@ -0,0 +1,238 @@
/*
* 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;
/**
* 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 <a href="https://tools.ietf.org/html/rfc6265">RFC 6265</a>
*/
public final class ResponseCookie extends HttpCookie {
private final Duration maxAge;
private final Optional<String> domain;
private final Optional<String> path;
private final boolean secure;
private final boolean httpOnly;
/**
* Private constructor. See {@link #from(String, String)}.
*/
private ResponseCookie(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.
*
* <p>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<String> getDomain() {
return this.domain;
}
/**
* Return the cookie "Path" attribute.
*/
public Optional<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 <a href="http://www.owasp.org/index.php/HTTPOnly">http://www.owasp.org/index.php/HTTPOnly</a>
*/
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 ResponseCookie)) {
return false;
}
ResponseCookie otherCookie = (ResponseCookie) 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 ResponseCookieBuilder from(final String name, final String value) {
return new ResponseCookieBuilder() {
private Duration maxAge = Duration.ofSeconds(-1);
private String domain;
private String path;
private boolean secure;
private boolean httpOnly;
@Override
public ResponseCookieBuilder maxAge(Duration maxAge) {
this.maxAge = maxAge;
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;
return this;
}
@Override
public ResponseCookieBuilder path(String path) {
this.path = path;
return this;
}
@Override
public ResponseCookieBuilder secure(boolean secure) {
this.secure = secure;
return this;
}
@Override
public ResponseCookieBuilder httpOnly(boolean httpOnly) {
this.httpOnly = httpOnly;
return this;
}
@Override
public ResponseCookie build() {
return new ResponseCookie(name, value, this.maxAge, this.domain, this.path,
this.secure, this.httpOnly);
}
};
}
/**
* A builder for a server-defined HttpCookie with attributes.
*/
public interface ResponseCookieBuilder {
/**
* Set the cookie "Max-Age" attribute.
*
* <p>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.
*/
ResponseCookieBuilder maxAge(Duration maxAge);
/**
* Set the cookie "Max-Age" attribute in seconds.
*/
ResponseCookieBuilder maxAge(long maxAgeSeconds);
/**
* Set the cookie "Path" attribute.
*/
ResponseCookieBuilder path(String path);
/**
* Set the cookie "Domain" attribute.
*/
ResponseCookieBuilder domain(String domain);
/**
* Add the "Secure" attribute to the cookie.
*/
ResponseCookieBuilder secure(boolean secure);
/**
* Add the "HttpOnly" attribute to the cookie.
* @see <a href="http://www.owasp.org/index.php/HTTPOnly">http://www.owasp.org/index.php/HTTPOnly</a>
*/
ResponseCookieBuilder httpOnly(boolean httpOnly);
/**
* Create the HttpCookie.
*/
ResponseCookie build();
}
}

View File

@ -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 <a href="https://en.wikipedia.org/wiki/Zero-copy">Zero-copy</a>
*/
public interface ZeroCopyHttpOutputMessage extends ReactiveHttpOutputMessage {
/**
* 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<Void> writeWith(File file, long position, long count);
}

View File

@ -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.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.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.
*
* @author Rossen Stoyanchev
* @author Brian Clozel
*/
public abstract class AbstractClientHttpRequest implements ClientHttpRequest {
private final HttpHeaders headers;
private final MultiValueMap<String, HttpCookie> cookies;
private AtomicReference<State> state = new AtomicReference<>(State.NEW);
private final List<Supplier<? extends Mono<Void>>> beforeCommitActions = new ArrayList<>(4);
public AbstractClientHttpRequest() {
this(new HttpHeaders());
}
public AbstractClientHttpRequest(HttpHeaders headers) {
Assert.notNull(headers);
this.headers = headers;
this.cookies = new LinkedMultiValueMap<>();
}
@Override
public HttpHeaders getHeaders() {
if (State.COMITTED.equals(this.state.get())) {
return HttpHeaders.readOnlyHttpHeaders(this.headers);
}
return this.headers;
}
@Override
public MultiValueMap<String, HttpCookie> getCookies() {
if (State.COMITTED.equals(this.state.get())) {
return CollectionUtils.unmodifiableMultiValueMap(this.cookies);
}
return this.cookies;
}
protected Mono<Void> applyBeforeCommit() {
Mono<Void> mono = Mono.empty();
if (this.state.compareAndSet(State.NEW, State.COMMITTING)) {
for (Supplier<? extends Mono<Void>> action : this.beforeCommitActions) {
mono = mono.then(() -> action.get());
}
return mono
.otherwise(ex -> {
// Ignore errors from beforeCommit actions
return Mono.empty();
})
.then(() -> {
this.state.set(State.COMITTED);
writeHeaders();
writeCookies();
return Mono.empty();
});
}
return mono;
}
@Override
public void beforeCommit(Supplier<? extends Mono<Void>> action) {
Assert.notNull(action);
this.beforeCommitActions.add(action);
}
protected abstract void writeHeaders();
protected abstract void writeCookies();
private enum State {NEW, COMMITTING, COMITTED}
}

View File

@ -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.
* <p>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<Void>} by calling
* {@link ClientHttpRequest#writeWith} or {@link ClientHttpRequest#setComplete}.
* @return a publisher of the {@link ClientHttpResponse}
*/
Mono<ClientHttpResponse> connect(HttpMethod method, URI uri,
Function<? super ClientHttpRequest, Mono<Void>> requestCallback);
}

View File

@ -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.http.client.reactive;
import java.net.URI;
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.
*
* @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();
/**
* Return a mutable map of request cookies to send to the server.
*/
MultiValueMap<String, HttpCookie> getCookies();
}

View File

@ -0,0 +1,41 @@
/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS 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 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.
*
* @author Arjen Poutsma
*/
public interface ClientHttpResponse extends ReactiveHttpInputMessage {
/**
* @return the HTTP status as an {@link HttpStatus} enum value
*/
HttpStatus getStatusCode();
/**
* Return a read-only map of response cookies received from the server.
*/
MultiValueMap<String, ResponseCookie> getCookies();
}

View File

@ -0,0 +1,43 @@
/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS 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 org.springframework.http.HttpMethod;
import reactor.core.publisher.Mono;
/**
* Reactor-Netty implementation of {@link ClientHttpConnector}
*
* @author Brian Clozel
*/
public class ReactorClientHttpConnector implements ClientHttpConnector {
@Override
public Mono<ClientHttpResponse> connect(HttpMethod method, URI uri,
Function<? super ClientHttpRequest, Mono<Void>> 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));
}
}

View File

@ -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.client.reactive;
import java.net.URI;
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.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.NettyDataBuffer;
import org.springframework.core.io.buffer.NettyDataBufferFactory;
import org.springframework.http.HttpMethod;
/**
* {@link ClientHttpRequest} implementation for the Reactor-Netty HTTP client
*
* @author Brian Clozel
* @see reactor.io.netty.http.HttpClient
*/
public class ReactorClientHttpRequest extends AbstractClientHttpRequest {
private final HttpMethod httpMethod;
private final URI uri;
private final HttpClientRequest httpRequest;
private final NettyDataBufferFactory bufferFactory;
public ReactorClientHttpRequest(HttpMethod httpMethod, URI uri, HttpClientRequest httpRequest) {
this.httpMethod = httpMethod;
this.uri = uri;
this.httpRequest = httpRequest;
this.bufferFactory = new NettyDataBufferFactory(httpRequest.delegate().alloc());
}
@Override
public DataBufferFactory bufferFactory() {
return this.bufferFactory;
}
@Override
public HttpMethod getMethod() {
return this.httpMethod;
}
@Override
public URI getURI() {
return this.uri;
}
@Override
public Mono<Void> writeWith(Publisher<DataBuffer> body) {
return applyBeforeCommit()
.then(httpRequest.send(Flux.from(body).map(this::toByteBuf)));
}
@Override
public Mono<Void> setComplete() {
return applyBeforeCommit().then(httpRequest.sendHeaders());
}
private ByteBuf toByteBuf(DataBuffer buffer) {
if (buffer instanceof NettyDataBuffer) {
return ((NettyDataBuffer) buffer).getNativeBuffer();
}
else {
return Unpooled.wrappedBuffer(buffer.asByteBuffer());
}
}
@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));
}
}

View File

@ -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.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.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;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
/**
* {@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 NettyDataBufferFactory dataBufferFactory;
private final HttpClientResponse response;
public ReactorClientHttpResponse(HttpClientResponse response) {
this.response = response;
this.dataBufferFactory = new NettyDataBufferFactory(response.delegate().alloc());
}
@Override
public Flux<DataBuffer> getBody() {
return response.receive()
.map(buf -> {
buf.retain();
return dataBufferFactory.wrap(buf);
});
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
this.response.responseHeaders().entries().stream().forEach(e -> headers.add(e.getKey(), e.getValue()));
return headers;
}
@Override
public HttpStatus getStatusCode() {
return HttpStatus.valueOf(this.response.status().code());
}
@Override
public MultiValueMap<String, ResponseCookie> getCookies() {
MultiValueMap<String, ResponseCookie> result = new LinkedMultiValueMap<>();
this.response.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.isSecure())
.httpOnly(cookie.isHttpOnly())
.build();
result.add(cookie.name(), responseCookie);
});
return CollectionUtils.unmodifiableMultiValueMap(result);
}
@Override
public String toString() {
return "ReactorClientHttpResponse{" +
"request=" + this.response.method().name() + " " + this.response.uri() + "," +
"status=" + getStatusCode() +
'}';
}
}

View File

@ -0,0 +1,7 @@
/**
* 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
*/
package org.springframework.http.client.reactive;

View File

@ -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.http.codec;
import java.nio.charset.StandardCharsets;
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.codec.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;
/**
* 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<Object> {
private final List<Encoder<?>> dataEncoders;
public SseEventEncoder(List<Encoder<?>> dataEncoders) {
super(new MimeType("text", "event-stream"));
Assert.notNull(dataEncoders, "'dataEncoders' must not be null");
this.dataEncoders = dataEncoders;
}
@Override
public Flux<DataBuffer> 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> dataBuffer = Flux.empty();
MediaType mediaType = (event.getMediaType() == null ?
MediaType.ALL : event.getMediaType());
if (data != null) {
sb.append("data:");
if (data instanceof String) {
sb.append(((String)data).replaceAll("\\n", "\ndata:")).append("\n");
}
else {
Optional<Encoder<?>> encoder = dataEncoders
.stream()
.filter(e -> e.canEncode(ResolvableType.forClass(data.getClass()), mediaType))
.findFirst();
if (encoder.isPresent()) {
dataBuffer = ((Encoder<Object>)encoder.get())
.encode(Mono.just(data), bufferFactory,
ResolvableType.forClass(data.getClass()), mediaType)
.concatWith(encodeString("\n", bufferFactory));
}
else {
throw new CodecException("No suitable encoder found!");
}
}
}
// 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),
Flux.never()
);
});
}
private Mono<DataBuffer> encodeString(String str, DataBufferFactory bufferFactory) {
byte[] bytes = str.getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = bufferFactory.allocateBuffer(bytes.length).write(bytes);
return Mono.just(buffer);
}
}

View File

@ -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.http.codec.json;
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;
import reactor.core.publisher.Mono;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.CodecException;
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;
import org.springframework.util.MimeType;
/**
* 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<Object> {
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 JsonObjectDecoder fluxObjectDecoder = new JsonObjectDecoder(true);
private final JsonObjectDecoder monoObjectDecoder = new JsonObjectDecoder(false);
public JacksonJsonDecoder() {
this(new ObjectMapper());
}
public JacksonJsonDecoder(ObjectMapper mapper) {
super(MIME_TYPES);
this.mapper = mapper;
}
@Override
public Flux<Object> decode(Publisher<DataBuffer> inputStream, ResolvableType elementType,
MimeType mimeType, Object... hints) {
JsonObjectDecoder objectDecoder = this.fluxObjectDecoder;
return decodeInternal(objectDecoder, inputStream, elementType, mimeType, hints);
}
@Override
public Mono<Object> decodeToMono(Publisher<DataBuffer> inputStream, ResolvableType elementType,
MimeType mimeType, Object... hints) {
JsonObjectDecoder objectDecoder = this.monoObjectDecoder;
return decodeInternal(objectDecoder, inputStream, elementType, mimeType, hints).single();
}
private Flux<Object> decodeInternal(JsonObjectDecoder objectDecoder, Publisher<DataBuffer> 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 objectDecoder.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));
}
});
}
}

View File

@ -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.http.codec.json;
import java.io.IOException;
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;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.CodecException;
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;
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<Object> {
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());
}
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;
}
@Override
public Flux<DataBuffer> 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) {
return Flux.from(inputStream).map(value -> encodeValue(value, bufferFactory, elementType));
}
Mono<DataBuffer> startArray = Mono.just(bufferFactory.wrap(START_ARRAY_BUFFER));
Mono<DataBuffer> endArray = Mono.just(bufferFactory.wrap(END_ARRAY_BUFFER));
Flux<DataBuffer> 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) {
TypeFactory typeFactory = this.mapper.getTypeFactory();
JavaType javaType = typeFactory.constructType(type.getType());
ObjectWriter writer = this.mapper.writerFor(javaType);
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;
}
}

View File

@ -0,0 +1,265 @@
/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.codec.json;
import java.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 org.springframework.core.ResolvableType;
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;
import org.springframework.util.MimeType;
/**
* 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 byte
* is considered a JSON object/array if it contains a matching number of opening
* and closing braces/brackets.
*
* Based on <a href="https://github.com/netty/netty/blob/master/codec/src/main/java/io/netty/handler/codec/json/JsonObjectDecoder.java">Netty JsonObjectDecoder</a>
*
* @author Sebastien Deleuze
*/
class JsonObjectDecoder extends AbstractDecoder<DataBuffer> {
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
*/
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");
}
this.maxObjectLength = maxObjectLength;
this.streamArrayElements = streamArrayElements;
}
@Override
public Flux<DataBuffer> decode(Publisher<DataBuffer> inputStream, ResolvableType elementType,
MimeType mimeType, Object... hints) {
return Flux.from(inputStream)
.flatMap(new Function<DataBuffer, Publisher<? extends DataBuffer>>() {
int openBraces;
int index;
int state;
boolean insideString;
ByteBuf input;
Integer writerIndex;
@Override
public Publisher<? extends DataBuffer> apply(DataBuffer b) {
List<DataBuffer> 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) {
this.input.skipBytes(this.input.readableBytes());
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 Flux.error(new IllegalStateException("object length exceeds " +
maxObjectLength + ": " + this.writerIndex + " bytes discarded"));
}
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) {
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 (this.openBraces == 0) {
ByteBuf json = extractObject(this.input, this.input.readerIndex(),
this.index + 1 - this.input.readerIndex());
if (json != null) {
chunks.add(dataBufferFactory.wrap(json.nioBuffer()));
}
// The JSON object/array was extracted => discard the bytes from
// the input buffer.
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 (this.state == ST_DECODING_ARRAY_STREAM) {
decodeByte(c, this.input, this.index);
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 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 = this.index - 1;
while (idxNoSpaces >= this.input.readerIndex() &&
Character.isWhitespace(this.input.getByte(idxNoSpaces))) {
idxNoSpaces--;
}
ByteBuf json = extractObject(this.input, this.input.readerIndex(),
idxNoSpaces + 1 - this.input.readerIndex());
if (json != null) {
chunks.add(dataBufferFactory.wrap(json.nioBuffer()));
}
this.input.readerIndex(this.index + 1);
if (c == ']') {
reset();
}
}
// JSON object/array detected. Accumulate bytes until all braces/brackets are closed.
}
else if (c == '{' || c == '[') {
initDecoding(c, streamArrayElements);
if (this.state == ST_DECODING_ARRAY_STREAM) {
// Discard the array bracket
this.input.skipBytes(1);
}
// Discard leading spaces in front of a JSON object/array.
}
else if (Character.isWhitespace(c)) {
this.input.skipBytes(1);
}
else {
this.state = ST_CORRUPTED;
return Flux.error(new IllegalStateException(
"invalid JSON received at byte position " + this.index + ": " +
ByteBufUtil.hexDump(this.input)));
}
}
if (this.input.readableBytes() == 0) {
this.index = 0;
}
return Flux.fromIterable(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 input, int index) {
if ((c == '{' || c == '[') && !this.insideString) {
this.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 (!this.insideString) {
this.insideString = true;
// If the double quote wasn't escaped then this is the end of a string.
}
else if (input.getByte(index - 1) != '\\') {
this.insideString = false;
}
}
}
private void initDecoding(byte openingBrace, boolean streamArrayElements) {
this.openBraces = 1;
if (openingBrace == '[' && streamArrayElements) {
this.state = ST_DECODING_ARRAY_STREAM;
}
else {
this.state = ST_DECODING_NORMAL;
}
}
private void reset() {
this.insideString = false;
this.state = ST_INIT;
this.openBraces = 0;
}
});
}
}

View File

@ -0,0 +1,216 @@
/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.codec.xml;
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.Unmarshaller;
import javax.xml.bind.annotation.XmlRootElement;
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 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.AbstractDecoder;
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).
*
* @author Sebastien Deleuze
* @author Arjen Poutsma
* @see Jaxb2Encoder
*/
public class Jaxb2Decoder extends AbstractDecoder<Object> {
/**
* 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 elementType, MimeType mimeType, Object... hints) {
if (super.canDecode(elementType, mimeType, hints)) {
Class<?> outputClass = elementType.getRawClass();
return outputClass.isAnnotationPresent(XmlRootElement.class) ||
outputClass.isAnnotationPresent(XmlType.class);
}
else {
return false;
}
}
@Override
public Flux<Object> decode(Publisher<DataBuffer> inputStream, ResolvableType elementType,
MimeType mimeType, Object... hints) {
Class<?> outputClass = elementType.getRawClass();
Flux<XMLEvent> xmlEventFlux =
this.xmlEventDecoder.decode(inputStream, null, mimeType);
QName typeName = toQName(outputClass);
Flux<List<XMLEvent>> splitEvents = split(xmlEventFlux, typeName);
return splitEvents.map(events -> unmarshal(events, outputClass));
}
/**
* 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 {
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);
}
/**
* 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}".
* <ol>
* <li>The first list, dealing with the first branch of the tree
* <ol>
* <li>{@link javax.xml.stream.events.StartElement} {@code child}</li>
* <li>{@link javax.xml.stream.events.Characters} {@code foo}</li>
* <li>{@link javax.xml.stream.events.EndElement} {@code child}</li>
* </ol>
* <li>The second list, dealing with the second branch of the tree
* <ol>
* <li>{@link javax.xml.stream.events.StartElement} {@code child}</li>
* <li>{@link javax.xml.stream.events.Characters} {@code bar}</li>
* <li>{@link javax.xml.stream.events.EndElement} {@code child}</li>
* </ol>
* </li>
* </ol>
*/
Flux<List<XMLEvent>> split(Flux<XMLEvent> xmlEventFlux, QName desiredName) {
return xmlEventFlux
.flatMap(new Function<XMLEvent, Publisher<? extends List<XMLEvent>>>() {
private List<XMLEvent> events = null;
private int elementDepth = 0;
private int barrier = Integer.MAX_VALUE;
@Override
public Publisher<? extends List<XMLEvent>> 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<XMLEvent>();
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<XMLEvent> events, Class<?> outputClass) {
try {
Unmarshaller unmarshaller = this.jaxbContexts.createUnmarshaller(outputClass);
XMLEventReader eventReader = StaxUtils2.createXMLEventReader(events);
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(ex.getMessage(), ex);
}
}
}

View File

@ -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.http.codec.xml;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;
import reactor.core.publisher.Flux;
import org.springframework.core.ResolvableType;
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;
import org.springframework.util.MimeType;
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 AbstractSingleValueEncoder<Object> {
private final JaxbContextContainer jaxbContexts = new JaxbContextContainer();
public Jaxb2Encoder() {
super(MimeTypeUtils.APPLICATION_XML, MimeTypeUtils.TEXT_XML);
}
@Override
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);
}
else {
return false;
}
}
@Override
protected Flux<DataBuffer> encode(Object value, DataBufferFactory dataBufferFactory,
ResolvableType type, MimeType mimeType, Object... hints) {
try {
DataBuffer buffer = dataBufferFactory.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);
}
}
}

View File

@ -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.http.codec.xml;
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<Class<?>, 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;
}
}

View File

@ -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.http.codec.xml;
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.codec.AbstractDecoder;
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:
* <pre>{@code
* <root>
* <child>foo</child>
* <child>bar</child>
* </root>}
* </pre>
* this method with result in a flux with the following events:
* <ol>
* <li>{@link javax.xml.stream.events.StartDocument}</li>
* <li>{@link javax.xml.stream.events.StartElement} {@code root}</li>
* <li>{@link javax.xml.stream.events.StartElement} {@code child}</li>
* <li>{@link javax.xml.stream.events.Characters} {@code foo}</li>
* <li>{@link javax.xml.stream.events.EndElement} {@code child}</li>
* <li>{@link javax.xml.stream.events.StartElement} {@code child}</li>
* <li>{@link javax.xml.stream.events.Characters} {@code bar}</li>
* <li>{@link javax.xml.stream.events.EndElement} {@code child}</li>
* <li>{@link javax.xml.stream.events.EndElement} {@code root}</li>
* </ol>
*
* 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<XMLEvent> {
private static final boolean aaltoPresent = ClassUtils
.isPresent("com.fasterxml.aalto.AsyncXMLStreamReader",
XmlEventDecoder.class.getClassLoader());
private static final XMLInputFactory inputFactory = XMLInputFactory.newFactory();
boolean useAalto = true;
public XmlEventDecoder() {
super(MimeTypeUtils.APPLICATION_XML, MimeTypeUtils.TEXT_XML);
}
@Override
public Flux<XMLEvent> decode(Publisher<DataBuffer> inputStream, ResolvableType elementType,
MimeType mimeType, Object... hints) {
Flux<DataBuffer> flux = Flux.from(inputStream);
if (useAalto && aaltoPresent) {
return flux.flatMap(new AaltoDataBufferToXmlEvent());
}
else {
Mono<DataBuffer> singleBuffer = flux.reduce(DataBuffer::write);
return singleBuffer.
flatMap(dataBuffer -> {
try {
InputStream is = dataBuffer.asInputStream();
XMLEventReader eventReader =
inputFactory.createXMLEventReader(is);
return Flux
.fromIterable((Iterable<XMLEvent>) () -> eventReader);
}
catch (XMLStreamException ex) {
return Mono.error(ex);
}
finally {
DataBufferUtils.release(dataBuffer);
}
});
}
}
/*
* Separate static class to isolate Aalto dependency.
*/
private static class AaltoDataBufferToXmlEvent
implements Function<DataBuffer, Publisher<? extends XMLEvent>> {
private static final AsyncXMLInputFactory inputFactory =
(AsyncXMLInputFactory) XmlEventDecoder.inputFactory;
private final AsyncXMLStreamReader<AsyncByteBufferFeeder> streamReader =
inputFactory.createAsyncForByteBuffer();
private final XMLEventAllocator eventAllocator =
EventAllocatorImpl.getDefaultInstance();
@Override
public Publisher<? extends XMLEvent> apply(DataBuffer dataBuffer) {
try {
streamReader.getInputFeeder().feedInput(dataBuffer.asByteBuffer());
List<XMLEvent> 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);
}
finally {
DataBufferUtils.release(dataBuffer);
}
}
}
}

View File

@ -0,0 +1,189 @@
/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS 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 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.DataBufferFactory;
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
* @author Sebastien Deleuze
* @author Rossen Stoyanchev
*/
public class CodecHttpMessageConverter<T> implements HttpMessageConverter<T> {
private final Encoder<T> encoder;
private final Decoder<T> decoder;
private final List<MediaType> readableMediaTypes;
private final List<MediaType> writableMediaTypes;
/**
* 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<T> 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<T> 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<T> encoder, Decoder<T> 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
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<MediaType> getReadableMediaTypes() {
return this.readableMediaTypes;
}
@Override
public List<MediaType> getWritableMediaTypes() {
return this.writableMediaTypes;
}
@Override
public Flux<T> read(ResolvableType type, ReactiveHttpInputMessage inputMessage) {
if (this.decoder == null) {
return Flux.error(new IllegalStateException("No decoder set"));
}
MediaType contentType = getContentType(inputMessage);
return this.decoder.decode(inputMessage.getBody(), type, contentType);
}
@Override
public Mono<T> readMono(ResolvableType type, ReactiveHttpInputMessage inputMessage) {
if (this.decoder == null) {
return Mono.error(new IllegalStateException("No decoder set"));
}
MediaType contentType = getContentType(inputMessage);
return this.decoder.decodeToMono(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<Void> write(Publisher<? extends T> inputStream, ResolvableType type,
MediaType contentType, ReactiveHttpOutputMessage outputMessage) {
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);
}
else if (MediaType.APPLICATION_OCTET_STREAM.equals(contentType)) {
MediaType mediaType = getDefaultContentType(type);
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);
}
}
DataBufferFactory bufferFactory = outputMessage.bufferFactory();
Flux<DataBuffer> body = this.encoder.encode(inputStream, bufferFactory, type, contentType);
return outputMessage.writeWith(body);
}
/**
* Return the default content type for the given {@code ResolvableType}.
* Used when {@link #write} is called without a concrete content type.
*
* <p>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}
*/
@SuppressWarnings("UnusedParameters")
protected MediaType getDefaultContentType(ResolvableType elementType) {
return (!this.writableMediaTypes.isEmpty() ? this.writableMediaTypes.get(0) : null);
}
}

View File

@ -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.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<T> {
/**
* 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<MediaType> getReadableMediaTypes();
/**
* 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 {@link Flux} of elements
*/
Flux<T> 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<T> readMono(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<MediaType> 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<Void> write(Publisher<? extends T> inputStream,
ResolvableType type, MediaType contentType,
ReactiveHttpOutputMessage outputMessage);
}

View File

@ -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.http.converter.reactive;
import java.io.File;
import java.io.IOException;
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.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;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ReactiveHttpOutputMessage;
import org.springframework.http.ZeroCopyHttpOutputMessage;
import org.springframework.http.support.MediaTypeUtils;
import org.springframework.util.MimeTypeUtils2;
/**
* Implementation of {@link HttpMessageConverter} that can read and write
* {@link Resource Resources} and supports byte range requests.
**
* @author Arjen Poutsma
*/
public class ResourceHttpMessageConverter extends CodecHttpMessageConverter<Resource> {
public ResourceHttpMessageConverter() {
super(new ResourceEncoder(), new ResourceDecoder());
}
public ResourceHttpMessageConverter(int bufferSize) {
super(new ResourceEncoder(bufferSize), new ResourceDecoder());
}
@Override
public Mono<Void> write(Publisher<? extends Resource> 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);
return writeContent(resource, type, contentType, outputMessage);
}));
}
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);
}
}
private Mono<Void> writeContent(Resource resource, ResolvableType type,
MediaType contentType, ReactiveHttpOutputMessage outputMessage) {
if (outputMessage instanceof ZeroCopyHttpOutputMessage) {
Optional<File> file = getFile(resource);
if (file.isPresent()) {
ZeroCopyHttpOutputMessage zeroCopyResponse =
(ZeroCopyHttpOutputMessage) outputMessage;
return zeroCopyResponse
.writeWith(file.get(), (long) 0, file.get().length());
}
}
// non-zero copy fallback, using ResourceEncoder
return super.write(Mono.just(resource), type,
outputMessage.getHeaders().getContentType(), outputMessage);
}
private static Optional<Long> 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<File> getFile(Resource resource) {
if (ResourceUtils2.hasFile(resource)) {
try {
return Optional.of(resource.getFile());
}
catch (IOException ignored) {
// should not happen
}
}
return Optional.empty();
}
}

View File

@ -0,0 +1,5 @@
/**
* Contains a basic abstraction over client/server-side HTTP. This package contains
* the {@code HttpInputMessage} and {@code HttpOutputMessage} interfaces.
*/
package org.springframework.http;

View File

@ -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.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;
/**
* 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 AbstractRequestBodyPublisher implements Publisher<DataBuffer> {
protected final Log logger = LogFactory.getLog(getClass());
private final AtomicReference<State> state =
new AtomicReference<>(State.UNSUBSCRIBED);
private final AtomicLong demand = new AtomicLong();
private Subscriber<? super DataBuffer> subscriber;
@Override
public void subscribe(Subscriber<? super DataBuffer> subscriber) {
if (this.logger.isTraceEnabled()) {
this.logger.trace(this.state + " subscribe: " + subscriber);
}
this.state.get().subscribe(this, subscriber);
}
/**
* Called via a listener interface to indicate that reading is possible.
* @see ReadListener#onDataAvailable()
* @see org.xnio.ChannelListener#handleEvent(Channel)
*/
protected final void onDataAvailable() {
if (this.logger.isTraceEnabled()) {
this.logger.trace(this.state + " onDataAvailable");
}
this.state.get().onDataAvailable(this);
}
/**
* 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 onAllDataRead() {
if (this.logger.isTraceEnabled()) {
this.logger.trace(this.state + " onAllDataRead");
}
this.state.get().onAllDataRead(this);
}
/**
* Called by a listener interface to indicate that as error has occured.
* @param t the error
* @see ReadListener#onError(Throwable)
*/
protected final void onError(Throwable t) {
if (this.logger.isErrorEnabled()) {
this.logger.error(this.state + " onError: " + t, t);
}
this.state.get().onError(this, t);
}
/**
* 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 demand; {@code false} otherwise
*/
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 false;
}
protected abstract void checkOnDataAvailable();
/**
* 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;
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.
*
* <pre>
* UNSUBSCRIBED
* |
* v
* NO_DEMAND -------------------> DEMAND
* | ^ ^ |
* | | | |
* | --------- READING <----- |
* | | |
* | v |
* ------------> COMPLETED <---------
* </pre>
* 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 #NO_DEMAND}.
*/
UNSUBSCRIBED {
@Override
void subscribe(AbstractRequestBodyPublisher publisher,
Subscriber<? super DataBuffer> subscriber) {
Objects.requireNonNull(subscriber);
if (publisher.changeState(this, NO_DEMAND)) {
Subscription subscription = new RequestBodySubscription(
publisher);
publisher.subscriber = subscriber;
subscriber.onSubscribe(subscription);
}
else {
throw new IllegalStateException(toString());
}
}
},
/**
* 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.
*/
NO_DEMAND {
@Override
void request(AbstractRequestBodyPublisher publisher, long n) {
if (BackpressureUtils.checkRequest(n, publisher.subscriber)) {
BackpressureUtils.addAndGet(publisher.demand, n);
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.
*/
COMPLETED {
@Override
void request(AbstractRequestBodyPublisher publisher, long n) {
// ignore
}
@Override
void cancel(AbstractRequestBodyPublisher publisher) {
// ignore
}
@Override
void onAllDataRead(AbstractRequestBodyPublisher publisher) {
// ignore
}
@Override
void onError(AbstractRequestBodyPublisher publisher, Throwable t) {
// ignore
}
};
void subscribe(AbstractRequestBodyPublisher publisher,
Subscriber<? super DataBuffer> subscriber) {
throw new IllegalStateException(toString());
}
void request(AbstractRequestBodyPublisher publisher, long n) {
throw new IllegalStateException(toString());
}
void cancel(AbstractRequestBodyPublisher publisher) {
publisher.changeState(this, COMPLETED);
}
void onDataAvailable(AbstractRequestBodyPublisher publisher) {
// ignore
}
void onAllDataRead(AbstractRequestBodyPublisher publisher) {
if (publisher.changeState(this, COMPLETED)) {
if (publisher.subscriber != null) {
publisher.subscriber.onComplete();
}
}
}
void onError(AbstractRequestBodyPublisher publisher, Throwable t) {
if (publisher.changeState(this, COMPLETED)) {
if (publisher.subscriber != null) {
publisher.subscriber.onError(t);
}
}
}
}
}

View File

@ -0,0 +1,329 @@
/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS 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 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<DataBuffer, Void> {
protected final Log logger = LogFactory.getLog(getClass());
private final ResponseBodyWriteResultPublisher publisherDelegate =
new ResponseBodyWriteResultPublisher();
private final AtomicReference<State> state =
new AtomicReference<>(State.UNSUBSCRIBED);
private volatile DataBuffer currentBuffer;
private volatile boolean subscriberCompleted;
private Subscription subscription;
// Subscriber
@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);
}
// Publisher
@Override
public final void subscribe(Subscriber<? super Void> subscriber) {
this.publisherDelegate.subscribe(subscriber);
}
// 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.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;
}
/**
* Called when a {@link DataBuffer} is received via {@link Subscriber#onNext(Object)}
* or when only partial data from the {@link DataBuffer} was written.
*/
private void writeIfPossible() {
if (isWritePossible()) {
onWritePossible();
}
}
/**
* Called via a listener interface to determine whether writing is possible.
*/
protected boolean isWritePossible() {
return false;
}
/**
* 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;
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.
*
* <pre>
* UNSUBSCRIBED
* |
* v
* REQUESTED -------------------> RECEIVED
* ^ ^
* | |
* --------- WRITING <-----
* |
* v
* COMPLETED
* </pre>
* 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(AbstractResponseBodyProcessor processor,
Subscription subscription) {
Objects.requireNonNull(subscription, "Subscription cannot be null");
if (processor.changeState(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.changeState(this, RECEIVED)) {
processor.receiveBuffer(dataBuffer);
processor.writeIfPossible();
}
}
@Override
void onComplete(AbstractResponseBodyProcessor processor) {
if (processor.changeState(this, COMPLETED)) {
processor.subscriberCompleted = true;
processor.publisherDelegate.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.changeState(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.changeState(WRITING, REQUESTED);
processor.subscription.request(1);
}
else {
processor.changeState(WRITING, COMPLETED);
processor.publisherDelegate.publishComplete();
}
}
else {
processor.changeState(WRITING, RECEIVED);
processor.writeIfPossible();
}
}
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.changeState(this, COMPLETED)) {
processor.publisherDelegate.publishError(t);
}
}
void onComplete(AbstractResponseBodyProcessor processor) {
throw new IllegalStateException(toString());
}
void onWritePossible(AbstractResponseBodyProcessor processor) {
// ignore
}
}
}

View File

@ -0,0 +1,121 @@
/*
* 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 java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpHeaders;
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.
*
* @author Rossen Stoyanchev
*/
public abstract class AbstractServerHttpRequest implements ServerHttpRequest {
private static final Pattern QUERY_PATTERN = Pattern.compile("([^&=]+)(=?)([^&]+)?");
private URI uri;
private MultiValueMap<String, String> queryParams;
private HttpHeaders headers;
private MultiValueMap<String, HttpCookie> cookies;
@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 MultiValueMap<String, String> getQueryParams() {
if (this.queryParams == null) {
this.queryParams = CollectionUtils.unmodifiableMultiValueMap(initQueryParams());
}
return this.queryParams;
}
protected MultiValueMap<String, String> initQueryParams() {
MultiValueMap<String, String> 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) {
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();
@Override
public MultiValueMap<String, HttpCookie> getCookies() {
if (this.cookies == null) {
this.cookies = CollectionUtils.unmodifiableMultiValueMap(initCookies());
}
return this.cookies;
}
/**
* Initialize the cookies from the underlying request. Invoked lazily on the
* first access to cookies via {@link #getHeaders()} and then cached.
*/
protected abstract MultiValueMap<String, HttpCookie> initCookies();
}

View File

@ -0,0 +1,186 @@
/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS 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.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;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
/**
* Base class for {@link ServerHttpResponse} implementations.
*
* @author Rossen Stoyanchev
* @author Sebastien Deleuze
*/
public abstract class AbstractServerHttpResponse implements ServerHttpResponse {
private Log logger = LogFactory.getLog(getClass());
private static final int STATE_NEW = 1;
private static final int STATE_COMMITTING = 2;
private static final int STATE_COMMITTED = 3;
private final DataBufferFactory dataBufferFactory;
private HttpStatus statusCode;
private final HttpHeaders headers;
private final MultiValueMap<String, ResponseCookie> cookies;
private final List<Supplier<? extends Mono<Void>>> beforeCommitActions = new ArrayList<>(4);
private final AtomicInteger state = new AtomicInteger(STATE_NEW);
public AbstractServerHttpResponse(DataBufferFactory dataBufferFactory) {
Assert.notNull(dataBufferFactory, "'dataBufferFactory' must not be null");
this.dataBufferFactory = dataBufferFactory;
this.headers = new HttpHeaders();
this.cookies = new LinkedMultiValueMap<String, ResponseCookie>();
}
@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;
}
@Override
public HttpStatus getStatusCode() {
return this.statusCode;
}
@Override
public HttpHeaders getHeaders() {
if (STATE_COMMITTED == this.state.get()) {
return HttpHeaders.readOnlyHttpHeaders(this.headers);
}
else {
return this.headers;
}
}
@Override
public MultiValueMap<String, ResponseCookie> getCookies() {
if (STATE_COMMITTED == this.state.get()) {
return CollectionUtils.unmodifiableMultiValueMap(this.cookies);
}
return this.cookies;
}
@Override
public void beforeCommit(Supplier<? extends Mono<Void>> action) {
Assert.notNull(action);
this.beforeCommitActions.add(action);
}
@Override
public Mono<Void> writeWith(Publisher<DataBuffer> publisher) {
return new ChannelSendOperator<>(publisher, writePublisher ->
applyBeforeCommit().then(() -> writeWithInternal(writePublisher)));
}
protected Mono<Void> applyBeforeCommit() {
Mono<Void> mono = Mono.empty();
if (this.state.compareAndSet(STATE_NEW, STATE_COMMITTING)) {
for (Supplier<? extends Mono<Void>> action : this.beforeCommitActions) {
mono = mono.then(action);
}
mono = mono.otherwise(ex -> {
// Ignore errors from beforeCommit actions
return Mono.empty();
});
mono = mono.then(() -> {
this.state.set(STATE_COMMITTED);
writeStatusCode();
writeHeaders();
writeCookies();
return Mono.empty();
});
}
return mono;
}
/**
* Implement this method to write to the underlying the response.
* @param body the publisher to write with
*/
protected abstract Mono<Void> writeWithInternal(Publisher<DataBuffer> body);
/**
* 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.
*/
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();
@Override
public Mono<Void> setComplete() {
return applyBeforeCommit();
}
}

View File

@ -0,0 +1,241 @@
/*
* 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.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.EmptySubscription;
import org.springframework.util.Assert;
/**
* Given a write function that accepts a source {@code Publisher<T>} to write
* with and returns {@code Publisher<Void>} 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
* @author Stephane Maldini
*/
public class ChannelSendOperator<T> extends MonoSource<T, Void> {
private final Function<Publisher<T>, Publisher<Void>> writeFunction;
public ChannelSendOperator(Publisher<? extends T> source,
Function<Publisher<T>, Publisher<Void>> writeFunction) {
super(source);
this.writeFunction = writeFunction;
}
@Override
public void subscribe(Subscriber<? super Void> s) {
source.subscribe(new WriteWithBarrier(s));
}
private class WriteWithBarrier extends SubscriberBarrier<T, Void> implements Publisher<T> {
/**
* 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<? super T> writeSubscriber;
public WriteWithBarrier(Subscriber<? super Void> subscriber) {
super(subscriber);
}
@Override
protected void doOnSubscribe(Subscription subscription) {
super.doOnSubscribe(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(new DownstreamBridge(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(new DownstreamBridge(downstream()));
}
else {
this.completed = true;
}
}
}
@Override
public void subscribe(Subscriber<? super T> writeSubscriber) {
synchronized (this) {
Assert.isNull(this.writeSubscriber, "Only one writeSubscriber supported.");
this.writeSubscriber = writeSubscriber;
if (this.error != null || this.completed) {
this.writeSubscriber.onSubscribe(EmptySubscription.INSTANCE);
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 (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 class DownstreamBridge implements Subscriber<Void> {
private final Subscriber<? super Void> downstream;
public DownstreamBridge(Subscriber<? super Void> 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();
}
}
}

View File

@ -0,0 +1,37 @@
/*
* 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;
/**
* Contract for handling HTTP requests in a non-blocking way.
*
* @author Arjen Poutsma
*/
public interface HttpHandler {
/**
* Handle the given request and generate a response.
*
* @param request current HTTP request.
* @param response current HTTP response.
* @return {@code Mono<Void>} to indicate when request handling is complete.
*/
Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response);
}

View File

@ -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.server.reactive;
import io.netty.buffer.ByteBuf;
import reactor.core.publisher.Mono;
import reactor.io.ipc.ChannelHandler;
import reactor.io.netty.http.HttpChannel;
import org.springframework.core.io.buffer.NettyDataBufferFactory;
import org.springframework.util.Assert;
/**
* @author Stephane Maldini
*/
public class ReactorHttpHandlerAdapter
implements ChannelHandler<ByteBuf, ByteBuf, HttpChannel> {
private final HttpHandler httpHandler;
public ReactorHttpHandlerAdapter(HttpHandler httpHandler) {
Assert.notNull(httpHandler, "'httpHandler' is required.");
this.httpHandler = httpHandler;
}
@Override
public Mono<Void> apply(HttpChannel channel) {
NettyDataBufferFactory dataBufferFactory =
new NettyDataBufferFactory(channel.delegate().alloc());
ReactorServerHttpRequest adaptedRequest =
new ReactorServerHttpRequest(channel, dataBufferFactory);
ReactorServerHttpResponse adaptedResponse =
new ReactorServerHttpResponse(channel, dataBufferFactory);
return this.httpHandler.handle(adaptedRequest, adaptedResponse);
}
}

View File

@ -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.http.server.reactive;
import java.net.URI;
import java.net.URISyntaxException;
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.NettyDataBufferFactory;
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;
/**
* Adapt {@link ServerHttpRequest} to the Reactor Net {@link HttpChannel}.
*
* @author Stephane Maldini
*/
public class ReactorServerHttpRequest extends AbstractServerHttpRequest {
private final HttpChannel channel;
private final NettyDataBufferFactory dataBufferFactory;
public ReactorServerHttpRequest(HttpChannel request,
NettyDataBufferFactory dataBufferFactory) {
Assert.notNull("'request' must not be null");
Assert.notNull(dataBufferFactory, "'dataBufferFactory' must not be null");
this.channel = request;
this.dataBufferFactory = dataBufferFactory;
}
public HttpChannel getReactorChannel() {
return this.channel;
}
@Override
public HttpMethod getMethod() {
return HttpMethod.valueOf(this.channel.method().name());
}
@Override
protected URI initUri() throws URISyntaxException {
return new URI(this.channel.uri());
}
@Override
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 MultiValueMap<String, HttpCookie> initCookies() {
MultiValueMap<String, HttpCookie> 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
public Flux<DataBuffer> getBody() {
return this.channel.receive().retain().map(this.dataBufferFactory::wrap);
}
}

View File

@ -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.http.server.reactive;
import java.io.File;
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.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;
import org.springframework.http.ZeroCopyHttpOutputMessage;
import org.springframework.util.Assert;
/**
* Adapt {@link ServerHttpResponse} to the Reactor Net {@link HttpChannel}.
*
* @author Stephane Maldini
* @author Rossen Stoyanchev
*/
public class ReactorServerHttpResponse extends AbstractServerHttpResponse
implements ZeroCopyHttpOutputMessage {
private final HttpChannel channel;
public ReactorServerHttpResponse(HttpChannel response,
DataBufferFactory dataBufferFactory) {
super(dataBufferFactory);
Assert.notNull("'response' must not be null.");
this.channel = response;
}
public HttpChannel getReactorChannel() {
return this.channel;
}
@Override
protected void writeStatusCode() {
HttpStatus statusCode = this.getStatusCode();
if (statusCode != null) {
getReactorChannel().status(HttpResponseStatus.valueOf(statusCode.value()));
}
}
@Override
protected Mono<Void> writeWithInternal(Publisher<DataBuffer> publisher) {
return Flux.from(publisher)
.window()
.concatMap(w -> this.channel.send(w
.takeUntil(db -> db instanceof FlushingDataBuffer)
.map(this::toByteBuf)))
.then();
}
@Override
protected void writeHeaders() {
for (String name : getHeaders().keySet()) {
for (String value : getHeaders().get(name)) {
this.channel.responseHeaders().add(name, value);
}
}
}
@Override
protected void writeCookies() {
for (String name : getCookies().keySet()) {
for (ResponseCookie httpCookie : getCookies().get(name)) {
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(cookie);
}
}
}
private ByteBuf toByteBuf(DataBuffer buffer) {
if (buffer instanceof NettyDataBuffer) {
return ((NettyDataBuffer) buffer).getNativeBuffer();
}
else {
return Unpooled.wrappedBuffer(buffer.asByteBuffer());
}
}
@Override
public Mono<Void> writeWith(File file, long position, long count) {
return applyBeforeCommit().then(() -> {
return this.channel.sendFile(file, position, count);
});
}
}

View File

@ -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<Void> {
private static final Log logger =
LogFactory.getLog(ResponseBodyWriteResultPublisher.class);
private final AtomicReference<State> state =
new AtomicReference<>(State.UNSUBSCRIBED);
private Subscriber<? super Void> subscriber;
private volatile boolean publisherCompleted;
private volatile Throwable publisherError;
@Override
public final void subscribe(Subscriber<? super Void> 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<? super Void> 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<? super Void> 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());
}
}
}

View File

@ -0,0 +1,55 @@
/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS 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 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 reactor.core.converter.RxJava1ObservableConverter;
import rx.Observable;
import org.springframework.core.io.buffer.NettyDataBufferFactory;
import org.springframework.util.Assert;
/**
* @author Rossen Stoyanchev
*/
public class RxNettyHttpHandlerAdapter implements RequestHandler<ByteBuf, ByteBuf> {
private final HttpHandler httpHandler;
public RxNettyHttpHandlerAdapter(HttpHandler httpHandler) {
Assert.notNull(httpHandler, "'httpHandler' is required");
this.httpHandler = httpHandler;
}
@Override
public Observable<Void> handle(HttpServerRequest<ByteBuf> request, HttpServerResponse<ByteBuf> response) {
NettyDataBufferFactory dataBufferFactory =
new NettyDataBufferFactory(response.unsafeNettyChannel().alloc());
RxNettyServerHttpRequest adaptedRequest =
new RxNettyServerHttpRequest(request, dataBufferFactory);
RxNettyServerHttpResponse adaptedResponse =
new RxNettyServerHttpResponse(response, dataBufferFactory);
Publisher<Void> result = this.httpHandler.handle(adaptedRequest, adaptedResponse);
return RxJava1ObservableConverter.fromPublisher(result);
}
}

View File

@ -0,0 +1,100 @@
/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS 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 io.netty.buffer.ByteBuf;
import io.netty.handler.codec.http.cookie.Cookie;
import io.reactivex.netty.protocol.http.server.HttpServerRequest;
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.NettyDataBufferFactory;
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;
/**
* Adapt {@link ServerHttpRequest} to the RxNetty {@link HttpServerRequest}.
*
* @author Rossen Stoyanchev
* @author Stephane Maldini
*/
public class RxNettyServerHttpRequest extends AbstractServerHttpRequest {
private final HttpServerRequest<ByteBuf> request;
private final NettyDataBufferFactory dataBufferFactory;
public RxNettyServerHttpRequest(HttpServerRequest<ByteBuf> request,
NettyDataBufferFactory dataBufferFactory) {
Assert.notNull("'request', request must not be null");
Assert.notNull(dataBufferFactory, "'dataBufferFactory' must not be null");
this.dataBufferFactory = dataBufferFactory;
this.request = request;
}
public HttpServerRequest<ByteBuf> getRxNettyRequest() {
return this.request;
}
@Override
public HttpMethod getMethod() {
return HttpMethod.valueOf(this.request.getHttpMethod().name());
}
@Override
protected URI initUri() throws URISyntaxException {
return new URI(this.request.getUri());
}
@Override
protected HttpHeaders initHeaders() {
HttpHeaders headers = new HttpHeaders();
for (String name : this.request.getHeaderNames()) {
headers.put(name, this.request.getAllHeaderValues(name));
}
return headers;
}
@Override
protected MultiValueMap<String, HttpCookie> initCookies() {
MultiValueMap<String, HttpCookie> 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
public Flux<DataBuffer> getBody() {
Observable<DataBuffer> content = this.request.getContent().map(dataBufferFactory::wrap);
return RxJava1ObservableConverter.toPublisher(content);
}
}

View File

@ -0,0 +1,147 @@
/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS 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 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;
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 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;
import org.springframework.http.ResponseCookie;
import org.springframework.util.Assert;
/**
* Adapt {@link ServerHttpResponse} to the RxNetty {@link HttpServerResponse}.
*
* @author Rossen Stoyanchev
* @author Stephane Maldini
*/
public class RxNettyServerHttpResponse extends AbstractServerHttpResponse {
private final HttpServerResponse<ByteBuf> response;
public RxNettyServerHttpResponse(HttpServerResponse<ByteBuf> response,
NettyDataBufferFactory dataBufferFactory) {
super(dataBufferFactory);
Assert.notNull("'response', response must not be null.");
this.response = response;
}
public HttpServerResponse<?> getRxNettyResponse() {
return this.response;
}
@Override
protected void writeStatusCode() {
HttpStatus statusCode = this.getStatusCode();
if (statusCode != null) {
this.response.setStatus(HttpResponseStatus.valueOf(statusCode.value()));
}
}
@Override
protected Mono<Void> writeWithInternal(Publisher<DataBuffer> body) {
Observable<ByteBuf> content = RxJava1ObservableConverter.fromPublisher(body).map(this::toByteBuf);
return RxJava1ObservableConverter.toPublisher(this.response.write(content, bb -> bb instanceof FlushingByteBuf)).then();
}
private ByteBuf toByteBuf(DataBuffer buffer) {
ByteBuf byteBuf = (buffer instanceof NettyDataBuffer ? ((NettyDataBuffer) buffer).getNativeBuffer() : Unpooled.wrappedBuffer(buffer.asByteBuffer()));
return (buffer instanceof FlushingDataBuffer ? new FlushingByteBuf(byteBuf) : byteBuf);
}
@Override
protected void writeHeaders() {
for (String name : getHeaders().keySet()) {
for (String value : getHeaders().get(name))
this.response.addHeader(name, value);
}
}
@Override
protected void writeCookies() {
for (String name : getCookies().keySet()) {
for (ResponseCookie httpCookie : getCookies().get(name)) {
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.response.addCookie(cookie);
}
}
}
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
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<Void> writeWith(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<String, List<String>> header : getHeaders().entrySet()) {
String headerName = header.getKey();
for (String headerValue : header.getValue()) {
headers.add(headerName, headerValue);
}
}
Mono<Void> responseWrite = MonoChannelFuture.from(channel.write(httpResponse));
FileRegion fileRegion = new DefaultFileRegion(file, position, count);
Mono<Void> fileWrite = MonoChannelFuture.from(channel.writeAndFlush(fileRegion));
return Flux.concat(applyBeforeCommit(), responseWrite, fileWrite).then();
}
*/
}

View File

@ -0,0 +1,41 @@
/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS 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.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
*
* @author Arjen Poutsma
*/
public interface ServerHttpRequest extends HttpRequest, ReactiveHttpInputMessage {
/**
* Return a read-only map with parsed and decoded query parameter values.
*/
MultiValueMap<String, String> getQueryParams();
/**
* Return a read-only map of cookies sent by the client.
*/
MultiValueMap<String, HttpCookie> getCookies();
}

View File

@ -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.server.reactive;
import reactor.core.publisher.Mono;
import org.springframework.http.HttpStatus;
import org.springframework.http.ReactiveHttpOutputMessage;
import org.springframework.http.ResponseCookie;
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.
*/
boolean setStatusCode(HttpStatus status);
/**
* 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<String, ResponseCookie> getCookies();
/**
* 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).
* <p>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<Void> setComplete();
}

View File

@ -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.server.reactive;
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.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.util.Assert;
/**
* @author Arjen Poutsma
* @author Rossen Stoyanchev
*/
@WebServlet(asyncSupported = true)
public class ServletHttpHandlerAdapter extends HttpServlet {
private static final int DEFAULT_BUFFER_SIZE = 8192;
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
// (i.e. 'false' as constructor argument)
private DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory(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 setDataBufferFactory(DataBufferFactory dataBufferFactory) {
Assert.notNull(dataBufferFactory, "'dataBufferFactory' must not be null");
this.dataBufferFactory = dataBufferFactory;
}
public void setBufferSize(int bufferSize) {
Assert.isTrue(bufferSize > 0);
this.bufferSize = bufferSize;
}
@Override
protected void service(HttpServletRequest servletRequest,
HttpServletResponse servletResponse) throws ServletException, IOException {
AsyncContext asyncContext = servletRequest.startAsync();
ServletServerHttpRequest request =
new ServletServerHttpRequest(servletRequest, this.dataBufferFactory,
this.bufferSize);
ServletServerHttpResponse response =
new ServletServerHttpResponse(servletResponse, this.dataBufferFactory,
this.bufferSize);
HandlerResultSubscriber resultSubscriber =
new HandlerResultSubscriber(asyncContext);
this.handler.handle(request, response).subscribe(resultSubscriber);
}
private static class HandlerResultSubscriber implements Subscriber<Void> {
private final AsyncContext asyncContext;
public HandlerResultSubscriber(AsyncContext asyncContext) {
this.asyncContext = asyncContext;
}
@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);
HttpServletResponse response =
(HttpServletResponse) this.asyncContext.getResponse();
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
this.asyncContext.complete();
}
@Override
public void onComplete() {
this.asyncContext.complete();
}
}
}

View File

@ -0,0 +1,236 @@
/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS 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.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 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;
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;
/**
* Adapt {@link ServerHttpRequest} to the Servlet {@link HttpServletRequest}.
* @author Rossen Stoyanchev
*/
public class ServletServerHttpRequest extends AbstractServerHttpRequest {
private final Object bodyPublisherMonitor = new Object();
private volatile RequestBodyPublisher bodyPublisher;
private final HttpServletRequest request;
private final DataBufferFactory dataBufferFactory;
private final int bufferSize;
public ServletServerHttpRequest(HttpServletRequest request,
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.dataBufferFactory = dataBufferFactory;
this.bufferSize = bufferSize;
}
public HttpServletRequest getServletRequest() {
return this.request;
}
@Override
public HttpMethod getMethod() {
return HttpMethod.valueOf(getServletRequest().getMethod());
}
@Override
protected URI initUri() throws URISyntaxException {
StringBuffer url = this.request.getRequestURL();
String query = this.request.getQueryString();
if (StringUtils.hasText(query)) {
url.append('?').append(query);
}
return new URI(url.toString());
}
@Override
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());
}
}
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<String, String> 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
protected MultiValueMap<String, HttpCookie> initCookies() {
MultiValueMap<String, HttpCookie> httpCookies = new LinkedMultiValueMap<>();
Cookie[] cookies = this.request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
String name = cookie.getName();
HttpCookie httpCookie = new HttpCookie(name, cookie.getValue());
httpCookies.add(name, httpCookie);
}
}
return httpCookies;
}
@Override
public Flux<DataBuffer> getBody() {
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);
}
}
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 {
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);
}
}
}
}

View File

@ -0,0 +1,244 @@
/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS 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.io.InputStream;
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.Cookie;
import javax.servlet.http.HttpServletResponse;
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.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseCookie;
import org.springframework.util.Assert;
/**
* Adapt {@link ServerHttpResponse} to the Servlet {@link HttpServletResponse}.
* @author Rossen Stoyanchev
*/
public class ServletServerHttpResponse extends AbstractServerHttpResponse {
private final Object bodyProcessorMonitor = new Object();
private volatile ResponseBodyProcessor bodyProcessor;
private final HttpServletResponse response;
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.bufferSize = bufferSize;
}
public HttpServletResponse getServletResponse() {
return this.response;
}
@Override
protected void writeStatusCode() {
HttpStatus statusCode = this.getStatusCode();
if (statusCode != null) {
getServletResponse().setStatus(statusCode.value());
}
}
@Override
protected Mono<Void> writeWithInternal(Publisher<DataBuffer> publisher) {
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
protected void writeHeaders() {
for (Map.Entry<String, List<String>> entry : getHeaders().entrySet()) {
String headerName = entry.getKey();
for (String headerValue : entry.getValue()) {
this.response.addHeader(headerName, headerValue);
}
}
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());
}
}
@Override
protected void writeCookies() {
for (String name : getCookies().keySet()) {
for (ResponseCookie httpCookie : getCookies().get(name)) {
Cookie cookie = new Cookie(name, httpCookie.getValue());
if (!httpCookie.getMaxAge().isNegative()) {
cookie.setMaxAge((int) httpCookie.getMaxAge().getSeconds());
}
httpCookie.getDomain().ifPresent(cookie::setDomain);
httpCookie.getPath().ifPresent(cookie::setPath);
cookie.setSecure(httpCookie.isSecure());
cookie.setHttpOnly(httpCookie.isHttpOnly());
this.response.addCookie(cookie);
}
}
}
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);
}
}
}
}

View File

@ -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.http.server.reactive;
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.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 {
private static Log logger = LogFactory.getLog(UndertowHttpHandlerAdapter.class);
private final HttpHandler delegate;
private final DataBufferFactory dataBufferFactory;
public UndertowHttpHandlerAdapter(HttpHandler delegate,
DataBufferFactory dataBufferFactory) {
Assert.notNull(delegate, "'delegate' is required");
Assert.notNull(dataBufferFactory, "'dataBufferFactory' must not be null");
this.delegate = delegate;
this.dataBufferFactory = dataBufferFactory;
}
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
ServerHttpRequest request =
new UndertowServerHttpRequest(exchange, this.dataBufferFactory);
ServerHttpResponse response =
new UndertowServerHttpResponse(exchange, this.dataBufferFactory);
this.delegate.handle(request, response).subscribe(new Subscriber<Void>() {
@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);
if (!exchange.isResponseStarted() && exchange.getStatusCode() <= 500) {
exchange.setStatusCode(500);
}
exchange.endExchange();
}
@Override
public void onComplete() {
exchange.endExchange();
}
});
}
}

View File

@ -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.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.xnio.ChannelListener;
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;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
/**
* Adapt {@link ServerHttpRequest} to the Underow {@link HttpServerExchange}.
*
* @author Marek Hawrylczak
* @author Rossen Stoyanchev
*/
public class UndertowServerHttpRequest extends AbstractServerHttpRequest {
private final HttpServerExchange exchange;
private final RequestBodyPublisher body;
public UndertowServerHttpRequest(HttpServerExchange exchange,
DataBufferFactory dataBufferFactory) {
Assert.notNull(exchange, "'exchange' is required.");
this.exchange = exchange;
this.body = new RequestBodyPublisher(exchange, dataBufferFactory);
this.body.registerListener();
}
public HttpServerExchange getUndertowExchange() {
return this.exchange;
}
@Override
public HttpMethod getMethod() {
return HttpMethod.valueOf(this.getUndertowExchange().getRequestMethod().toString());
}
@Override
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
protected HttpHeaders initHeaders() {
HttpHeaders headers = new HttpHeaders();
for (HeaderValues values : this.getUndertowExchange().getRequestHeaders()) {
headers.put(values.getHeaderName().toString(), values);
}
return headers;
}
@Override
protected MultiValueMap<String, HttpCookie> initCookies() {
MultiValueMap<String, HttpCookie> 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
public Flux<DataBuffer> getBody() {
return Flux.from(this.body);
}
private static class RequestBodyPublisher extends AbstractRequestBodyPublisher {
private final ChannelListener<StreamSourceChannel> readListener =
new ReadListener();
private final ChannelListener<StreamSourceChannel> 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 void checkOnDataAvailable() {
onDataAvailable();
}
@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;
}
private class ReadListener implements ChannelListener<StreamSourceChannel> {
@Override
public void handleEvent(StreamSourceChannel channel) {
onDataAvailable();
}
}
private class CloseListener implements ChannelListener<StreamSourceChannel> {
@Override
public void handleEvent(StreamSourceChannel channel) {
onAllDataRead();
}
}
}
}

View File

@ -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.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 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.channels.StreamSinkChannel;
import reactor.core.publisher.Mono;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseCookie;
import org.springframework.http.ZeroCopyHttpOutputMessage;
import org.springframework.util.Assert;
/**
* Adapt {@link ServerHttpResponse} to the Undertow {@link HttpServerExchange}.
* @author Marek Hawrylczak
* @author Rossen Stoyanchev
* @author Arjen Poutsma
*/
public class UndertowServerHttpResponse extends AbstractServerHttpResponse
implements ZeroCopyHttpOutputMessage {
private final Object bodyProcessorMonitor = new Object();
private volatile ResponseBodyProcessor bodyProcessor;
private final HttpServerExchange exchange;
public UndertowServerHttpResponse(HttpServerExchange exchange,
DataBufferFactory dataBufferFactory) {
super(dataBufferFactory);
Assert.notNull(exchange, "'exchange' is required.");
this.exchange = exchange;
}
public HttpServerExchange getUndertowExchange() {
return this.exchange;
}
@Override
protected void writeStatusCode() {
HttpStatus statusCode = this.getStatusCode();
if (statusCode != null) {
getUndertowExchange().setStatusCode(statusCode.value());
}
}
@Override
protected Mono<Void> writeWithInternal(Publisher<DataBuffer> publisher) {
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<Void> writeWith(File file, long position, long count) {
writeHeaders();
writeCookies();
try {
StreamSinkChannel responseChannel =
getUndertowExchange().getResponseChannel();
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"));
}
else {
return Mono.empty();
}
}
catch (IOException ex) {
return Mono.error(ex);
}
}
@Override
protected void writeHeaders() {
for (Map.Entry<String, List<String>> entry : getHeaders().entrySet()) {
HttpString headerName = HttpString.tryFromString(entry.getKey());
this.exchange.getResponseHeaders().addAll(headerName, entry.getValue());
}
}
@Override
protected void writeCookies() {
for (String name : getCookies().keySet()) {
for (ResponseCookie httpCookie : getCookies().get(name)) {
Cookie cookie = new CookieImpl(name, httpCookie.getValue());
if (!httpCookie.getMaxAge().isNegative()) {
cookie.setMaxAge((int) httpCookie.getMaxAge().getSeconds());
}
httpCookie.getDomain().ifPresent(cookie::setDomain);
httpCookie.getPath().ifPresent(cookie::setPath);
cookie.setSecure(httpCookie.isSecure());
cookie.setHttpOnly(httpCookie.isHttpOnly());
this.exchange.getResponseCookies().putIfAbsent(name, cookie);
}
}
}
private static class ResponseBodyProcessor extends AbstractResponseBodyProcessor {
private final ChannelListener<StreamSinkChannel> listener = new WriteListener();
private final StreamSinkChannel responseChannel;
private volatile ByteBuffer byteBuffer;
public ResponseBodyProcessor(HttpServerExchange exchange) {
this.responseChannel = exchange.getResponseChannel();
}
public void registerListener() {
this.responseChannel.getWriteSetter().set(this.listener);
this.responseChannel.resumeWrites();
}
@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;
}
private class WriteListener implements ChannelListener<StreamSinkChannel> {
@Override
public void handleEvent(StreamSinkChannel channel) {
onWritePossible();
}
}
}
}

View File

@ -0,0 +1,4 @@
/**
* Core package of the reactive server-side HTTP support.
*/
package org.springframework.http.server.reactive;

View File

@ -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<MediaType> toMediaTypes(List<MimeType> 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());
}
}

View File

@ -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<MimeType> getMimeType(String filename) {
if (filename != null) {
String mimeType = fileTypeMap.getContentType(filename);
if (StringUtils.hasText(mimeType)) {
return Optional.of(parseMimeType(mimeType));
}
}
return Optional.empty();
}
}

View File

@ -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.util.xml;
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.ClassUtils;
/**
* Abstract base class for {@code XMLEventReader}s.
* @author Arjen Poutsma
*/
abstract class AbstractXMLEventReader implements XMLEventReader {
private boolean closed;
@Override
public Object next() {
try {
return nextEvent();
}
catch (XMLStreamException ex) {
throw new NoSuchElementException();
}
}
@Override
public void remove() {
throw new UnsupportedOperationException(
"remove not supported on " + ClassUtils.getShortName(getClass()));
}
@Override
public String getElementText() throws XMLStreamException {
checkIfClosed();
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 {
checkIfClosed();
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 + "]");
}
/**
* 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;
}
}

View File

@ -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<XMLEvent> events;
private int cursor = 0;
public ListBasedXMLEventReader(List<XMLEvent> 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();
}
}

View File

@ -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<XMLEvent> events) {
return new ListBasedXMLEventReader(events);
}
}

View File

@ -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.
* <p>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<DataBuffer>} 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<String, HttpCookie> 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<String, HttpCookie> getCookies() {
return cookies;
}
public void setCookies(MultiValueMap<String, HttpCookie> 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;
}
}

View File

@ -0,0 +1,28 @@
/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS 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;
/**
* Build {@link ClientWebRequest}s
*
* @author Brian Clozel
*/
public interface ClientWebRequestBuilder {
ClientWebRequest build();
}

View File

@ -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);
}
}

View File

@ -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;
/**
* Allow post processing and/or wrapping the {@link ClientWebRequest} before
* it's sent to the origin server.
*
* @author Rob Winch
* @author Brian Clozel
* @see DefaultClientWebRequestBuilder#apply(ClientWebRequestPostProcessor)
*/
public interface ClientWebRequestPostProcessor {
/**
* Implementations can modify and/or wrap the {@link ClientWebRequest} passed in
* and return it
*
* @param request the {@link ClientWebRequest} to be modified and/or wrapped.
*/
ClientWebRequest postProcess(ClientWebRequest request);
}

View File

@ -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.
*
* <p>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<String, HttpCookie> cookies = new LinkedMultiValueMap<>();
private Publisher body;
private ResolvableType elementType;
private List<ClientWebRequestPostProcessor> 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;
}
}

View File

@ -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.web.client.reactive;
import java.util.List;
import reactor.core.publisher.Mono;
import org.springframework.http.client.reactive.ClientHttpResponse;
import org.springframework.http.converter.reactive.HttpMessageConverter;
/**
* 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.
*
* <p>See static factory methods in {@link ResponseExtractors}.
*
* @author Brian Clozel
*/
public interface ResponseExtractor<T> {
T extract(Mono<ClientHttpResponse> clientResponse, List<HttpMessageConverter<?>> messageConverters);
}

View File

@ -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<T>}
* @see ResolvableType#forClassWithGenerics(Class, Class[])
*/
public static <T> ResponseExtractor<Mono<T>> body(ResolvableType bodyType) {
// noinspection unchecked
return (clientResponse, messageConverters) -> (Mono<T>) clientResponse
.flatMap(resp -> decodeResponseBody(resp, bodyType,
messageConverters))
.next();
}
/**
* Extract the response body and decode it, returning it as a {@code Mono<T>}
*/
public static <T> ResponseExtractor<Mono<T>> body(Class<T> sourceClass) {
ResolvableType bodyType = ResolvableType.forClass(sourceClass);
return body(bodyType);
}
/**
* Extract the response body and decode it, returning it as a {@code Flux<T>}
* @see ResolvableType#forClassWithGenerics(Class, Class[])
*/
public static <T> ResponseExtractor<Flux<T>> 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<T>}
*/
public static <T> ResponseExtractor<Flux<T>> bodyStream(Class<T> 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 <T> ResponseExtractor<Mono<ResponseEntity<T>>> 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 <T> ResponseExtractor<Mono<ResponseEntity<T>>> response(
Class<T> 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<T>}
* @see ResolvableType#forClassWithGenerics(Class, Class[])
*/
public static <T> ResponseExtractor<Mono<ResponseEntity<Flux<T>>>> 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<T>}
*/
public static <T> ResponseExtractor<Mono<ResponseEntity<Flux<T>>>> responseStream(
Class<T> sourceClass) {
ResolvableType resolvableType = ResolvableType.forClass(sourceClass);
return responseStream(resolvableType);
}
/**
* Extract the response headers as an {@code HttpHeaders} instance
*/
public static ResponseExtractor<Mono<HttpHeaders>> headers() {
return (clientResponse, messageConverters) -> clientResponse.map(resp -> resp.getHeaders());
}
protected static <T> Flux<T> decodeResponseBody(ClientHttpResponse response,
ResolvableType responseType,
List<HttpMessageConverter<?>> messageConverters) {
MediaType contentType = response.getHeaders().getContentType();
Optional<HttpMessageConverter<?>> 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<T>) converter.get().read(responseType, response);
}
protected static Optional<HttpMessageConverter<?>> resolveConverter(
List<HttpMessageConverter<?>> messageConverters, ResolvableType type,
MediaType mediaType) {
return messageConverters.stream().filter(e -> e.canRead(type, mediaType))
.findFirst();
}
}

View File

@ -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.client.reactive;
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 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;
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.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.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
*
* <p>Here is a simple example of a GET request:
*
* <pre class="code">
* // should be shared between HTTP calls
* WebClient client = new WebClient(new ReactorHttpClient());
*
* Mono&lt;String&gt; result = client
* .perform(ClientWebRequestBuilders.get("http://example.org/resource")
* .accept(MediaType.TEXT_PLAIN))
* .extract(ResponseExtractors.body(String.class));
* </pre>
*
* <p>This Web client relies on
* <ul>
* <li>an {@link ClientHttpConnector} implementation that drives the underlying library (e.g. Reactor-Netty)</li>
* <li>a {@link ClientWebRequestBuilder} which creates a Web request with a builder API (see
* {@link ClientWebRequestBuilders})</li>
* <li>an {@link ResponseExtractor} which extracts the relevant part of the server
* response with the composition API of choice (see {@link ResponseExtractors}</li>
* </ul>
*
* @author Brian Clozel
* @see ClientWebRequestBuilders
* @see ResponseExtractors
*/
public final class WebClient {
private static final ClassLoader classLoader = WebClient.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 ClientHttpConnector clientHttpConnector;
private List<HttpMessageConverter<?>> messageConverters;
/**
* 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:
* <ul>
* <li>{@link ByteBufferEncoder} / {@link ByteBufferDecoder}</li>
* <li>{@link StringEncoder} / {@link StringDecoder}</li>
* <li>{@link Jaxb2Encoder} / {@link Jaxb2Decoder}</li>
* <li>{@link JacksonJsonEncoder} / {@link JacksonJsonDecoder}</li>
* </ul>
*
* @param clientHttpConnector the {@code ClientHttpRequestFactory} to use
*/
public WebClient(ClientHttpConnector clientHttpConnector) {
this.clientHttpConnector = clientHttpConnector;
this.messageConverters = new ArrayList<>();
addDefaultHttpMessageConverters(this.messageConverters);
}
/**
* Adds default HTTP message converters
*/
protected final void addDefaultHttpMessageConverters(
List<HttpMessageConverter<?>> 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 <T> HttpMessageConverter<T> converter(Encoder<T> encoder,
Decoder<T> decoder) {
return new CodecHttpMessageConverter<>(encoder, decoder);
}
/**
* Set the list of {@link HttpMessageConverter}s to use for encoding and decoding HTTP
* messages
*/
public void setMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
this.messageConverters = messageConverters;
}
/**
* Perform the actual HTTP request/response exchange
*
* <p>
* Requesting from the exposed {@code Flux} will result in:
* <ul>
* <li>building the actual HTTP request using the provided {@code ClientWebRequestBuilder}</li>
* <li>encoding the HTTP request body with the configured {@code HttpMessageConverter}s</li>
* <li>returning the response with a publisher of the body</li>
* </ul>
*/
public WebResponseActions perform(ClientWebRequestBuilder builder) {
ClientWebRequest clientWebRequest = builder.build();
final Mono<ClientHttpResponse> 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<HttpStatus> consumer) {
clientResponse.doOnNext(clientHttpResponse ->
consumer.accept(clientHttpResponse.getStatusCode()));
}
@Override
public <T> T extract(ResponseExtractor<T> extractor) {
return extractor.extract(clientResponse, messageConverters);
}
};
}
protected class DefaultRequestCallback implements Function<ClientHttpRequest, Mono<Void>> {
private final ClientWebRequest clientWebRequest;
public DefaultRequestCallback(ClientWebRequest clientWebRequest) {
this.clientWebRequest = clientWebRequest;
}
@Override
public Mono<Void> 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<Void> writeRequestBody(Publisher<?> content,
ResolvableType requestType, ClientHttpRequest request,
List<HttpMessageConverter<?>> messageConverters) {
MediaType contentType = request.getHeaders().getContentType();
Optional<HttpMessageConverter<?>> 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<HttpMessageConverter<?>> resolveConverter(
List<HttpMessageConverter<?>> messageConverters, ResolvableType type,
MediaType mediaType) {
return messageConverters.stream().filter(e -> e.canWrite(type, mediaType)).findFirst();
}
}
}

View File

@ -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);
}
}

View File

@ -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<HttpStatus> consumer);
/**
* Perform an extraction of the response body into a higher level representation.
*
* <pre class="code">
* static imports: HttpRequestBuilders.*, HttpResponseExtractors.*
*
* webClient
* .perform(get(baseUrl.toString()).accept(MediaType.TEXT_PLAIN))
* .extract(response(String.class));
* </pre>
*/
<T> T extract(ResponseExtractor<T> extractor);
}

View File

@ -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.
*
* <p>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();
}
}

View File

@ -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);
}
}

View File

@ -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<T>}
*/
public static <T> ResponseExtractor<Single<T>> body(Class<T> sourceClass) {
ResolvableType resolvableType = ResolvableType.forClass(sourceClass);
//noinspection unchecked
return (clientResponse, messageConverters) -> (Single<T>) RxJava1SingleConverter
.fromPublisher(clientResponse
.flatMap(resp -> decodeResponseBody(resp, resolvableType, messageConverters)).next());
}
/**
* Extract the response body and decode it, returning it as an {@code Observable<T>}
*/
public static <T> ResponseExtractor<Observable<T>> bodyStream(Class<T> 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 <T> ResponseExtractor<Single<ResponseEntity<T>>> response(Class<T> sourceClass) {
ResolvableType resolvableType = ResolvableType.forClass(sourceClass);
return (clientResponse, messageConverters) -> (Single<ResponseEntity<T>>)
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<T>}
*/
public static <T> ResponseExtractor<Single<ResponseEntity<Observable<T>>>> responseStream(Class<T> 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<Single<HttpHeaders>> headers() {
return (clientResponse, messageConverters) -> RxJava1SingleConverter
.fromPublisher(clientResponse.map(resp -> resp.getHeaders()));
}
protected static <T> Flux<T> decodeResponseBody(ClientHttpResponse response, ResolvableType responseType,
List<HttpMessageConverter<?>> messageConverters) {
MediaType contentType = response.getHeaders().getContentType();
Optional<HttpMessageConverter<?>> 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<T>) converter.get().read(responseType, response);
}
protected static Optional<HttpMessageConverter<?>> resolveConverter(List<HttpMessageConverter<?>> messageConverters,
ResolvableType type, MediaType mediaType) {
return messageConverters.stream().filter(e -> e.canRead(type, mediaType)).findFirst();
}
}

Some files were not shown because too many files have changed in this diff Show More