initial commit

This commit is contained in:
Jörg Prante 2017-05-02 00:52:09 +02:00
commit 1e1b8469b2
43 changed files with 5262 additions and 0 deletions

12
.gitignore vendored Normal file
View file

@ -0,0 +1,12 @@
/data
/work
/logs
/.idea
/target
.DS_Store
/.settings
/.classpath
/.project
/.gradle
/build
*~

202
LICENSE.txt Normal file
View file

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

121
build.gradle Normal file
View file

@ -0,0 +1,121 @@
plugins {
id "org.sonarqube" version "2.2"
id "org.xbib.gradle.plugin.asciidoctor" version "1.5.4.1.0"
id "io.codearte.nexus-staging" version "0.7.0"
}
printf "Host: %s\nOS: %s %s %s\nJVM: %s %s %s %s\nGroovy: %s\nGradle: %s\n" +
"Build: group: ${project.group} name: ${project.name} version: ${project.version}\n",
InetAddress.getLocalHost(),
System.getProperty("os.name"),
System.getProperty("os.arch"),
System.getProperty("os.version"),
System.getProperty("java.version"),
System.getProperty("java.vm.version"),
System.getProperty("java.vm.vendor"),
System.getProperty("java.vm.name"),
GroovySystem.getVersion(),
gradle.gradleVersion
apply plugin: 'java'
apply plugin: 'maven'
apply plugin: 'signing'
apply plugin: 'findbugs'
apply plugin: 'pmd'
apply plugin: 'checkstyle'
apply plugin: "jacoco"
apply plugin: 'org.xbib.gradle.plugin.asciidoctor'
apply plugin: "io.codearte.nexus-staging"
repositories {
mavenCentral()
}
configurations {
alpnagent
asciidoclet
wagon
}
dependencies {
compile "io.netty:netty-codec-http2:${project.property('netty.version')}"
compile "io.netty:netty-handler-proxy:${project.property('netty.version')}"
compile "io.netty:netty-tcnative-boringssl-static:${project.property('tcnative.version')}"
alpnagent "org.mortbay.jetty.alpn:jetty-alpn-agent:${project.property('alpnagent.version')}"
testCompile "junit:junit:${project.property('junit.version')}"
asciidoclet "org.asciidoctor:asciidoclet:${project.property('asciidoclet.version')}"
wagon "org.apache.maven.wagon:wagon-ssh:${project.property('wagon.version')}"
}
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
[compileJava, compileTestJava]*.options*.encoding = 'UTF-8'
tasks.withType(JavaCompile) {
options.compilerArgs << "-Xlint:all"
}
jar {
manifest {
attributes('Implementation-Version': project.version)
}
}
test {
jvmArgs "-javaagent:" + configurations.alpnagent.asPath
//include 'org/xbib/netty/http/client/test/Http2Test*'
testLogging {
showStandardStreams = false
exceptionFormat = 'full'
}
}
asciidoctor {
backends 'html5'
separateOutputDirs = false
attributes 'source-highlighter': 'coderay',
toc : '',
idprefix : '',
idseparator : '-',
stylesheet: "${projectDir}/src/docs/asciidoc/css/foundation.css"
}
javadoc {
options.docletpath = configurations.asciidoclet.files.asType(List)
options.doclet = 'org.asciidoctor.Asciidoclet'
options.overview = "src/docs/asciidoclet/overview.adoc"
options.addStringOption "-base-dir", "${projectDir}"
options.addStringOption "-attribute",
"name=${project.name},version=${project.version},title-link=https://github.com/jprante/${project.name}"
configure(options) {
noTimestamp = true
}
}
task javadocJar(type: Jar, dependsOn: classes) {
from javadoc
into "build/tmp"
classifier 'javadoc'
}
task sourcesJar(type: Jar, dependsOn: classes) {
from sourceSets.main.allSource
into "build/tmp"
classifier 'sources'
}
artifacts {
archives javadocJar, sourcesJar
}
if (project.hasProperty('signing.keyId')) {
signing {
sign configurations.archives
}
}
apply from: 'gradle/ext.gradle'
apply from: 'gradle/publish.gradle'
apply from: 'gradle/sonarqube.gradle'

View file

@ -0,0 +1,323 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE module PUBLIC
"-//Puppy Crawl//DTD Check Configuration 1.3//EN"
"http://www.puppycrawl.com/dtds/configuration_1_3.dtd">
<!-- This is a checkstyle configuration file. For descriptions of
what the following rules do, please see the checkstyle configuration
page at http://checkstyle.sourceforge.net/config.html -->
<module name="Checker">
<module name="FileTabCharacter">
<!-- Checks that there are no tab characters in the file.
-->
</module>
<module name="NewlineAtEndOfFile">
<property name="lineSeparator" value="lf"/>
</module>
<module name="RegexpSingleline">
<!-- Checks that FIXME is not used in comments. TODO is preferred.
-->
<property name="format" value="((//.*)|(\*.*))FIXME" />
<property name="message" value='TODO is preferred to FIXME. e.g. "TODO(johndoe): Refactor when v2 is released."' />
</module>
<module name="RegexpSingleline">
<!-- Checks that TODOs are named. (Actually, just that they are followed
by an open paren.)
-->
<property name="format" value="((//.*)|(\*.*))TODO[^(]" />
<property name="message" value='All TODOs should be named. e.g. "TODO(johndoe): Refactor when v2 is released."' />
</module>
<module name="JavadocPackage">
<!-- Checks that each Java package has a Javadoc file used for commenting.
Only allows a package-info.java, not package.html. -->
</module>
<!-- All Java AST specific tests live under TreeWalker module. -->
<module name="TreeWalker">
<!--
IMPORT CHECKS
-->
<module name="RedundantImport">
<!-- Checks for redundant import statements. -->
<property name="severity" value="error"/>
</module>
<module name="ImportOrder">
<!-- Checks for out of order import statements. -->
<property name="severity" value="warning"/>
<property name="groups" value="com,io,junit,net,org,java,javax"/>
<!-- This ensures that static imports go last. -->
<property name="option" value="under"/>
<property name="tokens" value="STATIC_IMPORT, IMPORT"/>
</module>
<!--
JAVADOC CHECKS
-->
<!-- Checks for Javadoc comments. -->
<!-- See http://checkstyle.sf.net/config_javadoc.html -->
<module name="JavadocMethod">
<property name="scope" value="protected"/>
<property name="severity" value="warning"/>
<property name="allowMissingJavadoc" value="true"/>
<property name="allowMissingParamTags" value="true"/>
<property name="allowMissingReturnTag" value="true"/>
<property name="allowMissingThrowsTags" value="true"/>
<property name="allowThrowsTagsForSubclasses" value="true"/>
<property name="allowUndeclaredRTE" value="true"/>
</module>
<module name="JavadocType">
<property name="scope" value="protected"/>
<property name="severity" value="error"/>
</module>
<module name="JavadocStyle">
<property name="severity" value="warning"/>
</module>
<!--
NAMING CHECKS
-->
<!-- Item 38 - Adhere to generally accepted naming conventions -->
<module name="PackageName">
<!-- Validates identifiers for package names against the
supplied expression. -->
<!-- Here the default checkstyle rule restricts package name parts to
seven characters, this is not in line with common practice at Google.
-->
<property name="format" value="^[a-z]+(\.[a-z][a-z0-9]{1,})*$"/>
<property name="severity" value="warning"/>
</module>
<module name="TypeNameCheck">
<!-- Validates static, final fields against the
expression "^[A-Z][a-zA-Z0-9]*$". -->
<metadata name="altname" value="TypeName"/>
<property name="severity" value="warning"/>
</module>
<module name="ConstantNameCheck">
<!-- Validates non-private, static, final fields against the supplied
public/package final fields "^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$". -->
<metadata name="altname" value="ConstantName"/>
<property name="applyToPublic" value="true"/>
<property name="applyToProtected" value="true"/>
<property name="applyToPackage" value="true"/>
<property name="applyToPrivate" value="false"/>
<property name="format" value="^([A-Z][A-Z0-9]*(_[A-Z0-9]+)*|FLAG_.*)$"/>
<message key="name.invalidPattern"
value="Variable ''{0}'' should be in ALL_CAPS (if it is a constant) or be private (otherwise)."/>
<property name="severity" value="warning"/>
</module>
<module name="StaticVariableNameCheck">
<!-- Validates static, non-final fields against the supplied
expression "^[a-z][a-zA-Z0-9]*_?$". -->
<metadata name="altname" value="StaticVariableName"/>
<property name="applyToPublic" value="true"/>
<property name="applyToProtected" value="true"/>
<property name="applyToPackage" value="true"/>
<property name="applyToPrivate" value="true"/>
<property name="format" value="^[a-z][a-zA-Z0-9]*_?$"/>
<property name="severity" value="warning"/>
</module>
<module name="MemberNameCheck">
<!-- Validates non-static members against the supplied expression. -->
<metadata name="altname" value="MemberName"/>
<property name="applyToPublic" value="true"/>
<property name="applyToProtected" value="true"/>
<property name="applyToPackage" value="true"/>
<property name="applyToPrivate" value="true"/>
<property name="format" value="^[a-z][a-zA-Z0-9]*$"/>
<property name="severity" value="warning"/>
</module>
<module name="MethodNameCheck">
<!-- Validates identifiers for method names. -->
<metadata name="altname" value="MethodName"/>
<property name="format" value="^[a-z][a-zA-Z0-9]*(_[a-zA-Z0-9]+)*$"/>
<property name="severity" value="warning"/>
</module>
<module name="ParameterName">
<!-- Validates identifiers for method parameters against the
expression "^[a-z][a-zA-Z0-9]*$". -->
<property name="severity" value="warning"/>
</module>
<module name="LocalFinalVariableName">
<!-- Validates identifiers for local final variables against the
expression "^[a-z][a-zA-Z0-9]*$". -->
<property name="severity" value="warning"/>
</module>
<module name="LocalVariableName">
<!-- Validates identifiers for local variables against the
expression "^[a-z][a-zA-Z0-9]*$". -->
<property name="severity" value="warning"/>
</module>
<!--
LENGTH and CODING CHECKS
-->
<module name="LineLength">
<!-- Checks if a line is too long. -->
<property name="max" value="${com.puppycrawl.tools.checkstyle.checks.sizes.LineLength.max}" default="128"/>
<property name="severity" value="error"/>
<!--
The default ignore pattern exempts the following elements:
- import statements
- long URLs inside comments
-->
<property name="ignorePattern"
value="${com.puppycrawl.tools.checkstyle.checks.sizes.LineLength.ignorePattern}"
default="^(package .*;\s*)|(import .*;\s*)|( *(\*|//).*https?://.*)$"/>
</module>
<module name="LeftCurly">
<!-- Checks for placement of the left curly brace ('{'). -->
<property name="severity" value="warning"/>
</module>
<module name="RightCurly">
<!-- Checks right curlies on CATCH, ELSE, and TRY blocks are on
the same line. e.g., the following example is fine:
<pre>
if {
...
} else
</pre>
-->
<!-- This next example is not fine:
<pre>
if {
...
}
else
</pre>
-->
<property name="option" value="same"/>
<property name="severity" value="warning"/>
</module>
<!-- Checks for braces around if and else blocks -->
<module name="NeedBraces">
<property name="severity" value="warning"/>
<property name="tokens" value="LITERAL_IF, LITERAL_ELSE, LITERAL_FOR, LITERAL_WHILE, LITERAL_DO"/>
</module>
<module name="UpperEll">
<!-- Checks that long constants are defined with an upper ell.-->
<property name="severity" value="error"/>
</module>
<module name="FallThrough">
<!-- Warn about falling through to the next case statement. Similar to
javac -Xlint:fallthrough, but the check is suppressed if a single-line comment
on the last non-blank line preceding the fallen-into case contains 'fall through' (or
some other variants which we don't publicized to promote consistency).
-->
<property name="reliefPattern"
value="fall through|Fall through|fallthru|Fallthru|falls through|Falls through|fallthrough|Fallthrough|No break|NO break|no break|continue on"/>
<property name="severity" value="error"/>
</module>
<!--
MODIFIERS CHECKS
-->
<module name="ModifierOrder">
<!-- Warn if modifier order is inconsistent with JLS3 8.1.1, 8.3.1, and
8.4.3. The prescribed order is:
public, protected, private, abstract, static, final, transient, volatile,
synchronized, native, strictfp
-->
</module>
<!--
WHITESPACE CHECKS
-->
<module name="WhitespaceAround">
<!-- Checks that various tokens are surrounded by whitespace.
This includes most binary operators and keywords followed
by regular or curly braces.
-->
<property name="tokens" value="ASSIGN, BAND, BAND_ASSIGN, BOR,
BOR_ASSIGN, BSR, BSR_ASSIGN, BXOR, BXOR_ASSIGN, COLON, DIV, DIV_ASSIGN,
EQUAL, GE, GT, LAND, LE, LITERAL_CATCH, LITERAL_DO, LITERAL_ELSE,
LITERAL_FINALLY, LITERAL_FOR, LITERAL_IF, LITERAL_RETURN,
LITERAL_SYNCHRONIZED, LITERAL_TRY, LITERAL_WHILE, LOR, LT, MINUS,
MINUS_ASSIGN, MOD, MOD_ASSIGN, NOT_EQUAL, PLUS, PLUS_ASSIGN, QUESTION,
SL, SL_ASSIGN, SR_ASSIGN, STAR, STAR_ASSIGN"/>
<property name="severity" value="error"/>
</module>
<module name="WhitespaceAfter">
<!-- Checks that commas, semicolons and typecasts are followed by
whitespace.
-->
<property name="tokens" value="COMMA, SEMI, TYPECAST"/>
</module>
<module name="NoWhitespaceAfter">
<!-- Checks that there is no whitespace after various unary operators.
Linebreaks are allowed.
-->
<property name="tokens" value="BNOT, DEC, DOT, INC, LNOT, UNARY_MINUS,
UNARY_PLUS"/>
<property name="allowLineBreaks" value="true"/>
<property name="severity" value="error"/>
</module>
<module name="NoWhitespaceBefore">
<!-- Checks that there is no whitespace before various unary operators.
Linebreaks are allowed.
-->
<property name="tokens" value="SEMI, DOT, POST_DEC, POST_INC"/>
<property name="allowLineBreaks" value="true"/>
<property name="severity" value="error"/>
</module>
<module name="ParenPad">
<!-- Checks that there is no whitespace before close parens or after
open parens.
-->
<property name="severity" value="warning"/>
</module>
</module>
</module>

10
gradle.properties Normal file
View file

@ -0,0 +1,10 @@
group = org.xbib
name = netty-http-client
version = 4.1.10.0
netty.version = 4.1.10.Final
tcnative.version = 2.0.1.Final
alpnagent.version = 2.0.6
junit.version = 4.12
asciidoclet.version = 1.5.4
wagon.version = 2.12

9
gradle/ext.gradle Normal file
View file

@ -0,0 +1,9 @@
ext {
user = 'jprante'
name = 'netty-http-client'
description = 'A java client for Elasticsearch'
scmUrl = 'https://github.com/' + user + '/' + name
scmConnection = 'scm:git:git://github.com/' + user + '/' + name + '.git'
scmDeveloperConnection = 'scm:git:git://github.com/' + user + '/' + name + '.git'
}

70
gradle/publish.gradle Normal file
View file

@ -0,0 +1,70 @@
task xbibUpload(type: Upload) {
configuration = configurations.archives
uploadDescriptor = true
repositories {
if (project.hasProperty("xbibUsername")) {
mavenDeployer {
configuration = configurations.wagon
repository(url: 'sftp://xbib.org/repository') {
authentication(userName: xbibUsername, privateKey: xbibPrivateKey)
}
}
}
}
}
task sonaTypeUpload(type: Upload) {
configuration = configurations.archives
uploadDescriptor = true
repositories {
if (project.hasProperty('ossrhUsername')) {
mavenDeployer {
beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) }
repository(url: 'https://oss.sonatype.org/service/local/staging/deploy/maven2') {
authentication(userName: ossrhUsername, password: ossrhPassword)
}
snapshotRepository(url: 'https://oss.sonatype.org/content/repositories/snapshots') {
authentication(userName: ossrhUsername, password: ossrhPassword)
}
pom.project {
groupId project.group
artifactId project.name
version project.version
name project.name
description description
packaging 'jar'
inceptionYear '2012'
url scmUrl
organization {
name 'xbib'
url 'http://xbib.org'
}
developers {
developer {
id user
name 'Jörg Prante'
email 'joergprante@gmail.com'
url 'https://github.com/jprante'
}
}
scm {
url scmUrl
connection scmConnection
developerConnection scmDeveloperConnection
}
licenses {
license {
name 'The Apache License, Version 2.0'
url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
}
}
}
}
}
}
}
nexusStaging {
packageGroup = "org.xbib"
}

41
gradle/sonarqube.gradle Normal file
View file

@ -0,0 +1,41 @@
tasks.withType(FindBugs) {
ignoreFailures = true
reports {
xml.enabled = false
html.enabled = true
}
}
tasks.withType(Pmd) {
ignoreFailures = true
reports {
xml.enabled = true
html.enabled = true
}
}
tasks.withType(Checkstyle) {
ignoreFailures = true
reports {
xml.enabled = true
html.enabled = true
}
}
jacocoTestReport {
reports {
xml.enabled true
csv.enabled false
xml.destination "${buildDir}/reports/jacoco-xml"
html.destination "${buildDir}/reports/jacoco-html"
}
}
sonarqube {
properties {
property "sonar.projectName", "${project.group} ${project.name}"
property "sonar.sourceEncoding", "UTF-8"
property "sonar.tests", "src/integration-test/java"
property "sonar.scm.provider", "git"
property "sonar.java.coveragePlugin", "jacoco"
property "sonar.junit.reportsPath", "build/test-results/test/"
}
}

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View file

@ -0,0 +1,6 @@
#Mon Apr 17 15:12:33 CEST 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-3.5-all.zip

684
src/docs/asciidoc/css/foundation.css vendored Normal file
View file

@ -0,0 +1,684 @@
/*! normalize.css v2.1.2 | MIT License | git.io/normalize */
/* ========================================================================== HTML5 display definitions ========================================================================== */
/** Correct `block` display not defined in IE 8/9. */
article, aside, details, figcaption, figure, footer, header, hgroup, main, nav, section, summary { display: block; }
/** Correct `inline-block` display not defined in IE 8/9. */
audio, canvas, video { display: inline-block; }
/** Prevent modern browsers from displaying `audio` without controls. Remove excess height in iOS 5 devices. */
audio:not([controls]) { display: none; height: 0; }
/** Address `[hidden]` styling not present in IE 8/9. Hide the `template` element in IE, Safari, and Firefox < 22. */
[hidden], template { display: none; }
script { display: none !important; }
/* ========================================================================== Base ========================================================================== */
/** 1. Set default font family to sans-serif. 2. Prevent iOS text size adjust after orientation change, without disabling user zoom. */
html { font-family: sans-serif; /* 1 */ -ms-text-size-adjust: 100%; /* 2 */ -webkit-text-size-adjust: 100%; /* 2 */ }
/** Remove default margin. */
body { margin: 0; }
/* ========================================================================== Links ========================================================================== */
/** Remove the gray background color from active links in IE 10. */
a { background: transparent; }
/** Address `outline` inconsistency between Chrome and other browsers. */
a:focus { outline: thin dotted; }
/** Improve readability when focused and also mouse hovered in all browsers. */
a:active, a:hover { outline: 0; }
/* ========================================================================== Typography ========================================================================== */
/** Address variable `h1` font-size and margin within `section` and `article` contexts in Firefox 4+, Safari 5, and Chrome. */
h1 { font-size: 2em; margin: 0.67em 0; }
/** Address styling not present in IE 8/9, Safari 5, and Chrome. */
abbr[title] { border-bottom: 1px dotted; }
/** Address style set to `bolder` in Firefox 4+, Safari 5, and Chrome. */
b, strong { font-weight: bold; }
/** Address styling not present in Safari 5 and Chrome. */
dfn { font-style: italic; }
/** Address differences between Firefox and other browsers. */
hr { -moz-box-sizing: content-box; box-sizing: content-box; height: 0; }
/** Address styling not present in IE 8/9. */
mark { background: #ff0; color: #000; }
/** Correct font family set oddly in Safari 5 and Chrome. */
code, kbd, pre, samp { font-family: monospace, serif; font-size: 1em; }
/** Improve readability of pre-formatted text in all browsers. */
pre { white-space: pre-wrap; }
/** Set consistent quote types. */
q { quotes: "\201C" "\201D" "\2018" "\2019"; }
/** Address inconsistent and variable font size in all browsers. */
small { font-size: 80%; }
/** Prevent `sub` and `sup` affecting `line-height` in all browsers. */
sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; }
sup { top: -0.5em; }
sub { bottom: -0.25em; }
/* ========================================================================== Embedded content ========================================================================== */
/** Remove border when inside `a` element in IE 8/9. */
img { border: 0; }
/** Correct overflow displayed oddly in IE 9. */
svg:not(:root) { overflow: hidden; }
/* ========================================================================== Figures ========================================================================== */
/** Address margin not present in IE 8/9 and Safari 5. */
figure { margin: 0; }
/* ========================================================================== Forms ========================================================================== */
/** Define consistent border, margin, and padding. */
fieldset { border: 1px solid #c0c0c0; margin: 0 2px; padding: 0.35em 0.625em 0.75em; }
/** 1. Correct `color` not being inherited in IE 8/9. 2. Remove padding so people aren't caught out if they zero out fieldsets. */
legend { border: 0; /* 1 */ padding: 0; /* 2 */ }
/** 1. Correct font family not being inherited in all browsers. 2. Correct font size not being inherited in all browsers. 3. Address margins set differently in Firefox 4+, Safari 5, and Chrome. */
button, input, select, textarea { font-family: inherit; /* 1 */ font-size: 100%; /* 2 */ margin: 0; /* 3 */ }
/** Address Firefox 4+ setting `line-height` on `input` using `!important` in the UA stylesheet. */
button, input { line-height: normal; }
/** Address inconsistent `text-transform` inheritance for `button` and `select`. All other form control elements do not inherit `text-transform` values. Correct `button` style inheritance in Chrome, Safari 5+, and IE 8+. Correct `select` style inheritance in Firefox 4+ and Opera. */
button, select { text-transform: none; }
/** 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` and `video` controls. 2. Correct inability to style clickable `input` types in iOS. 3. Improve usability and consistency of cursor style between image-type `input` and others. */
button, html input[type="button"], input[type="reset"], input[type="submit"] { -webkit-appearance: button; /* 2 */ cursor: pointer; /* 3 */ }
/** Re-set default cursor for disabled elements. */
button[disabled], html input[disabled] { cursor: default; }
/** 1. Address box sizing set to `content-box` in IE 8/9. 2. Remove excess padding in IE 8/9. */
input[type="checkbox"], input[type="radio"] { box-sizing: border-box; /* 1 */ padding: 0; /* 2 */ }
/** 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome. 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome (include `-moz` to future-proof). */
input[type="search"] { -webkit-appearance: textfield; /* 1 */ -moz-box-sizing: content-box; -webkit-box-sizing: content-box; /* 2 */ box-sizing: content-box; }
/** Remove inner padding and search cancel button in Safari 5 and Chrome on OS X. */
input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-decoration { -webkit-appearance: none; }
/** Remove inner padding and border in Firefox 4+. */
button::-moz-focus-inner, input::-moz-focus-inner { border: 0; padding: 0; }
/** 1. Remove default vertical scrollbar in IE 8/9. 2. Improve readability and alignment in all browsers. */
textarea { overflow: auto; /* 1 */ vertical-align: top; /* 2 */ }
/* ========================================================================== Tables ========================================================================== */
/** Remove most spacing between table cells. */
table { border-collapse: collapse; border-spacing: 0; }
meta.foundation-mq-small { font-family: "only screen and (min-width: 768px)"; width: 768px; }
meta.foundation-mq-medium { font-family: "only screen and (min-width:1280px)"; width: 1280px; }
meta.foundation-mq-large { font-family: "only screen and (min-width:1440px)"; width: 1440px; }
*, *:before, *:after { -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; }
html, body { font-size: 100%; }
body { background: white; color: #222222; padding: 0; margin: 0; font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; font-weight: normal; font-style: normal; line-height: 1; position: relative; cursor: auto; }
a:hover { cursor: pointer; }
img, object, embed { max-width: 100%; height: auto; }
object, embed { height: 100%; }
img { -ms-interpolation-mode: bicubic; }
#map_canvas img, #map_canvas embed, #map_canvas object, .map_canvas img, .map_canvas embed, .map_canvas object { max-width: none !important; }
.left { float: left !important; }
.right { float: right !important; }
.text-left { text-align: left !important; }
.text-right { text-align: right !important; }
.text-center { text-align: center !important; }
.text-justify { text-align: justify !important; }
.hide { display: none; }
.antialiased { -webkit-font-smoothing: antialiased; }
img { display: inline-block; vertical-align: middle; }
textarea { height: auto; min-height: 50px; }
select { width: 100%; }
object, svg { display: inline-block; vertical-align: middle; }
.center { margin-left: auto; margin-right: auto; }
.spread { width: 100%; }
p.lead, .paragraph.lead > p, #preamble > .sectionbody > .paragraph:first-of-type p { font-size: 1.21875em; line-height: 1.6; }
.subheader, .admonitionblock td.content > .title, .audioblock > .title, .exampleblock > .title, .imageblock > .title, .listingblock > .title, .literalblock > .title, .stemblock > .title, .openblock > .title, .paragraph > .title, .quoteblock > .title, table.tableblock > .title, .verseblock > .title, .videoblock > .title, .dlist > .title, .olist > .title, .ulist > .title, .qlist > .title, .hdlist > .title { line-height: 1.4; color: #6f6f6f; font-weight: 300; margin-top: 0.2em; margin-bottom: 0.5em; }
/* Typography resets */
div, dl, dt, dd, ul, ol, li, h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6, pre, form, p, blockquote, th, td { margin: 0; padding: 0; direction: ltr; }
/* Default Link Styles */
a { color: #2ba6cb; text-decoration: none; line-height: inherit; }
a:hover, a:focus { color: #2795b6; }
a img { border: none; }
/* Default paragraph styles */
p { font-family: inherit; font-weight: normal; font-size: 1em; line-height: 1.6; margin-bottom: 1.25em; text-rendering: optimizeLegibility; }
p aside { font-size: 0.875em; line-height: 1.35; font-style: italic; }
/* Default header styles */
h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6 { font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; font-weight: bold; font-style: normal; color: #222222; text-rendering: optimizeLegibility; margin-top: 1em; margin-bottom: 0.5em; line-height: 1.2125em; }
h1 small, h2 small, h3 small, #toctitle small, .sidebarblock > .content > .title small, h4 small, h5 small, h6 small { font-size: 60%; color: #6f6f6f; line-height: 0; }
h1 { font-size: 2.125em; }
h2 { font-size: 1.6875em; }
h3, #toctitle, .sidebarblock > .content > .title { font-size: 1.375em; }
h4 { font-size: 1.125em; }
h5 { font-size: 1.125em; }
h6 { font-size: 1em; }
hr { border: solid #dddddd; border-width: 1px 0 0; clear: both; margin: 1.25em 0 1.1875em; height: 0; }
/* Helpful Typography Defaults */
em, i { font-style: italic; line-height: inherit; }
strong, b { font-weight: bold; line-height: inherit; }
small { font-size: 60%; line-height: inherit; }
code { font-family: Consolas, "Liberation Mono", Courier, monospace; font-weight: bold; color: #7f0a0c; }
/* Lists */
ul, ol, dl { font-size: 1em; line-height: 1.6; margin-bottom: 1.25em; list-style-position: outside; font-family: inherit; }
ul, ol { margin-left: 1.5em; }
ul.no-bullet, ol.no-bullet { margin-left: 1.5em; }
/* Unordered Lists */
ul li ul, ul li ol { margin-left: 1.25em; margin-bottom: 0; font-size: 1em; /* Override nested font-size change */ }
ul.square li ul, ul.circle li ul, ul.disc li ul { list-style: inherit; }
ul.square { list-style-type: square; }
ul.circle { list-style-type: circle; }
ul.disc { list-style-type: disc; }
ul.no-bullet { list-style: none; }
/* Ordered Lists */
ol li ul, ol li ol { margin-left: 1.25em; margin-bottom: 0; }
/* Definition Lists */
dl dt { margin-bottom: 0.3125em; font-weight: bold; }
dl dd { margin-bottom: 1.25em; }
/* Abbreviations */
abbr, acronym { text-transform: uppercase; font-size: 90%; color: #222222; border-bottom: 1px dotted #dddddd; cursor: help; }
abbr { text-transform: none; }
/* Blockquotes */
blockquote { margin: 0 0 1.25em; padding: 0.5625em 1.25em 0 1.1875em; border-left: 1px solid #dddddd; }
blockquote cite { display: block; font-size: 0.8125em; color: #555555; }
blockquote cite:before { content: "\2014 \0020"; }
blockquote cite a, blockquote cite a:visited { color: #555555; }
blockquote, blockquote p { line-height: 1.6; color: #6f6f6f; }
/* Microformats */
.vcard { display: inline-block; margin: 0 0 1.25em 0; border: 1px solid #dddddd; padding: 0.625em 0.75em; }
.vcard li { margin: 0; display: block; }
.vcard .fn { font-weight: bold; font-size: 0.9375em; }
.vevent .summary { font-weight: bold; }
.vevent abbr { cursor: auto; text-decoration: none; font-weight: bold; border: none; padding: 0 0.0625em; }
@media only screen and (min-width: 768px) { h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6 { line-height: 1.4; }
h1 { font-size: 2.75em; }
h2 { font-size: 2.3125em; }
h3, #toctitle, .sidebarblock > .content > .title { font-size: 1.6875em; }
h4 { font-size: 1.4375em; } }
/* Tables */
table { background: white; margin-bottom: 1.25em; border: solid 1px #dddddd; }
table thead, table tfoot { background: whitesmoke; font-weight: bold; }
table thead tr th, table thead tr td, table tfoot tr th, table tfoot tr td { padding: 0.5em 0.625em 0.625em; font-size: inherit; color: #222222; text-align: left; }
table tr th, table tr td { padding: 0.5625em 0.625em; font-size: inherit; color: #222222; }
table tr.even, table tr.alt, table tr:nth-of-type(even) { background: #f9f9f9; }
table thead tr th, table tfoot tr th, table tbody tr td, table tr td, table tfoot tr td { display: table-cell; line-height: 1.4; }
body { -moz-osx-font-smoothing: grayscale; -webkit-font-smoothing: antialiased; tab-size: 4; }
h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6 { line-height: 1.4; }
.clearfix:before, .clearfix:after, .float-group:before, .float-group:after { content: " "; display: table; }
.clearfix:after, .float-group:after { clear: both; }
*:not(pre) > code { font-size: inherit; font-style: normal !important; letter-spacing: 0; padding: 0; line-height: inherit; word-wrap: break-word; }
*:not(pre) > code.nobreak { word-wrap: normal; }
*:not(pre) > code.nowrap { white-space: nowrap; }
pre, pre > code { line-height: 1.4; color: black; font-family: monospace, serif; font-weight: normal; }
em em { font-style: normal; }
strong strong { font-weight: normal; }
.keyseq { color: #555555; }
kbd { font-family: Consolas, "Liberation Mono", Courier, monospace; display: inline-block; color: #222222; font-size: 0.65em; line-height: 1.45; background-color: #f7f7f7; border: 1px solid #ccc; -webkit-border-radius: 3px; border-radius: 3px; -webkit-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2), 0 0 0 0.1em white inset; box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2), 0 0 0 0.1em white inset; margin: 0 0.15em; padding: 0.2em 0.5em; vertical-align: middle; position: relative; top: -0.1em; white-space: nowrap; }
.keyseq kbd:first-child { margin-left: 0; }
.keyseq kbd:last-child { margin-right: 0; }
.menuseq, .menu { color: #090909; }
b.button:before, b.button:after { position: relative; top: -1px; font-weight: normal; }
b.button:before { content: "["; padding: 0 3px 0 2px; }
b.button:after { content: "]"; padding: 0 2px 0 3px; }
#header, #content, #footnotes, #footer { width: 100%; margin-left: auto; margin-right: auto; margin-top: 0; margin-bottom: 0; max-width: 62.5em; *zoom: 1; position: relative; padding-left: 0.9375em; padding-right: 0.9375em; }
#header:before, #header:after, #content:before, #content:after, #footnotes:before, #footnotes:after, #footer:before, #footer:after { content: " "; display: table; }
#header:after, #content:after, #footnotes:after, #footer:after { clear: both; }
#content { margin-top: 1.25em; }
#content:before { content: none; }
#header > h1:first-child { color: black; margin-top: 2.25rem; margin-bottom: 0; }
#header > h1:first-child + #toc { margin-top: 8px; border-top: 1px solid #dddddd; }
#header > h1:only-child, body.toc2 #header > h1:nth-last-child(2) { border-bottom: 1px solid #dddddd; padding-bottom: 8px; }
#header .details { border-bottom: 1px solid #dddddd; line-height: 1.45; padding-top: 0.25em; padding-bottom: 0.25em; padding-left: 0.25em; color: #555555; display: -ms-flexbox; display: -webkit-flex; display: flex; -ms-flex-flow: row wrap; -webkit-flex-flow: row wrap; flex-flow: row wrap; }
#header .details span:first-child { margin-left: -0.125em; }
#header .details span.email a { color: #6f6f6f; }
#header .details br { display: none; }
#header .details br + span:before { content: "\00a0\2013\00a0"; }
#header .details br + span.author:before { content: "\00a0\22c5\00a0"; color: #6f6f6f; }
#header .details br + span#revremark:before { content: "\00a0|\00a0"; }
#header #revnumber { text-transform: capitalize; }
#header #revnumber:after { content: "\00a0"; }
#content > h1:first-child:not([class]) { color: black; border-bottom: 1px solid #dddddd; padding-bottom: 8px; margin-top: 0; padding-top: 1rem; margin-bottom: 1.25rem; }
#toc { border-bottom: 1px solid #dddddd; padding-bottom: 0.5em; }
#toc > ul { margin-left: 0.125em; }
#toc ul.sectlevel0 > li > a { font-style: italic; }
#toc ul.sectlevel0 ul.sectlevel1 { margin: 0.5em 0; }
#toc ul { font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; list-style-type: none; }
#toc li { line-height: 1.3334; margin-top: 0.3334em; }
#toc a { text-decoration: none; }
#toc a:active { text-decoration: underline; }
#toctitle { color: #6f6f6f; font-size: 1.2em; }
@media only screen and (min-width: 768px) { #toctitle { font-size: 1.375em; }
body.toc2 { padding-left: 15em; padding-right: 0; }
#toc.toc2 { margin-top: 0 !important; background-color: #f2f2f2; position: fixed; width: 15em; left: 0; top: 0; border-right: 1px solid #dddddd; border-top-width: 0 !important; border-bottom-width: 0 !important; z-index: 1000; padding: 1.25em 1em; height: 100%; overflow: auto; }
#toc.toc2 #toctitle { margin-top: 0; margin-bottom: 0.8rem; font-size: 1.2em; }
#toc.toc2 > ul { font-size: 0.9em; margin-bottom: 0; }
#toc.toc2 ul ul { margin-left: 0; padding-left: 1em; }
#toc.toc2 ul.sectlevel0 ul.sectlevel1 { padding-left: 0; margin-top: 0.5em; margin-bottom: 0.5em; }
body.toc2.toc-right { padding-left: 0; padding-right: 15em; }
body.toc2.toc-right #toc.toc2 { border-right-width: 0; border-left: 1px solid #dddddd; left: auto; right: 0; } }
@media only screen and (min-width: 1280px) { body.toc2 { padding-left: 20em; padding-right: 0; }
#toc.toc2 { width: 20em; }
#toc.toc2 #toctitle { font-size: 1.375em; }
#toc.toc2 > ul { font-size: 0.95em; }
#toc.toc2 ul ul { padding-left: 1.25em; }
body.toc2.toc-right { padding-left: 0; padding-right: 20em; } }
#content #toc { border-style: solid; border-width: 1px; border-color: #d9d9d9; margin-bottom: 1.25em; padding: 1.25em; background: #f2f2f2; -webkit-border-radius: 0; border-radius: 0; }
#content #toc > :first-child { margin-top: 0; }
#content #toc > :last-child { margin-bottom: 0; }
#footer { max-width: 100%; background-color: #222222; padding: 1.25em; }
#footer-text { color: #dddddd; line-height: 1.44; }
.sect1 { padding-bottom: 0.625em; }
@media only screen and (min-width: 768px) { .sect1 { padding-bottom: 1.25em; } }
.sect1 + .sect1 { border-top: 1px solid #dddddd; }
#content h1 > a.anchor, h2 > a.anchor, h3 > a.anchor, #toctitle > a.anchor, .sidebarblock > .content > .title > a.anchor, h4 > a.anchor, h5 > a.anchor, h6 > a.anchor { position: absolute; z-index: 1001; width: 1.5ex; margin-left: -1.5ex; display: block; text-decoration: none !important; visibility: hidden; text-align: center; font-weight: normal; }
#content h1 > a.anchor:before, h2 > a.anchor:before, h3 > a.anchor:before, #toctitle > a.anchor:before, .sidebarblock > .content > .title > a.anchor:before, h4 > a.anchor:before, h5 > a.anchor:before, h6 > a.anchor:before { content: "\00A7"; font-size: 0.85em; display: block; padding-top: 0.1em; }
#content h1:hover > a.anchor, #content h1 > a.anchor:hover, h2:hover > a.anchor, h2 > a.anchor:hover, h3:hover > a.anchor, #toctitle:hover > a.anchor, .sidebarblock > .content > .title:hover > a.anchor, h3 > a.anchor:hover, #toctitle > a.anchor:hover, .sidebarblock > .content > .title > a.anchor:hover, h4:hover > a.anchor, h4 > a.anchor:hover, h5:hover > a.anchor, h5 > a.anchor:hover, h6:hover > a.anchor, h6 > a.anchor:hover { visibility: visible; }
#content h1 > a.link, h2 > a.link, h3 > a.link, #toctitle > a.link, .sidebarblock > .content > .title > a.link, h4 > a.link, h5 > a.link, h6 > a.link { color: #222222; text-decoration: none; }
#content h1 > a.link:hover, h2 > a.link:hover, h3 > a.link:hover, #toctitle > a.link:hover, .sidebarblock > .content > .title > a.link:hover, h4 > a.link:hover, h5 > a.link:hover, h6 > a.link:hover { color: #151515; }
.audioblock, .imageblock, .literalblock, .listingblock, .stemblock, .videoblock { margin-bottom: 1.25em; }
.admonitionblock td.content > .title, .audioblock > .title, .exampleblock > .title, .imageblock > .title, .listingblock > .title, .literalblock > .title, .stemblock > .title, .openblock > .title, .paragraph > .title, .quoteblock > .title, table.tableblock > .title, .verseblock > .title, .videoblock > .title, .dlist > .title, .olist > .title, .ulist > .title, .qlist > .title, .hdlist > .title { text-rendering: optimizeLegibility; text-align: left; }
table.tableblock > caption.title { white-space: nowrap; overflow: visible; max-width: 0; }
.paragraph.lead > p, #preamble > .sectionbody > .paragraph:first-of-type p { color: black; }
table.tableblock #preamble > .sectionbody > .paragraph:first-of-type p { font-size: inherit; }
.admonitionblock > table { border-collapse: separate; border: 0; background: none; width: 100%; }
.admonitionblock > table td.icon { text-align: center; width: 80px; }
.admonitionblock > table td.icon img { max-width: initial; }
.admonitionblock > table td.icon .title { font-weight: bold; font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; text-transform: uppercase; }
.admonitionblock > table td.content { padding-left: 1.125em; padding-right: 1.25em; border-left: 1px solid #dddddd; color: #555555; }
.admonitionblock > table td.content > :last-child > :last-child { margin-bottom: 0; }
.exampleblock > .content { border-style: solid; border-width: 1px; border-color: #e6e6e6; margin-bottom: 1.25em; padding: 1.25em; background: white; -webkit-border-radius: 0; border-radius: 0; }
.exampleblock > .content > :first-child { margin-top: 0; }
.exampleblock > .content > :last-child { margin-bottom: 0; }
.sidebarblock { border-style: solid; border-width: 1px; border-color: #d9d9d9; margin-bottom: 1.25em; padding: 1.25em; background: #f2f2f2; -webkit-border-radius: 0; border-radius: 0; }
.sidebarblock > :first-child { margin-top: 0; }
.sidebarblock > :last-child { margin-bottom: 0; }
.sidebarblock > .content > .title { color: #6f6f6f; margin-top: 0; }
.exampleblock > .content > :last-child > :last-child, .exampleblock > .content .olist > ol > li:last-child > :last-child, .exampleblock > .content .ulist > ul > li:last-child > :last-child, .exampleblock > .content .qlist > ol > li:last-child > :last-child, .sidebarblock > .content > :last-child > :last-child, .sidebarblock > .content .olist > ol > li:last-child > :last-child, .sidebarblock > .content .ulist > ul > li:last-child > :last-child, .sidebarblock > .content .qlist > ol > li:last-child > :last-child { margin-bottom: 0; }
.literalblock pre, .listingblock pre:not(.highlight), .listingblock pre[class="highlight"], .listingblock pre[class^="highlight "], .listingblock pre.CodeRay, .listingblock pre.prettyprint { background: #eeeeee; }
.sidebarblock .literalblock pre, .sidebarblock .listingblock pre:not(.highlight), .sidebarblock .listingblock pre[class="highlight"], .sidebarblock .listingblock pre[class^="highlight "], .sidebarblock .listingblock pre.CodeRay, .sidebarblock .listingblock pre.prettyprint { background: #f2f1f1; }
.literalblock pre, .literalblock pre[class], .listingblock pre, .listingblock pre[class] { border: 1px solid #cccccc; -webkit-border-radius: 0; border-radius: 0; word-wrap: break-word; padding: 0.8em 0.8em 0.65em 0.8em; font-size: 0.8125em; }
.literalblock pre.nowrap, .literalblock pre[class].nowrap, .listingblock pre.nowrap, .listingblock pre[class].nowrap { overflow-x: auto; white-space: pre; word-wrap: normal; }
@media only screen and (min-width: 768px) { .literalblock pre, .literalblock pre[class], .listingblock pre, .listingblock pre[class] { font-size: 0.90625em; } }
@media only screen and (min-width: 1280px) { .literalblock pre, .literalblock pre[class], .listingblock pre, .listingblock pre[class] { font-size: 1em; } }
.literalblock.output pre { color: #eeeeee; background-color: black; }
.listingblock pre.highlightjs { padding: 0; }
.listingblock pre.highlightjs > code { padding: 0.8em 0.8em 0.65em 0.8em; -webkit-border-radius: 0; border-radius: 0; }
.listingblock > .content { position: relative; }
.listingblock code[data-lang]:before { display: none; content: attr(data-lang); position: absolute; font-size: 0.75em; top: 0.425rem; right: 0.5rem; line-height: 1; text-transform: uppercase; color: #999; }
.listingblock:hover code[data-lang]:before { display: block; }
.listingblock.terminal pre .command:before { content: attr(data-prompt); padding-right: 0.5em; color: #999; }
.listingblock.terminal pre .command:not([data-prompt]):before { content: "$"; }
table.pyhltable { border-collapse: separate; border: 0; margin-bottom: 0; background: none; }
table.pyhltable td { vertical-align: top; padding-top: 0; padding-bottom: 0; line-height: 1.4; }
table.pyhltable td.code { padding-left: .75em; padding-right: 0; }
pre.pygments .lineno, table.pyhltable td:not(.code) { color: #999; padding-left: 0; padding-right: .5em; border-right: 1px solid #dddddd; }
pre.pygments .lineno { display: inline-block; margin-right: .25em; }
table.pyhltable .linenodiv { background: none !important; padding-right: 0 !important; }
.quoteblock { margin: 0 1em 1.25em 1.5em; display: table; }
.quoteblock > .title { margin-left: -1.5em; margin-bottom: 0.75em; }
.quoteblock blockquote, .quoteblock blockquote p { color: #6f6f6f; font-size: 1.15rem; line-height: 1.75; word-spacing: 0.1em; letter-spacing: 0; font-style: italic; text-align: justify; }
.quoteblock blockquote { margin: 0; padding: 0; border: 0; }
.quoteblock blockquote:before { content: "\201c"; float: left; font-size: 2.75em; font-weight: bold; line-height: 0.6em; margin-left: -0.6em; color: #6f6f6f; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); }
.quoteblock blockquote > .paragraph:last-child p { margin-bottom: 0; }
.quoteblock .attribution { margin-top: 0.5em; margin-right: 0.5ex; text-align: right; }
.quoteblock .quoteblock { margin-left: 0; margin-right: 0; padding: 0.5em 0; border-left: 3px solid #555555; }
.quoteblock .quoteblock blockquote { padding: 0 0 0 0.75em; }
.quoteblock .quoteblock blockquote:before { display: none; }
.verseblock { margin: 0 1em 1.25em 1em; }
.verseblock pre { font-family: "Open Sans", "DejaVu Sans", sans; font-size: 1.15rem; color: #6f6f6f; font-weight: 300; text-rendering: optimizeLegibility; }
.verseblock pre strong { font-weight: 400; }
.verseblock .attribution { margin-top: 1.25rem; margin-left: 0.5ex; }
.quoteblock .attribution, .verseblock .attribution { font-size: 0.8125em; line-height: 1.45; font-style: italic; }
.quoteblock .attribution br, .verseblock .attribution br { display: none; }
.quoteblock .attribution cite, .verseblock .attribution cite { display: block; letter-spacing: -0.025em; color: #555555; }
.quoteblock.abstract { margin: 0 0 1.25em 0; display: block; }
.quoteblock.abstract blockquote, .quoteblock.abstract blockquote p { text-align: left; word-spacing: 0; }
.quoteblock.abstract blockquote:before, .quoteblock.abstract blockquote p:first-of-type:before { display: none; }
table.tableblock { max-width: 100%; border-collapse: separate; }
table.tableblock td > .paragraph:last-child p > p:last-child, table.tableblock th > p:last-child, table.tableblock td > p:last-child { margin-bottom: 0; }
table.tableblock, th.tableblock, td.tableblock { border: 0 solid #dddddd; }
table.grid-all th.tableblock, table.grid-all td.tableblock { border-width: 0 1px 1px 0; }
table.grid-all tfoot > tr > th.tableblock, table.grid-all tfoot > tr > td.tableblock { border-width: 1px 1px 0 0; }
table.grid-cols th.tableblock, table.grid-cols td.tableblock { border-width: 0 1px 0 0; }
table.grid-all * > tr > .tableblock:last-child, table.grid-cols * > tr > .tableblock:last-child { border-right-width: 0; }
table.grid-rows th.tableblock, table.grid-rows td.tableblock { border-width: 0 0 1px 0; }
table.grid-all tbody > tr:last-child > th.tableblock, table.grid-all tbody > tr:last-child > td.tableblock, table.grid-all thead:last-child > tr > th.tableblock, table.grid-rows tbody > tr:last-child > th.tableblock, table.grid-rows tbody > tr:last-child > td.tableblock, table.grid-rows thead:last-child > tr > th.tableblock { border-bottom-width: 0; }
table.grid-rows tfoot > tr > th.tableblock, table.grid-rows tfoot > tr > td.tableblock { border-width: 1px 0 0 0; }
table.frame-all { border-width: 1px; }
table.frame-sides { border-width: 0 1px; }
table.frame-topbot { border-width: 1px 0; }
th.halign-left, td.halign-left { text-align: left; }
th.halign-right, td.halign-right { text-align: right; }
th.halign-center, td.halign-center { text-align: center; }
th.valign-top, td.valign-top { vertical-align: top; }
th.valign-bottom, td.valign-bottom { vertical-align: bottom; }
th.valign-middle, td.valign-middle { vertical-align: middle; }
table thead th, table tfoot th { font-weight: bold; }
tbody tr th { display: table-cell; line-height: 1.4; background: whitesmoke; }
tbody tr th, tbody tr th p, tfoot tr th, tfoot tr th p { color: #222222; font-weight: bold; }
p.tableblock > code:only-child { background: none; padding: 0; }
p.tableblock { font-size: 1em; }
td > div.verse { white-space: pre; }
ol { margin-left: 1.75em; }
ul li ol { margin-left: 1.5em; }
dl dd { margin-left: 1.125em; }
dl dd:last-child, dl dd:last-child > :last-child { margin-bottom: 0; }
ol > li p, ul > li p, ul dd, ol dd, .olist .olist, .ulist .ulist, .ulist .olist, .olist .ulist { margin-bottom: 0.625em; }
ul.unstyled, ol.unnumbered, ul.checklist, ul.none { list-style-type: none; }
ul.unstyled, ol.unnumbered, ul.checklist { margin-left: 0.625em; }
ul.checklist li > p:first-child > .fa-square-o:first-child, ul.checklist li > p:first-child > .fa-check-square-o:first-child { width: 1em; font-size: 0.85em; }
ul.checklist li > p:first-child > input[type="checkbox"]:first-child { width: 1em; position: relative; top: 1px; }
ul.inline { margin: 0 auto 0.625em auto; margin-left: -1.375em; margin-right: 0; padding: 0; list-style: none; overflow: hidden; }
ul.inline > li { list-style: none; float: left; margin-left: 1.375em; display: block; }
ul.inline > li > * { display: block; }
.unstyled dl dt { font-weight: normal; font-style: normal; }
ol.arabic { list-style-type: decimal; }
ol.decimal { list-style-type: decimal-leading-zero; }
ol.loweralpha { list-style-type: lower-alpha; }
ol.upperalpha { list-style-type: upper-alpha; }
ol.lowerroman { list-style-type: lower-roman; }
ol.upperroman { list-style-type: upper-roman; }
ol.lowergreek { list-style-type: lower-greek; }
.hdlist > table, .colist > table { border: 0; background: none; }
.hdlist > table > tbody > tr, .colist > table > tbody > tr { background: none; }
td.hdlist1, td.hdlist2 { vertical-align: top; padding: 0 0.625em; }
td.hdlist1 { font-weight: bold; padding-bottom: 1.25em; }
.literalblock + .colist, .listingblock + .colist { margin-top: -0.5em; }
.colist > table tr > td:first-of-type { padding: 0 0.75em; line-height: 1; }
.colist > table tr > td:first-of-type img { max-width: initial; }
.colist > table tr > td:last-of-type { padding: 0.25em 0; }
.thumb, .th { line-height: 0; display: inline-block; border: solid 4px white; -webkit-box-shadow: 0 0 0 1px #dddddd; box-shadow: 0 0 0 1px #dddddd; }
.imageblock.left, .imageblock[style*="float: left"] { margin: 0.25em 0.625em 1.25em 0; }
.imageblock.right, .imageblock[style*="float: right"] { margin: 0.25em 0 1.25em 0.625em; }
.imageblock > .title { margin-bottom: 0; }
.imageblock.thumb, .imageblock.th { border-width: 6px; }
.imageblock.thumb > .title, .imageblock.th > .title { padding: 0 0.125em; }
.image.left, .image.right { margin-top: 0.25em; margin-bottom: 0.25em; display: inline-block; line-height: 0; }
.image.left { margin-right: 0.625em; }
.image.right { margin-left: 0.625em; }
a.image { text-decoration: none; display: inline-block; }
a.image object { pointer-events: none; }
sup.footnote, sup.footnoteref { font-size: 0.875em; position: static; vertical-align: super; }
sup.footnote a, sup.footnoteref a { text-decoration: none; }
sup.footnote a:active, sup.footnoteref a:active { text-decoration: underline; }
#footnotes { padding-top: 0.75em; padding-bottom: 0.75em; margin-bottom: 0.625em; }
#footnotes hr { width: 20%; min-width: 6.25em; margin: -0.25em 0 0.75em 0; border-width: 1px 0 0 0; }
#footnotes .footnote { padding: 0 0.375em 0 0.225em; line-height: 1.3334; font-size: 0.875em; margin-left: 1.2em; text-indent: -1.05em; margin-bottom: 0.2em; }
#footnotes .footnote a:first-of-type { font-weight: bold; text-decoration: none; }
#footnotes .footnote:last-of-type { margin-bottom: 0; }
#content #footnotes { margin-top: -0.625em; margin-bottom: 0; padding: 0.75em 0; }
.gist .file-data > table { border: 0; background: #fff; width: 100%; margin-bottom: 0; }
.gist .file-data > table td.line-data { width: 99%; }
div.unbreakable { page-break-inside: avoid; }
.big { font-size: larger; }
.small { font-size: smaller; }
.underline { text-decoration: underline; }
.overline { text-decoration: overline; }
.line-through { text-decoration: line-through; }
.aqua { color: #00bfbf; }
.aqua-background { background-color: #00fafa; }
.black { color: black; }
.black-background { background-color: black; }
.blue { color: #0000bf; }
.blue-background { background-color: #0000fa; }
.fuchsia { color: #bf00bf; }
.fuchsia-background { background-color: #fa00fa; }
.gray { color: #606060; }
.gray-background { background-color: #7d7d7d; }
.green { color: #006000; }
.green-background { background-color: #007d00; }
.lime { color: #00bf00; }
.lime-background { background-color: #00fa00; }
.maroon { color: #600000; }
.maroon-background { background-color: #7d0000; }
.navy { color: #000060; }
.navy-background { background-color: #00007d; }
.olive { color: #606000; }
.olive-background { background-color: #7d7d00; }
.purple { color: #600060; }
.purple-background { background-color: #7d007d; }
.red { color: #bf0000; }
.red-background { background-color: #fa0000; }
.silver { color: #909090; }
.silver-background { background-color: #bcbcbc; }
.teal { color: #006060; }
.teal-background { background-color: #007d7d; }
.white { color: #bfbfbf; }
.white-background { background-color: #fafafa; }
.yellow { color: #bfbf00; }
.yellow-background { background-color: #fafa00; }
span.icon > .fa { cursor: default; }
.admonitionblock td.icon [class^="fa icon-"] { font-size: 2.5em; text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5); cursor: default; }
.admonitionblock td.icon .icon-note:before { content: "\f05a"; color: #207c98; }
.admonitionblock td.icon .icon-tip:before { content: "\f0eb"; text-shadow: 1px 1px 2px rgba(155, 155, 0, 0.8); color: #111; }
.admonitionblock td.icon .icon-warning:before { content: "\f071"; color: #bf6900; }
.admonitionblock td.icon .icon-caution:before { content: "\f06d"; color: #bf3400; }
.admonitionblock td.icon .icon-important:before { content: "\f06a"; color: #bf0000; }
.conum[data-value] { display: inline-block; color: #fff !important; background-color: #222222; -webkit-border-radius: 100px; border-radius: 100px; text-align: center; font-size: 0.75em; width: 1.67em; height: 1.67em; line-height: 1.67em; font-family: "Open Sans", "DejaVu Sans", sans-serif; font-style: normal; font-weight: bold; }
.conum[data-value] * { color: #fff !important; }
.conum[data-value] + b { display: none; }
.conum[data-value]:after { content: attr(data-value); }
pre .conum[data-value] { position: relative; top: -0.125em; }
b.conum * { color: inherit !important; }
.conum:not([data-value]):empty { display: none; }
.literalblock pre, .listingblock pre { background: #eeeeee; }

View file

@ -0,0 +1,11 @@
= Netty HTTP client
Jörg Prante
Version 4.1.9.0
:sectnums:
:toc: preamble
:toclevels: 4
:!toc-title: Content
:experimental:
:description: asynchronous Netty HTTP client for Java
:keywords: Java, Netty, HTTP, client
:icons: font

View file

@ -0,0 +1,4 @@
= Netty HTTP client
Jörg Prante
Version 4.1.9.0

View file

@ -0,0 +1,28 @@
/*
* Copyright 2017 Jörg Prante
*
* Jörg Prante licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package org.xbib.netty.http.client;
/**
*/
@FunctionalInterface
public interface ExceptionListener {
/**
* Called when an exception is transported to a listener.
* @param throwable the exception
*/
void onException(Throwable throwable);
}

View file

@ -0,0 +1,105 @@
/*
* Copyright 2017 Jörg Prante
*
* Jörg Prante licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package org.xbib.netty.http.client;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.pool.ChannelPool;
import io.netty.handler.codec.http.FullHttpResponse;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Netty channel handler for HTTP 1.1.
*/
@ChannelHandler.Sharable
final class Http1Handler extends ChannelInboundHandlerAdapter {
private static final Logger logger = Logger.getLogger(Http1Handler.class.getName());
private final HttpClient httpClient;
Http1Handler(HttpClient httpClient) {
this.httpClient = httpClient;
}
/**
*
* Read channel message, hand over content to response handler, and redirect to next URL if possible.
* @param ctx the channel handler context
* @param msg the channel message
* @throws Exception if processing of channel message fails
*/
@Override
public void channelRead(final ChannelHandlerContext ctx, Object msg) throws Exception {
logger.log(Level.FINE, () -> "channelRead msg " + msg.getClass().getName());
final HttpRequestContext httpRequestContext =
ctx.channel().attr(HttpClientChannelContext.REQUEST_CONTEXT_ATTRIBUTE_KEY).get();
if (msg instanceof FullHttpResponse) {
FullHttpResponse httpResponse = (FullHttpResponse) msg;
HttpResponseListener httpResponseListener =
ctx.channel().attr(HttpClientChannelContext.RESPONSE_LISTENER_ATTRIBUTE_KEY).get();
if (httpResponseListener != null) {
httpResponseListener.onResponse(httpResponse);
}
if (httpClient.tryRedirect(ctx.channel(), httpResponse, httpRequestContext)) {
return;
}
logger.log(Level.FINE, () -> "success");
httpRequestContext.success("response arrived");
final ChannelPool channelPool =
ctx.channel().attr(HttpClientChannelContext.CHANNEL_POOL_ATTRIBUTE_KEY).get();
channelPool.release(ctx.channel());
}
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
logger.log(Level.FINE, () -> "channelInactive " + ctx);
final HttpRequestContext httpRequestContext =
ctx.channel().attr(HttpClientChannelContext.REQUEST_CONTEXT_ATTRIBUTE_KEY).get();
if (httpRequestContext.getRedirectCount().get() == 0 && !httpRequestContext.isSucceeded()) {
httpRequestContext.fail("channel inactive");
}
final ChannelPool channelPool =
ctx.channel().attr(HttpClientChannelContext.CHANNEL_POOL_ATTRIBUTE_KEY).get();
channelPool.release(ctx.channel());
}
/**
* Forward channel exceptions to the exception listener.
* @param ctx the channel handler context
* @param cause the cause of the exception
* @throws Exception if forwarding fails
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ExceptionListener exceptionListener =
ctx.channel().attr(HttpClientChannelContext.EXCEPTION_LISTENER_ATTRIBUTE_KEY).get();
logger.log(Level.FINE, () -> "exceptionCaught");
if (exceptionListener != null) {
exceptionListener.onException(cause);
}
final HttpRequestContext httpRequestContext =
ctx.channel().attr(HttpClientChannelContext.REQUEST_CONTEXT_ATTRIBUTE_KEY).get();
httpRequestContext.fail(cause.getMessage());
final ChannelPool channelPool =
ctx.channel().attr(HttpClientChannelContext.CHANNEL_POOL_ATTRIBUTE_KEY).get();
channelPool.release(ctx.channel());
}
}

View file

@ -0,0 +1,161 @@
/*
* Copyright 2017 Jörg Prante
*
* Jörg Prante licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package org.xbib.netty.http.client;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.pool.ChannelPool;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http2.HttpConversionUtil;
import io.netty.util.internal.PlatformDependent;
import java.util.AbstractMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Netty channel handler for HTTP/2 responses.
*/
@ChannelHandler.Sharable
public class Http2Handler extends SimpleChannelInboundHandler<FullHttpResponse> {
private static final Logger logger = Logger.getLogger(Http2Handler.class.getName());
private final Map<Integer, Entry<ChannelFuture, ChannelPromise>> streamidPromiseMap;
private final HttpClient httpClient;
Http2Handler(HttpClient httpClient) {
this.streamidPromiseMap = PlatformDependent.newConcurrentHashMap();
this.httpClient = httpClient;
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpResponse httpResponse) throws Exception {
logger.log(Level.FINE, () -> httpResponse.getClass().getName());
Integer streamId = httpResponse.headers().getInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text());
if (streamId == null) {
logger.log(Level.WARNING, () -> "stream ID missing");
return;
}
final HttpRequestContext httpRequestContext =
ctx.channel().attr(HttpClientChannelContext.REQUEST_CONTEXT_ATTRIBUTE_KEY).get();
Entry<ChannelFuture, ChannelPromise> entry = streamidPromiseMap.get(streamId);
if (entry != null) {
HttpResponseListener httpResponseListener =
ctx.channel().attr(HttpClientChannelContext.RESPONSE_LISTENER_ATTRIBUTE_KEY).get();
if (httpResponseListener != null) {
httpResponseListener.onResponse(httpResponse);
}
entry.getValue().setSuccess();
if (httpClient.tryRedirect(ctx.channel(), httpResponse, httpRequestContext)) {
return;
}
logger.log(Level.FINE, () -> "success");
httpRequestContext.success("response arrived");
final ChannelPool channelPool =
ctx.channel().attr(HttpClientChannelContext.CHANNEL_POOL_ATTRIBUTE_KEY).get();
channelPool.release(ctx.channel());
} else {
logger.log(Level.WARNING, () -> "stream id not found in promises: " + streamId);
}
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
logger.log(Level.FINE, ctx::toString);
final ChannelPool channelPool =
ctx.channel().attr(HttpClientChannelContext.CHANNEL_POOL_ATTRIBUTE_KEY).get();
channelPool.release(ctx.channel());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ExceptionListener exceptionListener =
ctx.channel().attr(HttpClientChannelContext.EXCEPTION_LISTENER_ATTRIBUTE_KEY).get();
logger.log(Level.FINE, () -> "exception caught");
if (exceptionListener != null) {
exceptionListener.onException(cause);
}
final HttpRequestContext httpRequestContext =
ctx.channel().attr(HttpClientChannelContext.REQUEST_CONTEXT_ATTRIBUTE_KEY).get();
httpRequestContext.fail(cause.getMessage());
final ChannelPool channelPool =
ctx.channel().attr(HttpClientChannelContext.CHANNEL_POOL_ATTRIBUTE_KEY).get();
channelPool.release(ctx.channel());
}
void put(int streamId, ChannelFuture channelFuture, ChannelPromise promise) {
logger.log(Level.FINE, "put stream ID " + streamId);
streamidPromiseMap.put(streamId, new AbstractMap.SimpleEntry<>(channelFuture, promise));
}
void awaitResponses(HttpRequestContext httpRequestContext, ExceptionListener exceptionListener) {
int timeout = httpRequestContext.getTimeout();
Iterator<Entry<Integer, Entry<ChannelFuture, ChannelPromise>>> iterator = streamidPromiseMap.entrySet().iterator();
while (iterator.hasNext()) {
Entry<Integer, Entry<ChannelFuture, ChannelPromise>> entry = iterator.next();
ChannelFuture channelFuture = entry.getValue().getKey();
if (!channelFuture.awaitUninterruptibly(timeout, TimeUnit.MILLISECONDS)) {
IllegalStateException illegalStateException =
new IllegalStateException("time out while waiting to write for stream id " + entry.getKey());
if (exceptionListener != null) {
exceptionListener.onException(illegalStateException);
httpRequestContext.fail(illegalStateException.getMessage());
final ChannelPool channelPool =
channelFuture.channel().attr(HttpClientChannelContext.CHANNEL_POOL_ATTRIBUTE_KEY).get();
channelPool.release(channelFuture.channel());
}
throw illegalStateException;
}
if (!channelFuture.isSuccess()) {
throw new RuntimeException(channelFuture.cause());
}
ChannelPromise promise = entry.getValue().getValue();
if (!promise.awaitUninterruptibly(timeout, TimeUnit.MILLISECONDS)) {
IllegalStateException illegalStateException =
new IllegalStateException("time out while waiting for response on stream id " + entry.getKey());
if (exceptionListener != null) {
exceptionListener.onException(illegalStateException);
httpRequestContext.fail(illegalStateException.getMessage());
final ChannelPool channelPool =
channelFuture.channel().attr(HttpClientChannelContext.CHANNEL_POOL_ATTRIBUTE_KEY).get();
channelPool.release(channelFuture.channel());
}
throw illegalStateException;
}
if (!promise.isSuccess()) {
RuntimeException runtimeException = new RuntimeException(promise.cause());
if (exceptionListener != null) {
exceptionListener.onException(runtimeException);
httpRequestContext.fail(runtimeException.getMessage());
final ChannelPool channelPool =
channelFuture.channel().attr(HttpClientChannelContext.CHANNEL_POOL_ATTRIBUTE_KEY).get();
channelPool.release(channelFuture.channel());
}
throw runtimeException;
}
iterator.remove();
}
}
}

View file

@ -0,0 +1,363 @@
/*
* Copyright 2017 Jörg Prante
*
* Jörg Prante licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package org.xbib.netty.http.client;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBufAllocator;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelPromise;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.pool.ChannelPool;
import io.netty.channel.pool.FixedChannelPool;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.DefaultHttpRequest;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.FutureListener;
import java.io.Closeable;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLDecoder;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* A Netty HTTP client.
*/
public final class HttpClient implements Closeable {
private static final Logger logger = Logger.getLogger(HttpClient.class.getName());
private final ByteBufAllocator byteBufAllocator;
private final EventLoopGroup eventLoopGroup;
private final HttpClientChannelPoolMap poolMap;
/**
* Create a new HTTP client.
*/
HttpClient(ByteBufAllocator byteBufAllocator,
EventLoopGroup eventLoopGroup,
Bootstrap bootstrap,
int maxConnections,
HttpClientChannelContext httpClientChannelContext) {
this.byteBufAllocator = byteBufAllocator;
this.eventLoopGroup = eventLoopGroup;
this.poolMap = new HttpClientChannelPoolMap(this, httpClientChannelContext, bootstrap, maxConnections);
}
/**
* Create a builder to configure connecting.
*
* @return A builder
*/
public static HttpClientBuilder builder() {
return new HttpClientBuilder();
}
public HttpClientRequestBuilder prepareRequest(HttpMethod method) {
return new HttpClientRequestBuilder(this, method, byteBufAllocator);
}
/**
* Prepare a HTTP GET request.
*
* @return a request builder
*/
public HttpClientRequestBuilder prepareGet() {
return prepareRequest(HttpMethod.GET);
}
/**
* Prepare a HTTP HEAD request.
*
* @return a request builder
*/
public HttpClientRequestBuilder prepareHead() {
return prepareRequest(HttpMethod.HEAD);
}
/**
* Prepare a HTTP PUT request.
*
* @return a request builder
*/
public HttpClientRequestBuilder preparePut() {
return prepareRequest(HttpMethod.PUT);
}
/**
* Prepare a HTTP POST request.
*
* @return a request builder
*/
public HttpClientRequestBuilder preparePost() {
return prepareRequest(HttpMethod.POST);
}
/**
* Prepare a HTTP DELETE request.
*
* @return a request builder
*/
public HttpClientRequestBuilder prepareDelete() {
return prepareRequest(HttpMethod.DELETE);
}
/**
* Prepare a HTTP OPTIONS request.
*
* @return a request builder
*/
public HttpClientRequestBuilder prepareOptions() {
return prepareRequest(HttpMethod.OPTIONS);
}
/**
* Prepare a HTTP PATCH request.
*
* @return a request builder
*/
public HttpClientRequestBuilder preparePatch() {
return prepareRequest(HttpMethod.PATCH);
}
/**
* Prepare a HTTP TRACE request.
*
* @return a request builder
*/
public HttpClientRequestBuilder prepareTrace() {
return prepareRequest(HttpMethod.TRACE);
}
public HttpClientChannelPoolMap poolMap() {
return poolMap;
}
/**
* Close client.
*/
public void close() {
logger.log(Level.FINE, () -> "closing pool map");
poolMap.close();
logger.log(Level.FINE, () -> "closing event loop group");
if (!eventLoopGroup.isTerminated()) {
eventLoopGroup.shutdownGracefully();
}
logger.log(Level.FINE, () -> "closed");
}
void dispatch(HttpRequestContext httpRequestContext, HttpResponseListener httpResponseListener,
ExceptionListener exceptionListener) {
final URL url = httpRequestContext.getURL();
final HttpRequest httpRequest = httpRequestContext.getHttpRequest();
logger.log(Level.FINE, () -> "trying URL " + url);
if (httpRequestContext.isExpired()) {
httpRequestContext.fail("request expired");
}
if (httpRequestContext.isFailed()) {
logger.log(Level.FINE, () -> "request is cancelled");
return;
}
HttpVersion version = httpRequestContext.getHttpRequest().protocolVersion();
InetAddressKey inetAddressKey = new InetAddressKey(url, version);
// effectivly disable pool for HTTP/2
if (version.majorVersion() == 2) {
poolMap.remove(inetAddressKey);
}
final FixedChannelPool pool = poolMap.get(inetAddressKey);
logger.log(Level.FINE, () -> "connecting to " + inetAddressKey);
Future<Channel> futureChannel = pool.acquire();
futureChannel.addListener((FutureListener<Channel>) future -> {
if (future.isSuccess()) {
Channel channel = future.getNow();
channel.attr(HttpClientChannelContext.CHANNEL_POOL_ATTRIBUTE_KEY).set(pool);
channel.attr(HttpClientChannelContext.REQUEST_CONTEXT_ATTRIBUTE_KEY).set(httpRequestContext);
if (httpResponseListener != null) {
channel.attr(HttpClientChannelContext.RESPONSE_LISTENER_ATTRIBUTE_KEY).set(httpResponseListener);
}
if (exceptionListener != null) {
channel.attr(HttpClientChannelContext.EXCEPTION_LISTENER_ATTRIBUTE_KEY).set(exceptionListener);
}
if (httpRequestContext.isFailed()) {
logger.log(Level.FINE, () -> "detected fail, close now");
future.cancel(true);
if (channel.isOpen()) {
channel.close();
}
logger.log(Level.FINE, () -> "release channel to pool");
pool.release(channel);
return;
}
if (httpRequest.protocolVersion().majorVersion() == 1) {
logger.log(Level.FINE, "HTTP1: write and flush " + httpRequest.toString());
channel.writeAndFlush(httpRequest)
.addListener((ChannelFutureListener) future1 -> {
if (httpRequestContext.isFailed()) {
logger.log(Level.FINE, () -> "detected fail, close now");
future1.cancel(true);
if (future1.channel().isOpen()) {
future1.channel().close();
}
}
});
} else if (httpRequest.protocolVersion().majorVersion() == 2) {
HttpClientChannelInitializer.Http2SettingsHandler http2SettingsHandler =
poolMap.getHttpClientChannelInitializer().getHttp2SettingsHandler();
if (http2SettingsHandler != null) {
logger.log(Level.FINE, "HTTP2: waiting for settings");
http2SettingsHandler.awaitSettings(httpRequestContext, exceptionListener);
}
Http2Handler http2Handler = poolMap.getHttpClientChannelInitializer().getHttp2Handler();
if (http2Handler != null) {
logger.log(Level.FINE, () ->
"HTTP2: trying to write, streamID=" + httpRequestContext.getStreamId() +
" request: " + httpRequest.toString());
ChannelPromise channelPromise = channel.newPromise();
http2Handler.put(httpRequestContext.getStreamId(), channel.write(httpRequest), channelPromise);
channel.flush();
logger.log(Level.FINE, "HTTP2: waiting for responses");
http2Handler.awaitResponses(httpRequestContext, exceptionListener);
}
}
} else {
if (exceptionListener != null) {
exceptionListener.onException(future.cause());
}
httpRequestContext.fail("channel pool failure");
}
});
}
boolean tryRedirect(Channel channel, FullHttpResponse httpResponse, HttpRequestContext httpRequestContext)
throws IOException {
if (httpRequestContext.isFollowRedirect()) {
String redirUrl = findRedirect(httpRequestContext, httpResponse);
if (redirUrl != null) {
HttpMethod method = httpResponse.status().code() == 303 ? HttpMethod.GET :
httpRequestContext.getHttpRequest().method();
if (httpRequestContext.getRedirectCount().getAndIncrement() < httpRequestContext.getMaxRedirects()) {
dispatchRedirect(channel, method, new URL(redirUrl), httpRequestContext);
} else {
httpRequestContext.fail("too many redirections");
final ChannelPool channelPool =
channel.attr(HttpClientChannelContext.CHANNEL_POOL_ATTRIBUTE_KEY).get();
channelPool.release(channel);
}
return true;
}
}
return false;
}
private String findRedirect(HttpRequestContext httpRequestContext, HttpResponse httpResponse)
throws IOException {
if (httpResponse == null) {
return null;
}
switch (httpResponse.status().code()) {
case 300:
case 301:
case 302:
case 303:
case 305:
case 307:
case 308:
String location = URLDecoder.decode(httpResponse.headers().get(HttpHeaderNames.LOCATION), "UTF-8");
if (location != null && (location.toLowerCase().startsWith("http://") ||
location.toLowerCase().startsWith("https://"))) {
logger.log(Level.FINE, "(absolute) redirect to " + location);
return location;
} else {
logger.log(Level.FINE, "(relative->absolute) redirect to " + location);
return makeAbsolute(httpRequestContext.getURL(), location);
}
default:
break;
}
return null;
}
private void dispatchRedirect(Channel channel, HttpMethod method, URL url,
HttpRequestContext httpRequestContext) {
final String uri = httpRequestContext.getHttpRequest().protocolVersion().majorVersion() == 2 ?
url.toExternalForm() : makeRelative(url);
final HttpRequest httpRequest;
if (method.equals(httpRequestContext.getHttpRequest().method()) &&
httpRequestContext.getHttpRequest() instanceof DefaultFullHttpRequest) {
DefaultFullHttpRequest defaultFullHttpRequest = (DefaultFullHttpRequest) httpRequestContext.getHttpRequest();
FullHttpRequest fullHttpRequest = defaultFullHttpRequest.copy();
fullHttpRequest.setUri(uri);
httpRequest = fullHttpRequest;
} else {
httpRequest = new DefaultHttpRequest(httpRequestContext.getHttpRequest().protocolVersion(), method, uri);
}
for (Map.Entry<String, String> e : httpRequestContext.getHttpRequest().headers().entries()) {
httpRequest.headers().add(e.getKey(), e.getValue());
}
httpRequest.headers().set(HttpHeaderNames.HOST, url.getHost());
HttpRequestContext redirectContext = new HttpRequestContext(url, httpRequest,
httpRequestContext);
logger.log(Level.FINE, "dispatchRedirect url = " + url + " with new request " + httpRequest.toString());
HttpResponseListener httpResponseListener =
channel.attr(HttpClientChannelContext.RESPONSE_LISTENER_ATTRIBUTE_KEY).get();
ExceptionListener exceptionListener =
channel.attr(HttpClientChannelContext.EXCEPTION_LISTENER_ATTRIBUTE_KEY).get();
dispatch(redirectContext, httpResponseListener, exceptionListener);
}
private String makeRelative(URL base) {
String uri = base.getPath();
if (base.getQuery() != null) {
uri = uri + "?" + base.getQuery();
}
return uri;
}
private String makeAbsolute(URL base, String location) throws UnsupportedEncodingException {
String path = base.getPath() == null ? "/" : URLDecoder.decode(base.getPath(), "UTF-8");
if (location.startsWith("/")) {
path = location;
} else if (path.endsWith("/")) {
path += location;
} else {
path += "/" + location;
}
String scheme = base.getProtocol();
StringBuilder sb = new StringBuilder(scheme).append("://").append(base.getHost());
int defaultPort = "http".equals(scheme) ? 80 : "https".equals(scheme) ? 443 : -1;
if (defaultPort != -1 && base.getPort() != -1 && defaultPort != base.getPort()) {
sb.append(":").append(base.getPort());
}
if (path.charAt(0) != '/') {
sb.append('/');
}
sb.append(path);
return sb.toString();
}
}

View file

@ -0,0 +1,326 @@
/*
* Copyright 2017 Jörg Prante
*
* Jörg Prante licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package org.xbib.netty.http.client;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.PooledByteBufAllocator;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.proxy.HttpProxyHandler;
import io.netty.handler.proxy.Socks4ProxyHandler;
import io.netty.handler.proxy.Socks5ProxyHandler;
import io.netty.handler.ssl.CipherSuiteFilter;
import io.netty.handler.ssl.SslProvider;
import java.io.InputStream;
import java.net.InetSocketAddress;
import javax.net.ssl.TrustManagerFactory;
/**
*
*/
public class HttpClientBuilder implements HttpClientChannelContextDefaults {
private ByteBufAllocator byteBufAllocator;
private EventLoopGroup eventLoopGroup;
private Class<? extends SocketChannel> socketChannelClass;
private Bootstrap bootstrap;
// let Netty decide, where default is Runtime.getRuntime().availableProcessors() * 2
private int threads = 0;
private boolean tcpNodelay = DEFAULT_TCP_NODELAY;
private boolean keepAlive = DEFAULT_SO_KEEPALIVE;
private boolean reuseAddr = DEFAULT_SO_REUSEADDR;
private int tcpSendBufferSize = DEFAULT_TCP_SEND_BUFFER_SIZE;
private int tcpReceiveBufferSize = DEFAULT_TCP_RECEIVE_BUFFER_SIZE;
private int maxChunkSize = DEFAULT_MAX_CHUNK_SIZE;
private int maxInitialLineLength = DEFAULT_MAX_INITIAL_LINE_LENGTH;
private int maxHeadersSize = DEFAULT_MAX_HEADERS_SIZE;
private int maxConnections = DEFAULT_MAX_CONNECTIONS;
private int maxContentLength = DEFAULT_MAX_CONTENT_LENGTH;
private int maxCompositeBufferComponents = DEFAULT_MAX_COMPOSITE_BUFFER_COMPONENTS;
private int connectTimeoutMillis = DEFAULT_TIMEOUT_MILLIS;
private int readTimeoutMillis = DEFAULT_TIMEOUT_MILLIS;
private boolean enableGzip = DEFAULT_ENABLE_GZIP;
private boolean installHttp2Upgrade = DEFAULT_INSTALL_HTTP_UPGRADE2;
private SslProvider sslProvider = DEFAULT_SSL_PROVIDER;
private Iterable<String> ciphers = DEFAULT_CIPHERS;
private CipherSuiteFilter cipherSuiteFilter = DEFAULT_CIPHER_SUITE_FILTER;
private TrustManagerFactory trustManagerFactory = DEFAULT_TRUST_MANAGER_FACTORY;
private InputStream keyCertChainInputStream;
private InputStream keyInputStream;
private String keyPassword;
private boolean useServerNameIdentification = DEFAULT_USE_SERVER_NAME_IDENTIFICATION;
private SslClientAuthMode sslClientAuthMode = DEFAULT_SSL_CLIENT_AUTH_MODE;
private HttpProxyHandler httpProxyHandler;
private Socks4ProxyHandler socks4ProxyHandler;
private Socks5ProxyHandler socks5ProxyHandler;
public HttpClientBuilder withByteBufAllocator(ByteBufAllocator byteBufAllocator) {
this.byteBufAllocator = byteBufAllocator;
return this;
}
public HttpClientBuilder withEventLoop(EventLoopGroup eventLoopGroup) {
this.eventLoopGroup = eventLoopGroup;
return this;
}
public HttpClientBuilder withChannelClass(Class<SocketChannel> socketChannelClass) {
this.socketChannelClass = socketChannelClass;
return this;
}
public HttpClientBuilder withBootstrap(Bootstrap bootstrap) {
this.bootstrap = bootstrap;
return this;
}
public HttpClientBuilder setConnectTimeoutMillis(int connectTimeoutMillis) {
this.connectTimeoutMillis = connectTimeoutMillis;
return this;
}
public HttpClientBuilder setThreadCount(int count) {
this.threads = count;
return this;
}
public HttpClientBuilder setTcpSendBufferSize(int tcpSendBufferSize) {
this.tcpSendBufferSize = tcpSendBufferSize;
return this;
}
public HttpClientBuilder setTcpReceiveBufferSize(int tcpReceiveBufferSize) {
this.tcpReceiveBufferSize = tcpReceiveBufferSize;
return this;
}
public HttpClientBuilder setTcpNodelay(boolean tcpNodelay) {
this.tcpNodelay = tcpNodelay;
return this;
}
public HttpClientBuilder setKeepAlive(boolean keepAlive) {
this.keepAlive = keepAlive;
return this;
}
public HttpClientBuilder setReuseAddr(boolean reuseAddr) {
this.reuseAddr = reuseAddr;
return this;
}
public HttpClientBuilder setMaxChunkSize(int maxChunkSize) {
this.maxChunkSize = maxChunkSize;
return this;
}
public HttpClientBuilder setMaxInitialLineLength(int maxInitialLineLength) {
this.maxInitialLineLength = maxInitialLineLength;
return this;
}
public HttpClientBuilder setMaxHeadersSize(int maxHeadersSize) {
this.maxHeadersSize = maxHeadersSize;
return this;
}
public HttpClientBuilder setMaxContentLength(int maxContentLength) {
this.maxContentLength = maxContentLength;
return this;
}
public HttpClientBuilder setMaxCompositeBufferComponents(int maxCompositeBufferComponents) {
this.maxCompositeBufferComponents = maxCompositeBufferComponents;
return this;
}
public HttpClientBuilder setMaxConnections(int maxConnections) {
this.maxConnections = maxConnections;
return this;
}
public HttpClientBuilder setReadTimeoutMillis(int readTimeoutMillis) {
this.readTimeoutMillis = readTimeoutMillis;
return this;
}
public HttpClientBuilder setEnableGzip(boolean enableGzip) {
this.enableGzip = enableGzip;
return this;
}
public HttpClientBuilder setInstallHttp2Upgrade(boolean installHttp2Upgrade) {
this.installHttp2Upgrade = installHttp2Upgrade;
return this;
}
public HttpClientBuilder withSslProvider(SslProvider sslProvider) {
this.sslProvider = sslProvider;
return this;
}
public HttpClientBuilder withJdkSslProvider() {
this.sslProvider = SslProvider.JDK;
return this;
}
public HttpClientBuilder withOpenSSLSslProvider() {
this.sslProvider = SslProvider.OPENSSL;
return this;
}
public HttpClientBuilder withCiphers(Iterable<String> ciphers) {
this.ciphers = ciphers;
return this;
}
public HttpClientBuilder withCipherSuiteFilter(CipherSuiteFilter cipherSuiteFilter) {
this.cipherSuiteFilter = cipherSuiteFilter;
return this;
}
public HttpClientBuilder withTrustManagerFactory(TrustManagerFactory trustManagerFactory) {
this.trustManagerFactory = trustManagerFactory;
return this;
}
public HttpClientBuilder setKeyCert(InputStream keyCertChainInputStream, InputStream keyInputStream) {
this.keyCertChainInputStream = keyCertChainInputStream;
this.keyInputStream = keyInputStream;
return this;
}
public HttpClientBuilder setKeyCert(InputStream keyCertChainInputStream, InputStream keyInputStream,
String keyPassword) {
this.keyCertChainInputStream = keyCertChainInputStream;
this.keyInputStream = keyInputStream;
this.keyPassword = keyPassword;
return this;
}
public HttpClientBuilder setUseServerNameIdentification(boolean useServerNameIdentification) {
this.useServerNameIdentification = useServerNameIdentification;
return this;
}
public HttpClientBuilder setSslClientAuthMode(SslClientAuthMode sslClientAuthMode) {
this.sslClientAuthMode = sslClientAuthMode;
return this;
}
public HttpClientBuilder setHttpProxyHandler(InetSocketAddress proxyAddress) {
this.httpProxyHandler = new HttpProxyHandler(proxyAddress);
return this;
}
public HttpClientBuilder setHttpProxyHandler(InetSocketAddress proxyAddress, String username, String password) {
this.httpProxyHandler = new HttpProxyHandler(proxyAddress, username, password);
return this;
}
public HttpClientBuilder setSocks4Proxy(InetSocketAddress proxyAddress) {
this.socks4ProxyHandler = new Socks4ProxyHandler(proxyAddress);
return this;
}
public HttpClientBuilder setSocks4Proxy(InetSocketAddress proxyAddress, String username) {
this.socks4ProxyHandler = new Socks4ProxyHandler(proxyAddress, username);
return this;
}
public HttpClientBuilder setSocks5Proxy(InetSocketAddress proxyAddress) {
this.socks5ProxyHandler = new Socks5ProxyHandler(proxyAddress);
return this;
}
public HttpClientBuilder setSocks5Proxy(InetSocketAddress proxyAddress, String username, String password) {
this.socks5ProxyHandler = new Socks5ProxyHandler(proxyAddress, username, password);
return this;
}
/**
* Build a HTTP client.
* @return a http client
*/
public HttpClient build() {
if (byteBufAllocator == null) {
byteBufAllocator = PooledByteBufAllocator.DEFAULT;
}
if (eventLoopGroup == null) {
eventLoopGroup = new NioEventLoopGroup(threads, new HttpClientThreadFactory());
}
if (socketChannelClass == null) {
socketChannelClass = NioSocketChannel.class;
}
if (bootstrap == null) {
bootstrap = new Bootstrap();
}
bootstrap.option(ChannelOption.TCP_NODELAY, tcpNodelay);
bootstrap.option(ChannelOption.SO_KEEPALIVE, keepAlive);
bootstrap.option(ChannelOption.SO_REUSEADDR, reuseAddr);
bootstrap.option(ChannelOption.SO_SNDBUF, tcpSendBufferSize);
bootstrap.option(ChannelOption.SO_RCVBUF, tcpReceiveBufferSize);
bootstrap.option(ChannelOption.ALLOCATOR, byteBufAllocator);
bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectTimeoutMillis);
bootstrap.group(eventLoopGroup);
bootstrap.channel(socketChannelClass);
final HttpClientChannelContext httpClientChannelContext =
new HttpClientChannelContext(maxInitialLineLength, maxHeadersSize, maxChunkSize, maxContentLength,
maxCompositeBufferComponents,
readTimeoutMillis, enableGzip, installHttp2Upgrade,
sslProvider, ciphers, cipherSuiteFilter, trustManagerFactory,
keyCertChainInputStream, keyInputStream, keyPassword,
useServerNameIdentification, sslClientAuthMode,
httpProxyHandler, socks4ProxyHandler, socks5ProxyHandler);
return new HttpClient(byteBufAllocator, eventLoopGroup, bootstrap, maxConnections, httpClientChannelContext);
}
}

View file

@ -0,0 +1,206 @@
/*
* Copyright 2017 Jörg Prante
*
* Jörg Prante licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package org.xbib.netty.http.client;
import io.netty.channel.pool.ChannelPool;
import io.netty.handler.proxy.HttpProxyHandler;
import io.netty.handler.proxy.Socks4ProxyHandler;
import io.netty.handler.proxy.Socks5ProxyHandler;
import io.netty.handler.ssl.CipherSuiteFilter;
import io.netty.handler.ssl.SslProvider;
import io.netty.util.AttributeKey;
import java.io.InputStream;
import javax.net.ssl.TrustManagerFactory;
/**
*/
final class HttpClientChannelContext {
static final AttributeKey<ChannelPool> CHANNEL_POOL_ATTRIBUTE_KEY =
AttributeKey.valueOf("httpClientChannelPool");
static final AttributeKey<HttpRequestContext> REQUEST_CONTEXT_ATTRIBUTE_KEY =
AttributeKey.valueOf("httpClientRequestContext");
static final AttributeKey<HttpResponseListener> RESPONSE_LISTENER_ATTRIBUTE_KEY =
AttributeKey.valueOf("httpClientResponseListener");
static final AttributeKey<ExceptionListener> EXCEPTION_LISTENER_ATTRIBUTE_KEY =
AttributeKey.valueOf("httpClientExceptionListener");
private final int maxInitialLineLength;
private final int maxHeaderSize;
private final int maxChunkSize;
private final int maxContentLength;
private final int maxCompositeBufferComponents;
private final int readTimeoutMillis;
private final boolean enableGzip;
private final boolean installHttp2Upgrade;
private final SslProvider sslProvider;
private final Iterable<String> ciphers;
private final CipherSuiteFilter cipherSuiteFilter;
private final TrustManagerFactory trustManagerFactory;
private final InputStream keyCertChainInputStream;
private final InputStream keyInputStream;
private final String keyPassword;
private final boolean useServerNameIdentification;
private final SslClientAuthMode sslClientAuthMode;
private final HttpProxyHandler httpProxyHandler;
private final Socks4ProxyHandler socks4ProxyHandler;
private final Socks5ProxyHandler socks5ProxyHandler;
HttpClientChannelContext(int maxInitialLineLength,
int maxHeaderSize,
int maxChunkSize,
int maxContentLength,
int maxCompositeBufferComponents,
int readTimeoutMillis,
boolean enableGzip,
boolean installHttp2Upgrade,
SslProvider sslProvider,
Iterable<String> ciphers,
CipherSuiteFilter cipherSuiteFilter,
TrustManagerFactory trustManagerFactory,
InputStream keyCertChainInputStream,
InputStream keyInputStream,
String keyPassword,
boolean useServerNameIdentification,
SslClientAuthMode sslClientAuthMode,
HttpProxyHandler httpProxyHandler,
Socks4ProxyHandler socks4ProxyHandler,
Socks5ProxyHandler socks5ProxyHandler) {
this.maxInitialLineLength = maxInitialLineLength;
this.maxHeaderSize = maxHeaderSize;
this.maxChunkSize = maxChunkSize;
this.maxContentLength = maxContentLength;
this.maxCompositeBufferComponents = maxCompositeBufferComponents;
this.readTimeoutMillis = readTimeoutMillis;
this.enableGzip = enableGzip;
this.installHttp2Upgrade = installHttp2Upgrade;
this.sslProvider = sslProvider;
this.ciphers = ciphers;
this.cipherSuiteFilter = cipherSuiteFilter;
this.trustManagerFactory = trustManagerFactory;
this.keyCertChainInputStream = keyCertChainInputStream;
this.keyInputStream = keyInputStream;
this.keyPassword = keyPassword;
this.useServerNameIdentification = useServerNameIdentification;
this.sslClientAuthMode = sslClientAuthMode;
this.httpProxyHandler = httpProxyHandler;
this.socks4ProxyHandler = socks4ProxyHandler;
this.socks5ProxyHandler = socks5ProxyHandler;
}
int getMaxInitialLineLength() {
return maxInitialLineLength;
}
int getMaxHeaderSize() {
return maxHeaderSize;
}
int getMaxChunkSize() {
return maxChunkSize;
}
int getMaxContentLength() {
return maxContentLength;
}
int getMaxCompositeBufferComponents() {
return maxCompositeBufferComponents;
}
int getReadTimeoutMillis() {
return readTimeoutMillis;
}
boolean isGzipEnabled() {
return enableGzip;
}
boolean isInstallHttp2Upgrade() {
return installHttp2Upgrade;
}
SslProvider getSslProvider() {
return sslProvider;
}
Iterable<String> getCiphers() {
return ciphers;
}
CipherSuiteFilter getCipherSuiteFilter() {
return cipherSuiteFilter;
}
TrustManagerFactory getTrustManagerFactory() {
return trustManagerFactory;
}
InputStream getKeyCertChainInputStream() {
return keyCertChainInputStream;
}
InputStream getKeyInputStream() {
return keyInputStream;
}
String getKeyPassword() {
return keyPassword;
}
boolean isUseServerNameIdentification() {
return useServerNameIdentification;
}
SslClientAuthMode getSslClientAuthMode() {
return sslClientAuthMode;
}
HttpProxyHandler getHttpProxyHandler() {
return httpProxyHandler;
}
Socks4ProxyHandler getSocks4ProxyHandler() {
return socks4ProxyHandler;
}
Socks5ProxyHandler getSocks5ProxyHandler() {
return socks5ProxyHandler;
}
}

View file

@ -0,0 +1,122 @@
/*
* Copyright 2017 Jörg Prante
*
* Jörg Prante licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package org.xbib.netty.http.client;
import io.netty.handler.codec.http2.Http2SecurityUtil;
import io.netty.handler.ssl.CipherSuiteFilter;
import io.netty.handler.ssl.SslProvider;
import io.netty.handler.ssl.SupportedCipherSuiteFilter;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import javax.net.ssl.TrustManagerFactory;
/**
*/
public interface HttpClientChannelContextDefaults {
/**
* Default for TCP_NODELAY.
*/
boolean DEFAULT_TCP_NODELAY = true;
/**
* Default for SO_KEEPALIVE.
*/
boolean DEFAULT_SO_KEEPALIVE = true;
/**
* Default for SO_REUSEADDR.
*/
boolean DEFAULT_SO_REUSEADDR = true;
/**
* Set TCP send buffer to 64k per socket.
*/
int DEFAULT_TCP_SEND_BUFFER_SIZE = 64 * 1024;
/**
* Set TCP receive buffer to 64k per socket.
*/
int DEFAULT_TCP_RECEIVE_BUFFER_SIZE = 64 * 1024;
/**
* Set HTTP chunk maximum size to 8k.
* See {@link io.netty.handler.codec.http.HttpClientCodec}.
*/
int DEFAULT_MAX_CHUNK_SIZE = 8 * 1024;
/**
* Set HTTP initial line length to 4k.
* See {@link io.netty.handler.codec.http.HttpClientCodec}.
*/
int DEFAULT_MAX_INITIAL_LINE_LENGTH = 4 * 1024;
/**
* Set HTTP maximum headers size to 8k.
* See {@link io.netty.handler.codec.http.HttpClientCodec}.
*/
int DEFAULT_MAX_HEADERS_SIZE = 8 * 1024;
/**
* Set maximum content length to 100 MB.
*/
int DEFAULT_MAX_CONTENT_LENGTH = 100 * 1024 * 1024;
/**
* This is Netty's default.
* See {@link io.netty.handler.codec.MessageAggregator#DEFAULT_MAX_COMPOSITEBUFFER_COMPONENTS}.
*/
int DEFAULT_MAX_COMPOSITE_BUFFER_COMPONENTS = 1024;
/**
* Allow maximum concurrent connections to an {@link InetAddressKey}.
* Usually, browsers restrict concurrent connections to 8 for a single address.
*/
int DEFAULT_MAX_CONNECTIONS = 8;
/**
* Default read/write timeout in milliseconds.
*/
int DEFAULT_TIMEOUT_MILLIS = 5000;
/**
* Default for gzip codec.
*/
boolean DEFAULT_ENABLE_GZIP = true;
/**
* Default for HTTP/2 only.
*/
boolean DEFAULT_INSTALL_HTTP_UPGRADE2 = false;
/**
* Default SSL provider.
*/
SslProvider DEFAULT_SSL_PROVIDER = SslProvider.OPENSSL;
Iterable<String> DEFAULT_CIPHERS = Http2SecurityUtil.CIPHERS;
CipherSuiteFilter DEFAULT_CIPHER_SUITE_FILTER = SupportedCipherSuiteFilter.INSTANCE;
TrustManagerFactory DEFAULT_TRUST_MANAGER_FACTORY = InsecureTrustManagerFactory.INSTANCE;
boolean DEFAULT_USE_SERVER_NAME_IDENTIFICATION = true;
/**
* Default for SSL client authentication.
*/
SslClientAuthMode DEFAULT_SSL_CLIENT_AUTH_MODE = SslClientAuthMode.NONE;
}

View file

@ -0,0 +1,379 @@
/*
* Copyright 2017 Jörg Prante
*
* Jörg Prante licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package org.xbib.netty.http.client;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.ChannelPromise;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.socket.ChannelInputShutdownReadComplete;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpClientUpgradeHandler;
import io.netty.handler.codec.http.HttpContentDecompressor;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http2.DefaultHttp2Connection;
import io.netty.handler.codec.http2.DelegatingDecompressorFrameListener;
import io.netty.handler.codec.http2.Http2ClientUpgradeCodec;
import io.netty.handler.codec.http2.Http2Connection;
import io.netty.handler.codec.http2.Http2ConnectionPrefaceWrittenEvent;
import io.netty.handler.codec.http2.Http2FrameLogger;
import io.netty.handler.codec.http2.Http2Settings;
import io.netty.handler.codec.http2.HttpToHttp2ConnectionHandler;
import io.netty.handler.codec.http2.HttpToHttp2ConnectionHandlerBuilder;
import io.netty.handler.codec.http2.InboundHttp2ToHttpAdapterBuilder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.ssl.ApplicationProtocolConfig;
import io.netty.handler.ssl.ApplicationProtocolNames;
import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler;
import io.netty.handler.ssl.SslCloseCompletionEvent;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.timeout.ReadTimeoutHandler;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.net.ssl.SNIHostName;
import javax.net.ssl.SNIServerName;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLParameters;
/**
*/
class HttpClientChannelInitializer extends ChannelInitializer<SocketChannel> {
private static final Logger logger = Logger.getLogger(HttpClientChannelInitializer.class.getName());
private static final Http2FrameLogger frameLogger =
new Http2FrameLogger(LogLevel.TRACE, HttpClientChannelInitializer.class);
private final HttpClientChannelContext context;
private final Http1Handler http1Handler;
private final Http2Handler http2Handler;
private InetAddressKey key;
private Http2SettingsHandler http2SettingsHandler;
private UserEventLogger userEventLogger;
HttpClientChannelInitializer(HttpClientChannelContext context, Http1Handler http1Handler,
Http2Handler http2Handler) {
this.context = context;
this.http1Handler = http1Handler;
this.http2Handler = http2Handler;
}
void initChannel(SocketChannel ch, InetAddressKey key) throws Exception {
this.key = key;
initChannel(ch);
}
@Override
protected void initChannel(SocketChannel ch) throws Exception {
logger.log(Level.FINE, () -> "initChannel with key = " + key);
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new TrafficLoggingHandler());
if (context.getHttpProxyHandler() != null) {
pipeline.addLast(context.getHttpProxyHandler());
}
if (context.getSocks4ProxyHandler() != null) {
pipeline.addLast(context.getSocks4ProxyHandler());
}
if (context.getSocks5ProxyHandler() != null) {
pipeline.addLast(context.getSocks5ProxyHandler());
}
pipeline.addLast(new ReadTimeoutHandler(context.getReadTimeoutMillis(), TimeUnit.MILLISECONDS));
http2SettingsHandler = new Http2SettingsHandler(ch.newPromise());
userEventLogger = new UserEventLogger();
if (context.getSslProvider() != null && key.isSecure()) {
configureEncrypted(ch);
} else {
configureClearText(ch);
}
logger.log(Level.FINE, () -> "initChannel pipeline handler names = " + ch.pipeline().names());
}
Http2SettingsHandler getHttp2SettingsHandler() {
return http2SettingsHandler;
}
Http2Handler getHttp2Handler() {
return http2Handler;
}
private void configureClearText(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
if (key.getVersion().majorVersion() == 1) {
HttpClientCodec http1connectionHandler = createHttp1ConnectionHandler();
pipeline.addLast(http1connectionHandler);
configureHttp1Pipeline(pipeline);
} else if (key.getVersion().majorVersion() == 2) {
HttpToHttp2ConnectionHandler http2connectionHandler = createHttp2ConnectionHandler();
if (context.isInstallHttp2Upgrade()) {
HttpClientCodec http1connectionHandler = createHttp1ConnectionHandler();
Http2ClientUpgradeCodec upgradeCodec =
new Http2ClientUpgradeCodec(http2connectionHandler);
HttpClientUpgradeHandler upgradeHandler =
new HttpClientUpgradeHandler(http1connectionHandler, upgradeCodec, context.getMaxContentLength());
UpgradeRequestHandler upgradeRequestHandler =
new UpgradeRequestHandler();
pipeline.addLast(upgradeHandler);
pipeline.addLast(upgradeRequestHandler);
} else {
pipeline.addLast(http2connectionHandler);
}
configureHttp2Pipeline(pipeline);
configureHttp1Pipeline(pipeline);
}
}
private void configureEncrypted(SocketChannel ch) throws SSLException {
ChannelPipeline pipeline = ch.pipeline();
if (key.getVersion().majorVersion() == 2) {
final SslContext http2SslContext = SslContextBuilder.forClient()
.sslProvider(context.getSslProvider())
.keyManager(context.getKeyCertChainInputStream(), context.getKeyInputStream(), context.getKeyPassword())
.ciphers(context.getCiphers(), context.getCipherSuiteFilter())
.trustManager(context.getTrustManagerFactory())
.applicationProtocolConfig(new ApplicationProtocolConfig(
ApplicationProtocolConfig.Protocol.ALPN,
ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE,
ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT,
ApplicationProtocolNames.HTTP_2,
ApplicationProtocolNames.HTTP_1_1))
.build();
SslHandler sslHandler = http2SslContext.newHandler(ch.alloc());
try {
SSLEngine engine = sslHandler.engine();
if (context.isUseServerNameIdentification()) {
// execute DNS lookup and/or reverse lookup if IP for host name
String fullQualifiedHostname = key.getInetSocketAddress().getHostName();
SSLParameters params = engine.getSSLParameters();
params.setServerNames(Arrays.asList(new SNIServerName[]{new SNIHostName(fullQualifiedHostname)}));
engine.setSSLParameters(params);
}
switch (context.getSslClientAuthMode()) {
case NEED:
engine.setNeedClientAuth(true);
break;
case WANT:
engine.setWantClientAuth(true);
break;
default:
break;
}
} finally {
pipeline.addLast(sslHandler);
}
pipeline.addLast(new Http2NegotiationHandler(ApplicationProtocolNames.HTTP_1_1));
} else if (key.getVersion().majorVersion() == 1) {
final SslContext hhtp1SslContext = SslContextBuilder.forClient()
.sslProvider(context.getSslProvider())
.keyManager(context.getKeyCertChainInputStream(), context.getKeyInputStream(), context.getKeyPassword())
.ciphers(context.getCiphers(), context.getCipherSuiteFilter())
.trustManager(context.getTrustManagerFactory())
.build();
SslHandler sslHandler = hhtp1SslContext.newHandler(ch.alloc());
switch (context.getSslClientAuthMode()) {
case NEED:
sslHandler.engine().setNeedClientAuth(true);
break;
case WANT:
sslHandler.engine().setWantClientAuth(true);
break;
default:
break;
}
pipeline.addLast(sslHandler);
HttpClientCodec http1connectionHandler = createHttp1ConnectionHandler();
pipeline.addLast(http1connectionHandler);
configureHttp1Pipeline(pipeline);
}
}
private void configureHttp1Pipeline(ChannelPipeline pipeline) {
if (context.isGzipEnabled()) {
pipeline.addLast(new HttpContentDecompressor());
}
HttpObjectAggregator httpObjectAggregator =
new HttpObjectAggregator(context.getMaxContentLength(), false);
httpObjectAggregator.setMaxCumulationBufferComponents(context.getMaxCompositeBufferComponents());
pipeline.addLast(httpObjectAggregator);
pipeline.addLast(http1Handler);
}
private void configureHttp2Pipeline(ChannelPipeline pipeline) {
pipeline.addLast(http2SettingsHandler);
pipeline.addLast(userEventLogger);
pipeline.addLast(http2Handler);
}
private HttpClientCodec createHttp1ConnectionHandler() {
return new HttpClientCodec(context.getMaxInitialLineLength(), context.getMaxHeaderSize(), context.getMaxChunkSize());
}
private HttpToHttp2ConnectionHandler createHttp2ConnectionHandler() {
final Http2Connection http2Connection = new DefaultHttp2Connection(false);
return new HttpToHttp2ConnectionHandlerBuilder()
.connection(http2Connection)
.frameLogger(frameLogger)
.frameListener(new DelegatingDecompressorFrameListener(http2Connection,
new InboundHttp2ToHttpAdapterBuilder(http2Connection)
.maxContentLength(context.getMaxContentLength())
.propagateSettings(true)
.validateHttpHeaders(false)
.build()))
.build();
}
private class Http2NegotiationHandler extends ApplicationProtocolNegotiationHandler {
Http2NegotiationHandler(String fallbackProtocol) {
super(fallbackProtocol);
}
@Override
protected void configurePipeline(ChannelHandlerContext ctx, String protocol) throws Exception {
if (ApplicationProtocolNames.HTTP_2.equals(protocol)) {
HttpToHttp2ConnectionHandler http2connectionHandler = createHttp2ConnectionHandler();
ctx.pipeline().addLast(http2connectionHandler);
configureHttp2Pipeline(ctx.pipeline());
logger.log(Level.FINE, "negotiated HTTP/2: handler = " + ctx.pipeline().names());
return;
}
if (ApplicationProtocolNames.HTTP_1_1.equals(protocol)) {
HttpClientCodec http1connectionHandler = createHttp1ConnectionHandler();
ctx.pipeline().addLast(http1connectionHandler);
configureHttp1Pipeline(ctx.pipeline());
logger.log(Level.FINE, "negotiated HTTP/1.1: handler = " + ctx.pipeline().names());
return;
}
ctx.close();
throw new IllegalStateException("unexpected protocol: " + protocol);
}
}
class Http2SettingsHandler extends SimpleChannelInboundHandler<Http2Settings> {
private final ChannelPromise promise;
Http2SettingsHandler(ChannelPromise promise) {
this.promise = promise;
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, Http2Settings msg) throws Exception {
promise.setSuccess();
ctx.pipeline().remove(this);
logger.log(Level.FINE, "settings handler removed, pipeline = " + ctx.pipeline().names());
}
/**
* Forward channel exceptions to the exception listener.
* @param ctx the channel handler context
* @param cause the cause of the exception
* @throws Exception if forwarding fails
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ExceptionListener exceptionListener =
ctx.channel().attr(HttpClientChannelContext.EXCEPTION_LISTENER_ATTRIBUTE_KEY).get();
logger.log(Level.FINE, () -> "exceptionCaught");
if (exceptionListener != null) {
exceptionListener.onException(cause);
}
final HttpRequestContext httpRequestContext =
ctx.channel().attr(HttpClientChannelContext.REQUEST_CONTEXT_ATTRIBUTE_KEY).get();
httpRequestContext.fail(cause.getMessage());
}
void awaitSettings(HttpRequestContext httpRequestContext, ExceptionListener exceptionListener) throws Exception {
int timeout = httpRequestContext.getTimeout();
if (!promise.awaitUninterruptibly(timeout, TimeUnit.MILLISECONDS)) {
IllegalStateException exception = new IllegalStateException("time out while waiting for HTTP/2 settings");
if (exceptionListener != null) {
exceptionListener.onException(exception);
httpRequestContext.fail(exception.getMessage());
}
throw exception;
}
if (!promise.isSuccess()) {
throw new RuntimeException(promise.cause());
}
}
}
@Sharable
private class UpgradeRequestHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
DefaultFullHttpRequest upgradeRequest = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/");
ctx.writeAndFlush(upgradeRequest);
super.channelActive(ctx);
ctx.pipeline().remove(this);
logger.log(Level.FINE, "upgrade request handler removed, pipeline = " + ctx.pipeline().names());
}
/**
* Forward channel exceptions to the exception listener.
* @param ctx the channel handler context
* @param cause the cause of the exception
* @throws Exception if forwarding fails
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ExceptionListener exceptionListener =
ctx.channel().attr(HttpClientChannelContext.EXCEPTION_LISTENER_ATTRIBUTE_KEY).get();
logger.log(Level.FINE, () -> "exceptionCaught");
if (exceptionListener != null) {
exceptionListener.onException(cause);
}
final HttpRequestContext httpRequestContext =
ctx.channel().attr(HttpClientChannelContext.REQUEST_CONTEXT_ATTRIBUTE_KEY).get();
httpRequestContext.fail(cause.getMessage());
}
}
@Sharable
private class UserEventLogger extends ChannelInboundHandlerAdapter {
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
logger.log(Level.FINE, () -> "got user event " + evt);
if (evt instanceof Http2ConnectionPrefaceWrittenEvent ||
evt instanceof SslCloseCompletionEvent ||
evt instanceof ChannelInputShutdownReadComplete) {
// Expected events
logger.log(Level.FINE, () -> "user event is expected: " + evt);
return;
}
super.userEventTriggered(ctx, evt);
}
}
}

View file

@ -0,0 +1,74 @@
/*
* Copyright 2017 Jörg Prante
*
* Jörg Prante licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package org.xbib.netty.http.client;
import io.netty.channel.Channel;
import io.netty.channel.pool.ChannelPoolHandler;
import io.netty.channel.socket.SocketChannel;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
*
*/
public class HttpClientChannelPoolHandler implements ChannelPoolHandler {
private static final Logger logger = Logger.getLogger(HttpClientChannelPoolHandler.class.getName());
private final HttpClientChannelInitializer channelInitializer;
private final InetAddressKey key;
private final AtomicInteger active = new AtomicInteger();
private int peak;
HttpClientChannelPoolHandler(HttpClientChannelInitializer channelInitializer, InetAddressKey key) {
this.channelInitializer = channelInitializer;
this.key = key;
}
@Override
public void channelCreated(Channel ch) throws Exception {
logger.log(Level.INFO, () -> "channel created " + ch + " key:" + key);
channelInitializer.initChannel((SocketChannel) ch, key);
int n = active.incrementAndGet();
if (n > peak) {
peak = n;
}
}
@Override
public void channelAcquired(Channel ch) throws Exception {
logger.log(Level.INFO, () -> "channel acquired from pool " + ch);
}
@Override
public void channelReleased(Channel ch) throws Exception {
logger.log(Level.INFO, () -> "channel released to pool " + ch);
active.decrementAndGet();
}
public int getActive() {
return active.get();
}
public int getPeak() {
return peak;
}
}

View file

@ -0,0 +1,65 @@
/*
* Copyright 2017 Jörg Prante
*
* Jörg Prante licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package org.xbib.netty.http.client;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.pool.AbstractChannelPoolMap;
import io.netty.channel.pool.FixedChannelPool;
/**
*
*/
public class HttpClientChannelPoolMap extends AbstractChannelPoolMap<InetAddressKey, FixedChannelPool> {
private final HttpClient httpClient;
private final HttpClientChannelContext httpClientChannelContext;
private final Bootstrap bootstrap;
private final int maxConnections;
private HttpClientChannelInitializer httpClientChannelInitializer;
private HttpClientChannelPoolHandler httpClientChannelPoolHandler;
HttpClientChannelPoolMap(HttpClient httpClient,
HttpClientChannelContext httpClientChannelContext,
Bootstrap bootstrap,
int maxConnections) {
this.httpClient = httpClient;
this.httpClientChannelContext = httpClientChannelContext;
this.bootstrap = bootstrap;
this.maxConnections = maxConnections;
}
@Override
protected FixedChannelPool newPool(InetAddressKey key) {
this.httpClientChannelInitializer = new HttpClientChannelInitializer(httpClientChannelContext,
new Http1Handler(httpClient), new Http2Handler(httpClient));
this.httpClientChannelPoolHandler = new HttpClientChannelPoolHandler(httpClientChannelInitializer, key);
return new FixedChannelPool(bootstrap.remoteAddress(key.getInetSocketAddress()),
httpClientChannelPoolHandler, maxConnections);
}
public HttpClientChannelInitializer getHttpClientChannelInitializer() {
return httpClientChannelInitializer;
}
public HttpClientChannelPoolHandler getHttpClientChannelPoolHandler() {
return httpClientChannelPoolHandler;
}
}

View file

@ -0,0 +1,352 @@
package org.xbib.netty.http.client;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.DefaultHttpHeaders;
import io.netty.handler.codec.http.DefaultHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http2.HttpConversionUtil;
import io.netty.util.AsciiString;
import io.netty.util.CharsetUtil;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
/**
*
*/
public class HttpClientRequestBuilder implements HttpRequestBuilder, HttpRequestDefaults {
private static final AtomicInteger streamId = new AtomicInteger(3);
private final HttpClient httpClient;
private final ByteBufAllocator byteBufAllocator;
private final DefaultHttpHeaders headers;
private final List<String> removeHeaders;
private final HttpMethod httpMethod;
private int timeout = DEFAULT_TIMEOUT_MILLIS;
private HttpVersion httpVersion = DEFAULT_HTTP_VERSION;
private String userAgent = DEFAULT_USER_AGENT;
private boolean gzip = DEFAULT_GZIP;
private boolean followRedirect = DEFAULT_FOLLOW_REDIRECT;
private int maxRedirects = DEFAULT_MAX_REDIRECT;
private URL url;
private ByteBuf body;
private HttpRequest httpRequest;
private HttpRequestContext httpRequestContext;
private HttpResponseListener httpResponseListener;
private ExceptionListener exceptionListener;
/**
* Construct HTTP client request builder.
*
* @param httpClient HTTP client
* @param httpMethod HTTP method
* @param byteBufAllocator byte buf allocator
*/
HttpClientRequestBuilder(HttpClient httpClient, HttpMethod httpMethod, ByteBufAllocator byteBufAllocator) {
this.httpClient = httpClient;
this.httpMethod = httpMethod;
this.byteBufAllocator = byteBufAllocator;
this.headers = new DefaultHttpHeaders();
this.removeHeaders = new ArrayList<>();
}
@Override
public HttpRequestBuilder setTimeout(int timeout) {
this.timeout = timeout;
return this;
}
protected int getTimeout() {
return timeout;
}
@Override
public HttpRequestBuilder setURL(String url) {
try {
this.url = new URL(url);
} catch (MalformedURLException e) {
throw new UncheckedIOException(e);
}
return this;
}
protected URL getURL() {
return url;
}
@Override
public HttpRequestBuilder addHeader(String name, Object value) {
headers.add(name, value);
return this;
}
@Override
public HttpRequestBuilder setHeader(String name, Object value) {
headers.set(name, value);
return this;
}
@Override
public HttpRequestBuilder removeHeader(String name) {
removeHeaders.add(name);
return this;
}
@Override
public HttpRequestBuilder contentType(String contentType) {
addHeader(HttpHeaderNames.CONTENT_TYPE, contentType);
return this;
}
@Override
public HttpRequestBuilder setVersion(String httpVersion) {
this.httpVersion = HttpVersion.valueOf(httpVersion);
return this;
}
protected HttpVersion getVersion() {
return httpVersion;
}
@Override
public HttpRequestBuilder acceptGzip(boolean gzip) {
this.gzip = gzip;
return this;
}
@Override
public HttpRequestBuilder setFollowRedirect(boolean followRedirect) {
this.followRedirect = followRedirect;
return this;
}
protected boolean isFollowRedirect() {
return followRedirect;
}
@Override
public HttpRequestBuilder setMaxRedirects(int maxRedirects) {
this.maxRedirects = maxRedirects;
return this;
}
protected int getMaxRedirects() {
return maxRedirects;
}
@Override
public HttpRequestBuilder setUserAgent(String userAgent) {
this.userAgent = userAgent;
return this;
}
@Override
public HttpRequestBuilder text(String text) throws IOException {
setBody(text, HttpHeaderValues.TEXT_PLAIN);
return this;
}
@Override
public HttpRequestBuilder json(String json) throws IOException {
setBody(json, HttpHeaderValues.APPLICATION_JSON);
return this;
}
@Override
public HttpRequestBuilder xml(String xml) throws IOException {
setBody(xml, "application/xml");
return this;
}
@Override
public HttpRequestBuilder setBody(CharSequence charSequence, String contentType) throws IOException {
setBody(charSequence.toString().getBytes(CharsetUtil.UTF_8), AsciiString.of(contentType));
return this;
}
@Override
public HttpRequestBuilder setBody(byte[] buf, String contentType) throws IOException {
setBody(buf, AsciiString.of(contentType));
return this;
}
@Override
public HttpRequestBuilder setBody(ByteBuf body, String contentType) throws IOException {
setBody(body, AsciiString.of(contentType));
return this;
}
@Override
public HttpRequest build() {
if (url == null) {
throw new IllegalStateException("URL not set");
}
if (url.getHost() == null) {
throw new IllegalStateException("URL host not set: " + url);
}
DefaultHttpRequest httpRequest = createHttpRequest();
String scheme = url.getProtocol();
StringBuilder sb = new StringBuilder(url.getHost());
int defaultPort = "http".equals(scheme) ? 80 : "https".equals(scheme) ? 443 : -1;
if (defaultPort != -1 && url.getPort() != -1 && defaultPort != url.getPort()) {
sb.append(":").append(url.getPort());
}
if (httpVersion.majorVersion() == 2) {
// this is a hack, because we only use the "origin-form" in request URIs
httpRequest.headers().set(HttpConversionUtil.ExtensionHeaderNames.SCHEME.text(), scheme);
}
httpRequest.headers().add(HttpHeaderNames.HOST, sb.toString());
httpRequest.headers().add(HttpHeaderNames.DATE,
DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.ofInstant(Instant.now(), ZoneId.of("GMT"))));
if (userAgent != null) {
httpRequest.headers().add(HttpHeaderNames.USER_AGENT, userAgent);
}
if (gzip) {
httpRequest.headers().add(HttpHeaderNames.ACCEPT_ENCODING, "gzip");
}
httpRequest.headers().setAll(headers);
if (!httpRequest.headers().contains(HttpHeaderNames.ACCEPT)) {
httpRequest.headers().add(HttpHeaderNames.ACCEPT, "*/*");
}
// RFC 2616 Section 14.10
// "An HTTP/1.1 client that does not support persistent connections MUST include the "close" connection
// option in every request message."
if (httpVersion.majorVersion() == 1 && !httpRequest.headers().contains(HttpHeaderNames.CONNECTION)) {
httpRequest.headers().add(HttpHeaderNames.CONNECTION, "close");
}
// forced removal of headers, at last
for (String headerName : removeHeaders) {
httpRequest.headers().remove(headerName);
}
return httpRequest;
}
private DefaultHttpRequest createHttpRequest() {
// Regarding request-target URI:
// RFC https://tools.ietf.org/html/rfc7230#section-5.3.2
// would allow url.toExternalForm as absolute-form,
// but some servers do not support that. So, we create origin-form.
// But for HTTP/2, we should create the absolute-form, otherwise
// netty will throw "java.lang.IllegalArgumentException: :scheme must be specified."
String requestTarget = toOriginForm(url);
return body == null ?
new DefaultHttpRequest(httpVersion, httpMethod, requestTarget) :
new DefaultFullHttpRequest(httpVersion, httpMethod, requestTarget, body);
}
private String toOriginForm(URL base) {
StringBuilder sb = new StringBuilder();
String path = base.getPath() != null && !base.getPath().isEmpty() ? base.getPath() : "/";
String query = base.getQuery();
String ref = base.getRef();
if (path.charAt(0) != '/') {
sb.append('/');
}
sb.append(path);
if (query != null && !query.isEmpty()) {
sb.append('?').append(query);
}
if (ref != null && !ref.isEmpty()) {
sb.append('#').append(ref);
}
return sb.toString();
}
private void addHeader(AsciiString name, Object value) {
headers.add(name, value);
}
private void setBody(CharSequence charSequence, AsciiString contentType) throws IOException {
setBody(charSequence.toString().getBytes(CharsetUtil.UTF_8), contentType);
}
private void setBody(byte[] buf, AsciiString contentType) throws IOException {
ByteBuf buffer = byteBufAllocator.buffer(buf.length).writeBytes(buf);
setBody(buffer, contentType);
}
private void setBody(ByteBuf body, AsciiString contentType) throws IOException {
this.body = body;
addHeader(HttpHeaderNames.CONTENT_LENGTH, (long) body.readableBytes());
addHeader(HttpHeaderNames.CONTENT_TYPE, contentType);
}
@Override
public HttpRequestBuilder onResponse(HttpResponseListener httpResponseListener) {
this.httpResponseListener = httpResponseListener;
return this;
}
@Override
public HttpRequestBuilder onError(ExceptionListener exceptionListener) {
this.exceptionListener = exceptionListener;
return this;
}
@Override
public HttpRequestContext execute() {
if (httpRequest == null) {
httpRequest = build();
}
if (httpRequestContext == null) {
httpRequestContext = new HttpRequestContext(getURL(),
httpRequest,
new AtomicBoolean(false),
new AtomicBoolean(false),
getTimeout(), System.currentTimeMillis(),
isFollowRedirect(), getMaxRedirects(), new AtomicInteger(0),
new CountDownLatch(1), streamId.get());
}
if (httpResponseListener == null) {
httpResponseListener = httpRequestContext;
}
httpClient.dispatch(httpRequestContext, httpResponseListener, exceptionListener);
return httpRequestContext;
}
@Override
public <T> CompletableFuture<T> execute(Function<FullHttpResponse, T> supplier) {
final CompletableFuture<T> completableFuture = new CompletableFuture<>();
onResponse(response -> completableFuture.complete(supplier.apply(response)));
onError(completableFuture::completeExceptionally);
execute();
return completableFuture;
}
}

View file

@ -0,0 +1,30 @@
/*
* Copyright 2017 Jörg Prante
*
* Jörg Prante licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package org.xbib.netty.http.client;
import java.util.concurrent.ThreadFactory;
class HttpClientThreadFactory implements ThreadFactory {
private int number = 0;
@Override
public Thread newThread(Runnable runnable) {
Thread thread = new Thread(runnable, "org-xbib-netty-http-client-pool-" + (number++));
thread.setDaemon(true);
return thread;
}
}

View file

@ -0,0 +1,58 @@
/*
* Copyright 2017 Jörg Prante
*
* Jörg Prante licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package org.xbib.netty.http.client;
import io.netty.bootstrap.Bootstrap;
import java.util.Optional;
/**
*/
public final class HttpClientUserAgent {
/**
* The default valut for {@code User-Agent}.
*/
private static final String USER_AGENT = String.format("XbibHttpClient/%s (Java/%s/%s) (Netty/%s)",
httpClientVersion(), javaVendor(), javaVersion(), nettyVersion());
private HttpClientUserAgent() {
}
public static String getUserAgent() {
return USER_AGENT;
}
private static String httpClientVersion() {
return Optional.ofNullable(HttpClient.class.getPackage().getImplementationVersion())
.orElse("unknown");
}
private static String javaVendor() {
return Optional.ofNullable(System.getProperty("java.vendor"))
.orElse("unknown");
}
private static String javaVersion() {
return Optional.ofNullable(System.getProperty("java.version"))
.orElse("unknown");
}
private static String nettyVersion() {
return Optional.ofNullable(Bootstrap.class.getPackage().getImplementationVersion())
.orElse("unknown");
}
}

View file

@ -0,0 +1,73 @@
/*
* Copyright 2017 Jörg Prante
*
* Jörg Prante licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package org.xbib.netty.http.client;
import io.netty.buffer.ByteBuf;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpRequest;
import java.io.IOException;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
/**
*/
public interface HttpRequestBuilder {
HttpRequestBuilder setTimeout(int timeout);
HttpRequestBuilder setVersion(String httpVersion);
HttpRequestBuilder setURL(String url);
HttpRequestBuilder setHeader(String name, Object value);
HttpRequestBuilder addHeader(String name, Object value);
HttpRequestBuilder removeHeader(String name);
HttpRequestBuilder contentType(String contentType);
HttpRequestBuilder acceptGzip(boolean gzip);
HttpRequestBuilder setFollowRedirect(boolean followRedirect);
HttpRequestBuilder setMaxRedirects(int maxRedirects);
HttpRequestBuilder setUserAgent(String userAgent);
HttpRequestBuilder setBody(CharSequence charSequence, String contentType) throws IOException;
HttpRequestBuilder text(String text) throws IOException;
HttpRequestBuilder json(String jsonText) throws IOException;
HttpRequestBuilder xml(String xmlText) throws IOException;
HttpRequestBuilder setBody(byte[] buf, String contentType) throws IOException;
HttpRequestBuilder setBody(ByteBuf body, String contentType) throws IOException;
HttpRequestBuilder onError(ExceptionListener exceptionListener);
HttpRequestBuilder onResponse(HttpResponseListener httpResponseListener);
HttpRequest build();
HttpRequestContext execute();
<T> CompletableFuture<T> execute(Function<FullHttpResponse, T> supplier);
}

View file

@ -0,0 +1,185 @@
/*
* Copyright 2017 Jörg Prante
*
* Jörg Prante licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package org.xbib.netty.http.client;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpRequest;
import java.net.URL;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
*
*/
public final class HttpRequestContext implements HttpResponseListener, HttpRequestDefaults {
private static final Logger logger = Logger.getLogger(HttpRequestContext.class.getName());
private final URL url;
private final HttpRequest httpRequest;
private final AtomicBoolean succeeded;
private final AtomicBoolean failed;
private final boolean followRedirect;
private final int maxRedirects;
private final AtomicInteger redirectCount;
private final Integer timeout;
private final Long startTime;
private final CountDownLatch latch;
private final Integer streamId;
private FullHttpResponse httpResponse;
private Long stopTime;
HttpRequestContext(URL url, HttpRequest httpRequest,
AtomicBoolean succeeded, AtomicBoolean failed,
int timeout, Long startTime,
boolean followRedirect, int maxRedirects, AtomicInteger redirectCount,
CountDownLatch latch, Integer streamId) {
this.url = url;
this.httpRequest = httpRequest;
this.succeeded = succeeded;
this.failed = failed;
this.timeout = timeout;
this.startTime = startTime;
this.followRedirect = followRedirect;
this.maxRedirects = maxRedirects;
this.redirectCount = redirectCount;
this.latch = latch;
this.streamId = streamId;
}
HttpRequestContext(URL url, HttpRequest httpRequest, HttpRequestContext httpRequestContext) {
this.url = url;
this.httpRequest = httpRequest;
this.succeeded = httpRequestContext.succeeded;
this.failed = httpRequestContext.failed;
this.failed.lazySet(false); // reset
this.timeout = httpRequestContext.timeout;
this.startTime = httpRequestContext.startTime;
this.followRedirect = httpRequestContext.followRedirect;
this.maxRedirects = httpRequestContext.maxRedirects;
this.redirectCount = httpRequestContext.redirectCount;
this.latch = httpRequestContext.latch;
this.streamId = httpRequestContext.streamId;
}
public URL getURL() {
return url;
}
public HttpRequest getHttpRequest() {
return httpRequest;
}
public int getTimeout() {
return timeout;
}
public long getStartTime() {
return startTime;
}
public boolean isSucceeded() {
return succeeded.get();
}
public boolean isFailed() {
return failed.get();
}
public boolean isFollowRedirect() {
return followRedirect;
}
public int getMaxRedirects() {
return maxRedirects;
}
public AtomicInteger getRedirectCount() {
return redirectCount;
}
public boolean isExpired() {
return timeout != null && System.currentTimeMillis() > startTime + timeout;
}
public long took() {
return stopTime != null ? stopTime - startTime : -1L;
}
public long remaining() {
return (startTime + timeout) - System.currentTimeMillis();
}
public CountDownLatch getLatch() {
return latch;
}
public Integer getStreamId() {
return streamId;
}
public HttpRequestContext get() throws InterruptedException {
return waitFor(DEFAULT_TIMEOUT_MILLIS, TimeUnit.SECONDS);
}
public HttpRequestContext waitFor(long value, TimeUnit timeUnit) throws InterruptedException {
latch.await(value, timeUnit);
stopTime = System.currentTimeMillis();
return this;
}
public void success(String reason) {
logger.log(Level.FINE, () -> "success because of " + reason);
if (succeeded.compareAndSet(false, true)) {
latch.countDown();
}
}
public void fail(String reason) {
logger.log(Level.FINE, () -> "failed because of " + reason);
if (failed.compareAndSet(false, true)) {
latch.countDown();
}
}
@Override
public void onResponse(FullHttpResponse fullHttpResponse) {
this.httpResponse = fullHttpResponse;
}
public FullHttpResponse getHttpResponse() {
return httpResponse;
}
}

View file

@ -0,0 +1,35 @@
/*
* Copyright 2017 Jörg Prante
*
* Jörg Prante licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package org.xbib.netty.http.client;
import io.netty.handler.codec.http.HttpVersion;
/**
*/
public interface HttpRequestDefaults {
int DEFAULT_TIMEOUT_MILLIS = 5000;
HttpVersion DEFAULT_HTTP_VERSION = HttpVersion.HTTP_1_1;
String DEFAULT_USER_AGENT = HttpClientUserAgent.getUserAgent();
boolean DEFAULT_GZIP = true;
boolean DEFAULT_FOLLOW_REDIRECT = true;
int DEFAULT_MAX_REDIRECT = 10;
}

View file

@ -0,0 +1,26 @@
/*
* Copyright 2017 Jörg Prante
*
* Jörg Prante licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package org.xbib.netty.http.client;
import io.netty.handler.codec.http.FullHttpResponse;
/**
*/
@FunctionalInterface
public interface HttpResponseListener {
void onResponse(FullHttpResponse fullHttpResponse);
}

View file

@ -0,0 +1,78 @@
/*
* Copyright 2017 Jörg Prante
*
* Jörg Prante licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package org.xbib.netty.http.client;
import io.netty.handler.codec.http.HttpVersion;
import java.net.InetSocketAddress;
import java.net.URL;
/**
*/
public class InetAddressKey {
private final InetSocketAddress inetSocketAddress;
private final HttpVersion version;
private final Boolean secure;
InetAddressKey(URL url, HttpVersion version) {
this.version = version;
String protocol = url.getProtocol();
this.secure = "https".equals(protocol);
int port = url.getPort();
if (port == -1) {
port = "http".equals(protocol) ? 80 : (secure ? 443 : -1);
}
this.inetSocketAddress = new InetSocketAddress(url.getHost(), port);
}
InetAddressKey(InetSocketAddress inetSocketAddress, HttpVersion version, boolean secure) {
this.inetSocketAddress = inetSocketAddress;
this.version = version;
this.secure = secure;
}
InetSocketAddress getInetSocketAddress() {
return inetSocketAddress;
}
HttpVersion getVersion() {
return version;
}
boolean isSecure() {
return secure;
}
public String toString() {
return inetSocketAddress + " (version:" + version + ",secure:" + secure + ")";
}
@Override
public boolean equals(Object object) {
return object instanceof InetAddressKey &&
inetSocketAddress.equals(((InetAddressKey) object).inetSocketAddress) &&
version.equals(((InetAddressKey) object).version) &&
secure == ((InetAddressKey) object).secure;
}
@Override
public int hashCode() {
return inetSocketAddress.hashCode() ^ version.hashCode() ^ secure.hashCode();
}
}

View file

@ -0,0 +1,23 @@
/*
* Copyright 2017 Jörg Prante
*
* Jörg Prante licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package org.xbib.netty.http.client;
/**
*
*/
public enum SslClientAuthMode {
NONE, WANT, NEED
}

View file

@ -0,0 +1,56 @@
/*
* Copyright 2017 Jörg Prante
*
* Jörg Prante licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package org.xbib.netty.http.client;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
/**
* A Netty handler that logs the I/O traffic of a connection.
*/
public final class TrafficLoggingHandler extends LoggingHandler {
public TrafficLoggingHandler() {
super("client", LogLevel.TRACE);
}
@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
ctx.fireChannelRegistered();
}
@Override
public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
ctx.fireChannelUnregistered();
}
@Override
public void flush(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
if (msg instanceof ByteBuf && !((ByteBuf) msg).isReadable()) {
ctx.write(msg, promise);
} else {
super.write(ctx, msg, promise);
}
}
}

View file

@ -0,0 +1,4 @@
/**
* Classes for Netty HTTP client.
*/
package org.xbib.netty.http.client;

View file

@ -0,0 +1,71 @@
package org.xbib.netty.http.client.test;
import org.junit.Test;
import org.xbib.netty.http.client.HttpClient;
import java.nio.charset.StandardCharsets;
import java.util.logging.ConsoleHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;
/**
*/
public class AkamaiTest {
static {
System.setProperty("java.util.logging.SimpleFormatter.format",
"%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$-7s [%3$s] %2$s %5$s %6$s%n");
LogManager.getLogManager().reset();
Logger rootLogger = LogManager.getLogManager().getLogger("");
Handler handler = new ConsoleHandler();
handler.setFormatter(new SimpleFormatter());
rootLogger.addHandler(handler);
rootLogger.setLevel(Level.ALL);
for (Handler h : rootLogger.getHandlers()) {
handler.setFormatter(new SimpleFormatter());
h.setLevel(Level.ALL);
}
}
private static final Logger logger = Logger.getLogger("");
@Test
public void testAkamai() throws Exception {
// here we can not deal with server PUSH_PROMISE as response to headers, a go-away frame is written.
// Probably because promised stream id is 2 which is smaller than 3?
/*
----------------INBOUND--------------------
[id: 0x27d23874, L:/192.168.178.23:52376 - R:http2.akamai.com/104.94.191.203:443]
PUSH_PROMISE: streamId=3, promisedStreamId=2, headers=DefaultHttp2Headers[:method: GET,
:path: /resources/push.css, :authority: http2.akamai.com, :scheme: https, host: http2.akamai.com,
user-agent: XbibHttpClient/unknown (Java/Azul Systems, Inc./1.8.0_112) (Netty/4.1.9.Final),
accept-encoding: gzip], padding=0
------------------------------------
2017-05-01 18:53:46.076 AM FEINSTEN [org.xbib.netty.http.client.HttpClientChannelInitializer]
io.netty.handler.codec.http2.Http2FrameLogger log
----------------OUTBOUND--------------------
[id: 0x27d23874, L:/192.168.178.23:52376 - R:http2.akamai.com/104.94.191.203:443]
GO_AWAY: lastStreamId=2, errorCode=1, length=75, bytes=556e7265636f676e697a656420485454...
*/
HttpClient httpClient = HttpClient.builder()
.build();
httpClient.prepareGet()
.setVersion("HTTP/2.0")
.setURL("https://http2.akamai.com/demo/h2_demo_frame.html")
.onError(e -> logger.log(Level.SEVERE, e.getMessage(), e))
.onResponse(fullHttpResponse -> {
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
})
.execute()
.get();
httpClient.close();
}
}

View file

@ -0,0 +1,127 @@
/*
* Copyright 2017 Jörg Prante
*
* Jörg Prante licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package org.xbib.netty.http.client.test;
import org.junit.Ignore;
import org.junit.Test;
import org.xbib.netty.http.client.HttpClient;
import org.xbib.netty.http.client.HttpRequestBuilder;
import org.xbib.netty.http.client.HttpRequestContext;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.ConsoleHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;
/**
*/
public class ElasticsearchTest {
static {
System.setProperty("java.util.logging.SimpleFormatter.format",
"%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$-7s [%3$s] %2$s %5$s %6$s%n");
LogManager.getLogManager().reset();
Logger rootLogger = LogManager.getLogManager().getLogger("");
Handler handler = new ConsoleHandler();
handler.setFormatter(new SimpleFormatter());
rootLogger.addHandler(handler);
rootLogger.setLevel(Level.INFO);
for (Handler h : rootLogger.getHandlers()) {
handler.setFormatter(new SimpleFormatter());
h.setLevel(Level.INFO);
}
}
private static final Logger logger = Logger.getLogger("");
@Test
public void testElasticsearchCreateDocument() throws Exception {
HttpClient httpClient = HttpClient.builder()
.build();
HttpRequestContext requestContext = httpClient.preparePut()
.setURL("http://localhost:9200/test/test/1")
.json("{\"text\":\"Hello World\"}")
.onResponse(fullHttpResponse -> {
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
})
.onError(e -> logger.log(Level.SEVERE, e.getMessage(), e))
.execute()
.get();
httpClient.close();
logger.log(Level.FINE, "took = " + requestContext.took());
}
@Test
public void testElasticsearchMatchQuery() throws Exception {
HttpClient httpClient = HttpClient.builder()
.build();
HttpRequestContext requestContext = httpClient.preparePost()
.setURL("http://localhost:9200/test/_search")
.json("{\"query\":{\"match\":{\"_all\":\"Hello World\"}}}")
.onResponse(fullHttpResponse -> {
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
})
.onError(e -> logger.log(Level.SEVERE, e.getMessage(), e))
.execute()
.get();
httpClient.close();
logger.log(Level.FINE, "took = " + requestContext.took());
}
@Test
public void testElasticsearchConcurrent() throws Exception {
int max = 100;
HttpClient httpClient = HttpClient.builder()
.build();
List<HttpRequestBuilder> queries = new ArrayList<>();
for (int i = 0; i < max; i++) {
queries.add(createQuery(httpClient));
}
List<HttpRequestContext> contexts = new ArrayList<>();
for (int i = 0; i < max; i++) {
contexts.add(queries.get(i).execute());
}
List<HttpRequestContext> responses = new ArrayList<>();
for (int i = 0; i < max; i++) {
responses.add(contexts.get(i).get());
}
for (int i = 0; i < max; i++) {
logger.log(Level.FINE, "took = " + responses.get(i).took());
}
httpClient.close();
logger.log(Level.INFO, "pool peak = " + httpClient.poolMap().getHttpClientChannelPoolHandler().getPeak());
}
private HttpRequestBuilder createQuery(HttpClient httpClient) throws IOException {
return httpClient.preparePost()
.setURL("http://localhost:9200/test/_search")
.json("{\"query\":{\"match\":{\"_all\":\"Hello World\"}}}")
.addHeader("connection", "keep-alive")
.onResponse(fullHttpResponse -> {
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
})
.onError(e -> logger.log(Level.SEVERE, e.getMessage(), e));
}
}

View file

@ -0,0 +1,68 @@
/*
* Copyright 2017 Jörg Prante
*
* Jörg Prante licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package org.xbib.netty.http.client.test;
import org.junit.Test;
import org.xbib.netty.http.client.HttpClient;
import java.nio.charset.StandardCharsets;
import java.util.logging.ConsoleHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;
/**
*/
public class ExceptionTest {
static {
System.setProperty("java.util.logging.SimpleFormatter.format",
"%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$-7s [%3$s] %2$s %5$s %6$s%n");
LogManager.getLogManager().reset();
Logger rootLogger = LogManager.getLogManager().getLogger("");
Handler handler = new ConsoleHandler();
handler.setFormatter(new SimpleFormatter());
rootLogger.addHandler(handler);
rootLogger.setLevel(Level.ALL);
for (Handler h : rootLogger.getHandlers()) {
handler.setFormatter(new SimpleFormatter());
h.setLevel(Level.ALL);
}
}
private static final Logger logger = Logger.getLogger("");
@Test
public void testConnectionRefused() throws Exception {
// this basically tests if the connection refuse terminates.
HttpClient httpClient = HttpClient.builder()
.build();
httpClient.prepareGet()
.setURL("http://localhost:1234")
.onResponse(fullHttpResponse -> {
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
})
.onError(e -> logger.log(Level.SEVERE, e.getMessage(), e))
.execute()
.get();
httpClient.close();
}
}

View file

@ -0,0 +1,140 @@
package org.xbib.netty.http.client.test;
import org.junit.Ignore;
import org.junit.Test;
import org.xbib.netty.http.client.HttpClient;
import org.xbib.netty.http.client.HttpRequestBuilder;
import org.xbib.netty.http.client.HttpRequestContext;
import java.nio.charset.StandardCharsets;
import java.util.logging.ConsoleHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;
/**
*/
public class GoogleTest {
static {
System.setProperty("java.util.logging.SimpleFormatter.format",
"%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$-7s [%3$s] %2$s %5$s %6$s%n");
LogManager.getLogManager().reset();
Logger rootLogger = LogManager.getLogManager().getLogger("");
Handler handler = new ConsoleHandler();
handler.setFormatter(new SimpleFormatter());
rootLogger.addHandler(handler);
rootLogger.setLevel(Level.ALL);
for (Handler h : rootLogger.getHandlers()) {
handler.setFormatter(new SimpleFormatter());
h.setLevel(Level.ALL);
}
}
private static final Logger logger = Logger.getLogger("");
@Test
public void testGoogleHttp1() throws Exception {
HttpClient httpClient = HttpClient.builder()
.build();
httpClient.prepareGet()
.setURL("http://www.google.com")
.onError(e -> logger.log(Level.SEVERE, e.getMessage(), e))
.onResponse(fullHttpResponse -> {
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
})
.execute()
.get();
httpClient.close();
}
@Test
public void testGoogleWithoutFollowRedirects() throws Exception {
HttpClient httpClient = HttpClient.builder()
.build();
httpClient.prepareGet()
.setURL("http://google.com")
.setFollowRedirect(false)
.onResponse(fullHttpResponse -> {
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
})
.execute()
.get();
logger.log(Level.INFO, "pool size = " + httpClient.poolMap().size());
httpClient.close();
}
@Test
public void testGoogleHttps1() throws Exception {
HttpClient httpClient = HttpClient.builder()
.build();
httpClient.prepareGet()
.setURL("https://www.google.com")
.onError(e -> logger.log(Level.SEVERE, e.getMessage(), e))
.onResponse(fullHttpResponse -> {
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
})
.execute()
.get();
httpClient.close();
}
@Test
public void testGoogleHttp2() throws Exception {
HttpClient httpClient = HttpClient.builder()
.build();
httpClient.prepareGet()
.setVersion("HTTP/2.0")
.setURL("https://www.google.com")
.onError(e -> logger.log(Level.SEVERE, e.getMessage(), e))
.onResponse(fullHttpResponse -> {
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
})
.execute()
.get();
httpClient.close();
}
@Test
public void testGoogleHttpTwo() throws Exception {
HttpClient httpClient = HttpClient.builder()
.build();
HttpRequestBuilder builder1 = httpClient.prepareGet()
.setVersion("HTTP/2.0")
.setURL("https://www.google.com")
.setTimeout(10000)
.onError(e -> logger.log(Level.SEVERE, e.getMessage(), e))
.onResponse(fullHttpResponse -> {
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
});
HttpRequestBuilder builder2 = httpClient.prepareGet()
.setVersion("HTTP/2.0")
.setURL("https://www.google.com")
.setTimeout(10000)
.onError(e -> logger.log(Level.SEVERE, e.getMessage(), e))
.onResponse(fullHttpResponse -> {
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
});
// only sequential ... this sucks.
HttpRequestContext context1 = builder1.execute();
context1.get();
HttpRequestContext context2 = builder2.execute();
context2.get();
httpClient.close();
}
}

View file

@ -0,0 +1,52 @@
package org.xbib.netty.http.client.test;
import org.junit.Test;
import org.xbib.netty.http.client.HttpClient;
import java.nio.charset.StandardCharsets;
import java.util.logging.ConsoleHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;
/**
*/
public class Http2PushioTest {
static {
System.setProperty("java.util.logging.SimpleFormatter.format",
"%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$-7s [%3$s] %2$s %5$s %6$s%n");
LogManager.getLogManager().reset();
Logger rootLogger = LogManager.getLogManager().getLogger("");
Handler handler = new ConsoleHandler();
handler.setFormatter(new SimpleFormatter());
rootLogger.addHandler(handler);
rootLogger.setLevel(Level.ALL);
for (Handler h : rootLogger.getHandlers()) {
handler.setFormatter(new SimpleFormatter());
h.setLevel(Level.ALL);
}
}
private static final Logger logger = Logger.getLogger("");
@Test
public void testHttpPushIo() throws Exception {
HttpClient httpClient = HttpClient.builder()
.build();
httpClient.prepareGet()
.setVersion("HTTP/2.0")
.setURL("https://http2-push.io")
.onError(e -> logger.log(Level.SEVERE, e.getMessage(), e))
.onResponse(fullHttpResponse -> {
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
})
.execute()
.get();
httpClient.close();
}
}

View file

@ -0,0 +1,209 @@
package org.xbib.netty.http.client.test;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http2.DefaultHttp2Connection;
import io.netty.handler.codec.http2.DefaultHttp2Headers;
import io.netty.handler.codec.http2.DelegatingDecompressorFrameListener;
import io.netty.handler.codec.http2.Http2Connection;
import io.netty.handler.codec.http2.Http2ConnectionHandler;
import io.netty.handler.codec.http2.Http2ConnectionHandlerBuilder;
import io.netty.handler.codec.http2.Http2Exception;
import io.netty.handler.codec.http2.Http2FrameAdapter;
import io.netty.handler.codec.http2.Http2FrameLogger;
import io.netty.handler.codec.http2.Http2SecurityUtil;
import io.netty.handler.codec.http2.Http2Settings;
import io.netty.handler.codec.http2.InboundHttp2ToHttpAdapterBuilder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.ssl.ApplicationProtocolConfig;
import io.netty.handler.ssl.ApplicationProtocolNames;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.ssl.SslProvider;
import io.netty.handler.ssl.SupportedCipherSuiteFilter;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import org.junit.Test;
import org.xbib.netty.http.client.Http2Handler;
import org.xbib.netty.http.client.TrafficLoggingHandler;
import javax.net.ssl.SNIHostName;
import javax.net.ssl.SNIServerName;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLParameters;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.util.Arrays;
import java.util.concurrent.CountDownLatch;
import java.util.logging.ConsoleHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;
/**
*/
public class Http2Test {
static {
System.setProperty("java.util.logging.SimpleFormatter.format",
"%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$-7s [%3$s] %2$s %5$s %6$s%n");
LogManager.getLogManager().reset();
Logger rootLogger = LogManager.getLogManager().getLogger("");
Handler handler = new ConsoleHandler();
handler.setFormatter(new SimpleFormatter());
rootLogger.addHandler(handler);
rootLogger.setLevel(Level.ALL);
for (Handler h : rootLogger.getHandlers()) {
handler.setFormatter(new SimpleFormatter());
h.setLevel(Level.ALL);
}
}
private static final Logger logger = Logger.getLogger("");
private final int serverExpectedDataFrames = 1;
public void testGeneric() throws Exception {
final InetSocketAddress inetSocketAddress = new InetSocketAddress("http2-push.io", 443);
final CountDownLatch dataLatch = new CountDownLatch(serverExpectedDataFrames);
EventLoopGroup group = new NioEventLoopGroup();
Channel clientChannel = null;
try {
Bootstrap bs = new Bootstrap();
bs.group(group);
bs.channel(NioSocketChannel.class);
bs.handler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
ch.pipeline().addLast(new TrafficLoggingHandler());
SslContext sslContext = SslContextBuilder.forClient()
.sslProvider(SslProvider.OPENSSL)
.trustManager(InsecureTrustManagerFactory.INSTANCE)
.keyManager((InputStream) null, null, null)
.ciphers(Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE)
.applicationProtocolConfig(new ApplicationProtocolConfig(
ApplicationProtocolConfig.Protocol.ALPN,
ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE,
ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT,
ApplicationProtocolNames.HTTP_2,
ApplicationProtocolNames.HTTP_1_1))
.build();
SslHandler sslHandler = sslContext.newHandler(ch.alloc());
SSLEngine engine = sslHandler.engine();
String fullQualifiedHostname = inetSocketAddress.getHostName();
SSLParameters params = engine.getSSLParameters();
params.setServerNames(Arrays.asList(new SNIServerName[]{new SNIHostName(fullQualifiedHostname)}));
engine.setSSLParameters(params);
ch.pipeline().addLast(sslHandler);
Http2ConnectionHandlerBuilder builder = new Http2ConnectionHandlerBuilder();
builder.server(false);
builder.frameListener(new Http2FrameAdapter() {
@Override
public void onSettingsRead(ChannelHandlerContext ctx, Http2Settings settings)
throws Http2Exception {
Http2ConnectionHandler handler = ctx.pipeline().get(Http2ConnectionHandler.class);
handler.encoder().writeHeaders(ctx, 3,
new DefaultHttp2Headers().method(HttpMethod.GET.asciiName())
.path("/")
.scheme("https")
.authority(inetSocketAddress.getHostName()),
0, true, ctx.newPromise());
ctx.channel().flush();
}
@Override
public int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding,
boolean endOfStream) throws Http2Exception {
dataLatch.countDown();
return super.onDataRead(ctx, streamId, data, padding, endOfStream);
}
});
builder.frameLogger(new Http2FrameLogger(LogLevel.INFO, "client"));
ch.pipeline().addLast(builder.build());
}
});
clientChannel = bs.connect(inetSocketAddress).syncUninterruptibly().channel();
dataLatch.await();
} finally {
if (clientChannel != null) {
clientChannel.close();
}
group.shutdownGracefully();
}
}
public void testHttpAdapter() throws Exception {
final InetSocketAddress inetSocketAddress = new InetSocketAddress("http2-push.io", 443);
final CountDownLatch dataLatch = new CountDownLatch(serverExpectedDataFrames);
EventLoopGroup group = new NioEventLoopGroup();
Channel clientChannel = null;
try {
Bootstrap bs = new Bootstrap();
bs.group(group);
bs.channel(NioSocketChannel.class);
bs.handler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
ch.pipeline().addLast(new TrafficLoggingHandler());
SslContext sslContext = SslContextBuilder.forClient()
.sslProvider(SslProvider.OPENSSL)
.trustManager(InsecureTrustManagerFactory.INSTANCE)
.keyManager((InputStream) null, null, null)
.ciphers(Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE)
.applicationProtocolConfig(new ApplicationProtocolConfig(
ApplicationProtocolConfig.Protocol.ALPN,
ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE,
ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT,
ApplicationProtocolNames.HTTP_2,
ApplicationProtocolNames.HTTP_1_1))
.build();
SslHandler sslHandler = sslContext.newHandler(ch.alloc());
SSLEngine engine = sslHandler.engine();
String fullQualifiedHostname = inetSocketAddress.getHostName();
SSLParameters params = engine.getSSLParameters();
params.setServerNames(Arrays.asList(new SNIServerName[]{new SNIHostName(fullQualifiedHostname)}));
engine.setSSLParameters(params);
ch.pipeline().addLast(sslHandler);
// settings handler
final Http2Connection http2Connection = new DefaultHttp2Connection(false);
Http2ConnectionHandler http2ConnectionHandler = new Http2ConnectionHandlerBuilder()
.frameLogger(new Http2FrameLogger(LogLevel.INFO, "client"))
.frameListener(new DelegatingDecompressorFrameListener(http2Connection,
new InboundHttp2ToHttpAdapterBuilder(http2Connection)
.maxContentLength(1024 * 1024)
.propagateSettings(true)
.validateHttpHeaders(false)
.build()))
.build();
ch.pipeline().addLast(http2ConnectionHandler);
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
dataLatch.countDown();
}
});
}
});
clientChannel = bs.connect(inetSocketAddress).syncUninterruptibly().channel();
dataLatch.await();
} finally {
if (clientChannel != null) {
clientChannel.close();
}
group.shutdownGracefully();
}
}
}

View file

@ -0,0 +1,188 @@
/*
* Copyright 2017 Jörg Prante
*
* Jörg Prante licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package org.xbib.netty.http.client.test;
import io.netty.handler.codec.http.FullHttpResponse;
import org.junit.Ignore;
import org.junit.Test;
import org.xbib.netty.http.client.HttpClient;
import org.xbib.netty.http.client.HttpRequestBuilder;
import org.xbib.netty.http.client.HttpRequestContext;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import java.util.logging.ConsoleHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;
/**
*/
public class IndexHbzTest {
static {
System.setProperty("java.util.logging.SimpleFormatter.format",
"%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$-7s [%3$s] %2$s %5$s %6$s%n");
LogManager.getLogManager().reset();
Logger rootLogger = LogManager.getLogManager().getLogger("");
Handler handler = new ConsoleHandler();
handler.setFormatter(new SimpleFormatter());
rootLogger.addHandler(handler);
rootLogger.setLevel(Level.ALL);
for (Handler h : rootLogger.getHandlers()) {
handler.setFormatter(new SimpleFormatter());
h.setLevel(Level.ALL);
}
}
private static final Logger logger = Logger.getLogger("");
@Test
public void testIndexHbz() throws Exception {
HttpClient httpClient = HttpClient.builder()
.build();
httpClient.prepareGet()
.setVersion("HTTP/1.1")
.setURL("http://index.hbz-nrw.de")
.onError(e -> logger.log(Level.SEVERE, e.getMessage(), e))
.onResponse(fullHttpResponse -> {
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
})
.execute()
.get();
httpClient.close();
}
@Test
public void testIndexHbzHttps() throws Exception {
HttpClient httpClient = HttpClient.builder()
.build();
httpClient.prepareGet()
.setVersion("HTTP/1.1")
.setURL("https://index.hbz-nrw.de")
.onError(e -> logger.log(Level.SEVERE, e.getMessage(), e))
.onResponse(fullHttpResponse -> {
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
})
.execute()
.get();
httpClient.close();
}
@Test
public void testIndexHbzWithCompletableFuture() throws Exception {
// fetches "test" as content from index.hbz-nrw.de and continues with sending another URL to google.com
// tricky: google.com does not completely redirect because the first httpResponseStringFunction wins
// and generates the desired string result
HttpClient httpClient = HttpClient.builder()
.build();
final Function<FullHttpResponse, String> httpResponseStringFunction =
response -> response.content().toString(StandardCharsets.UTF_8);
final CompletableFuture<String> completableFuture = httpClient.prepareGet()
.setURL("http://index.hbz-nrw.de")
.execute(httpResponseStringFunction)
.exceptionally(Throwable::getMessage)
.thenCompose(content -> httpClient.prepareGet()
.setURL("http://google.com/?query=" + content)
.execute(httpResponseStringFunction));
String result = completableFuture.join();
logger.log(Level.INFO, "completablefuture result = " + result);
httpClient.close();
}
@Test
public void testIndexHbzH2() throws Exception {
HttpClient httpClient = HttpClient.builder()
.build();
httpClient.prepareGet()
.setVersion("HTTP/2.0")
.setURL("https://index.hbz-nrw.de")
.setTimeout(5000)
.onError(e -> logger.log(Level.SEVERE, e.getMessage(), e))
.onResponse(fullHttpResponse -> {
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
})
.execute()
.get();
httpClient.close();
}
@Test
public void testIndexHbzH2C() throws Exception {
// times out waiting for http2 settings frame
HttpClient httpClient = HttpClient.builder()
.setInstallHttp2Upgrade(true)
.build();
httpClient.prepareGet()
.setVersion("HTTP/2.0")
.setURL("http://index.hbz-nrw.de")
.onError(e -> logger.log(Level.SEVERE, e.getMessage(), e))
.onResponse(fullHttpResponse -> {
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
})
.execute()
.get();
httpClient.close();
}
@Test
public void testIndexHbzConcurrent() throws Exception {
HttpClient httpClient = HttpClient.builder()
.build();
HttpRequestBuilder builder1 = httpClient.prepareGet()
.setVersion("HTTP/1.1")
.setURL("http://index.hbz-nrw.de")
.onError(e -> logger.log(Level.SEVERE, e.getMessage(), e))
.onResponse(fullHttpResponse -> {
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
});
HttpRequestBuilder builder2 = httpClient.prepareGet()
.setVersion("HTTP/1.1")
.setURL("http://index.hbz-nrw.de")
.onError(e -> logger.log(Level.SEVERE, e.getMessage(), e))
.onResponse(fullHttpResponse -> {
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
});
HttpRequestContext context1 = builder1.execute();
HttpRequestContext context2 = builder2.execute();
context1.get();
context2.get();
httpClient.close();
}
}

View file

@ -0,0 +1,161 @@
/*
* Copyright 2017 Jörg Prante
*
* Jörg Prante licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package org.xbib.netty.http.client.test;
import io.netty.handler.codec.http.FullHttpResponse;
import org.junit.Ignore;
import org.junit.Test;
import org.xbib.netty.http.client.HttpClient;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import java.util.logging.ConsoleHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;
/**
*/
public class XbibTest {
static {
System.setProperty("java.util.logging.SimpleFormatter.format",
"%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$-7s [%3$s] %2$s %5$s %6$s%n");
LogManager.getLogManager().reset();
Logger rootLogger = LogManager.getLogManager().getLogger("");
Handler handler = new ConsoleHandler();
handler.setFormatter(new SimpleFormatter());
rootLogger.addHandler(handler);
rootLogger.setLevel(Level.ALL);
for (Handler h : rootLogger.getHandlers()) {
handler.setFormatter(new SimpleFormatter());
h.setLevel(Level.ALL);
}
}
private static final Logger logger = Logger.getLogger("");
@Test
public void testXbibOrgWithDefaults() throws Exception {
HttpClient httpClient = HttpClient.builder()
.build();
httpClient.prepareGet()
.setURL("http://xbib.org")
.onResponse(fullHttpResponse -> {
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
})
.execute()
.get();
httpClient.close();
}
@Test
public void testXbibOrgWithCompletableFuture() throws Exception {
HttpClient httpClient = HttpClient.builder()
.setTcpNodelay(true)
.build();
final Function<FullHttpResponse, String> httpResponseStringFunction =
response -> response.content().toString(StandardCharsets.UTF_8);
final CompletableFuture<String> completableFuture = httpClient.prepareGet()
.setURL("http://index.hbz-nrw.de")
.execute(httpResponseStringFunction)
.exceptionally(Throwable::getMessage)
.thenCompose(content -> httpClient.prepareGet()
.setURL("http://google.de/?query=" + content)
.execute(httpResponseStringFunction));
String result = completableFuture.join();
logger.log(Level.FINE, "completablefuture result = " + result);
httpClient.close();
}
@Test
public void testXbibOrgWithProxy() throws Exception {
HttpClient httpClient = HttpClient.builder()
.setHttpProxyHandler(new InetSocketAddress("80.241.223.251", 8080))
.setConnectTimeoutMillis(30000)
.setReadTimeoutMillis(30000)
.build();
httpClient.prepareGet()
.setURL("http://xbib.org")
.onResponse(fullHttpResponse -> {
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
})
.onError(e -> logger.log(Level.SEVERE, e.getMessage(), e))
.execute()
.get();
httpClient.close();
}
@Test
public void testXbibOrgWithVeryShortReadTimeout() throws Exception {
logger.log(Level.FINE, "start");
HttpClient httpClient = HttpClient.builder()
.setReadTimeoutMillis(50)
.build();
httpClient.prepareGet()
.setURL("http://xbib.org")
.onResponse(fullHttpResponse -> {
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
})
.onError(e -> logger.log(Level.SEVERE, e.getMessage(), e))
.execute()
.get();
httpClient.close();
logger.log(Level.FINE, "end");
}
@Test
public void testXbibTwoSequentialRequests() throws Exception {
HttpClient httpClient = HttpClient.builder()
.build();
httpClient.prepareGet()
.setVersion("HTTP/1.1")
.setURL("http://xbib.org")
.onError(e -> logger.log(Level.SEVERE, e.getMessage(), e))
.onResponse(fullHttpResponse -> {
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
})
.execute()
.get();
httpClient.prepareGet()
.setVersion("HTTP/1.1")
.setURL("http://xbib.org")
.onError(e -> logger.log(Level.SEVERE, e.getMessage(), e))
.onResponse(fullHttpResponse -> {
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
})
.execute()
.get();
httpClient.close();
}
}

View file

@ -0,0 +1,4 @@
/**
* Classes for testing Netty HTTP client.
*/
package org.xbib.netty.http.client.test;