Merge spring-reactive
This commit is contained in:
commit
934bc4f953
|
@ -0,0 +1,10 @@
|
|||
target
|
||||
.project
|
||||
.classpath
|
||||
.settings
|
||||
*.iml
|
||||
/.idea/
|
||||
bin
|
||||
.gradle
|
||||
/tomcat.*/
|
||||
build
|
|
@ -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
|
|
@ -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"
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
version=0.1.0.BUILD-SNAPSHOT
|
||||
|
Binary file not shown.
|
@ -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
|
|
@ -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 "$@"
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
rootProject.name = "spring-reactive"
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
||||
}
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
|
||||
}
|
|
@ -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();
|
||||
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
/**
|
||||
* Default implementation of the type conversion system.
|
||||
*/
|
||||
package org.springframework.core.convert.support;
|
|
@ -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();
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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 + ")";
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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()));
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
|
||||
}
|
|
@ -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();
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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}
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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();
|
||||
|
||||
}
|
|
@ -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();
|
||||
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
|
@ -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() +
|
||||
'}';
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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;
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
*/
|
||||
}
|
|
@ -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();
|
||||
|
||||
}
|
|
@ -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();
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
/**
|
||||
* Core package of the reactive server-side HTTP support.
|
||||
*/
|
||||
package org.springframework.http.server.reactive;
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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<String> 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
Loading…
Reference in New Issue