initial commit
This commit is contained in:
commit
1e1b8469b2
43 changed files with 5262 additions and 0 deletions
12
.gitignore
vendored
Normal file
12
.gitignore
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
/data
|
||||
/work
|
||||
/logs
|
||||
/.idea
|
||||
/target
|
||||
.DS_Store
|
||||
/.settings
|
||||
/.classpath
|
||||
/.project
|
||||
/.gradle
|
||||
/build
|
||||
*~
|
202
LICENSE.txt
Normal file
202
LICENSE.txt
Normal 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
121
build.gradle
Normal 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'
|
323
config/checkstyle/checkstyle.xml
Normal file
323
config/checkstyle/checkstyle.xml
Normal 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
10
gradle.properties
Normal 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
9
gradle/ext.gradle
Normal 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
70
gradle/publish.gradle
Normal 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
41
gradle/sonarqube.gradle
Normal 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
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal 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
684
src/docs/asciidoc/css/foundation.css
vendored
Normal 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; }
|
11
src/docs/asciidoc/index.adoc
Normal file
11
src/docs/asciidoc/index.adoc
Normal 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
|
4
src/docs/asciidoclet/overview.adoc
Normal file
4
src/docs/asciidoclet/overview.adoc
Normal file
|
@ -0,0 +1,4 @@
|
|||
= Netty HTTP client
|
||||
Jörg Prante
|
||||
Version 4.1.9.0
|
||||
|
|
@ -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);
|
||||
}
|
105
src/main/java/org/xbib/netty/http/client/Http1Handler.java
Executable file
105
src/main/java/org/xbib/netty/http/client/Http1Handler.java
Executable 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());
|
||||
}
|
||||
}
|
161
src/main/java/org/xbib/netty/http/client/Http2Handler.java
Normal file
161
src/main/java/org/xbib/netty/http/client/Http2Handler.java
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
363
src/main/java/org/xbib/netty/http/client/HttpClient.java
Executable file
363
src/main/java/org/xbib/netty/http/client/HttpClient.java
Executable 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();
|
||||
}
|
||||
}
|
326
src/main/java/org/xbib/netty/http/client/HttpClientBuilder.java
Normal file
326
src/main/java/org/xbib/netty/http/client/HttpClientBuilder.java
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
185
src/main/java/org/xbib/netty/http/client/HttpRequestContext.java
Executable file
185
src/main/java/org/xbib/netty/http/client/HttpRequestContext.java
Executable 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
78
src/main/java/org/xbib/netty/http/client/InetAddressKey.java
Normal file
78
src/main/java/org/xbib/netty/http/client/InetAddressKey.java
Normal 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();
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
/**
|
||||
* Classes for Netty HTTP client.
|
||||
*/
|
||||
package org.xbib.netty.http.client;
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
140
src/test/java/org/xbib/netty/http/client/test/GoogleTest.java
Normal file
140
src/test/java/org/xbib/netty/http/client/test/GoogleTest.java
Normal 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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
209
src/test/java/org/xbib/netty/http/client/test/Http2Test.java
Normal file
209
src/test/java/org/xbib/netty/http/client/test/Http2Test.java
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
188
src/test/java/org/xbib/netty/http/client/test/IndexHbzTest.java
Normal file
188
src/test/java/org/xbib/netty/http/client/test/IndexHbzTest.java
Normal 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();
|
||||
}
|
||||
}
|
161
src/test/java/org/xbib/netty/http/client/test/XbibTest.java
Normal file
161
src/test/java/org/xbib/netty/http/client/test/XbibTest.java
Normal 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();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
/**
|
||||
* Classes for testing Netty HTTP client.
|
||||
*/
|
||||
package org.xbib.netty.http.client.test;
|
Loading…
Reference in a new issue