commit 1e1b8469b2c7e524242a86d34937629bf19980bb Author: Jörg Prante Date: Tue May 2 00:52:09 2017 +0200 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..644a0f3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +/data +/work +/logs +/.idea +/target +.DS_Store +/.settings +/.classpath +/.project +/.gradle +/build +*~ \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE.txt @@ -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. diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..387b777 --- /dev/null +++ b/build.gradle @@ -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' diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000..b22b02d --- /dev/null +++ b/config/checkstyle/checkstyle.xml @@ -0,0 +1,323 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..7557ac9 --- /dev/null +++ b/gradle.properties @@ -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 diff --git a/gradle/ext.gradle b/gradle/ext.gradle new file mode 100644 index 0000000..dcb8f32 --- /dev/null +++ b/gradle/ext.gradle @@ -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' +} diff --git a/gradle/publish.gradle b/gradle/publish.gradle new file mode 100644 index 0000000..ccafc9b --- /dev/null +++ b/gradle/publish.gradle @@ -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" +} diff --git a/gradle/sonarqube.gradle b/gradle/sonarqube.gradle new file mode 100644 index 0000000..ba85ed2 --- /dev/null +++ b/gradle/sonarqube.gradle @@ -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/" + } +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..978e78a Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..d42d371 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/src/docs/asciidoc/css/foundation.css b/src/docs/asciidoc/css/foundation.css new file mode 100644 index 0000000..27be611 --- /dev/null +++ b/src/docs/asciidoc/css/foundation.css @@ -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; } diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc new file mode 100644 index 0000000..f57255f --- /dev/null +++ b/src/docs/asciidoc/index.adoc @@ -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 diff --git a/src/docs/asciidoclet/overview.adoc b/src/docs/asciidoclet/overview.adoc new file mode 100644 index 0000000..0cbb854 --- /dev/null +++ b/src/docs/asciidoclet/overview.adoc @@ -0,0 +1,4 @@ += Netty HTTP client +Jörg Prante +Version 4.1.9.0 + diff --git a/src/main/java/org/xbib/netty/http/client/ExceptionListener.java b/src/main/java/org/xbib/netty/http/client/ExceptionListener.java new file mode 100644 index 0000000..4adc333 --- /dev/null +++ b/src/main/java/org/xbib/netty/http/client/ExceptionListener.java @@ -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); +} diff --git a/src/main/java/org/xbib/netty/http/client/Http1Handler.java b/src/main/java/org/xbib/netty/http/client/Http1Handler.java new file mode 100755 index 0000000..7f2d84e --- /dev/null +++ b/src/main/java/org/xbib/netty/http/client/Http1Handler.java @@ -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()); + } +} diff --git a/src/main/java/org/xbib/netty/http/client/Http2Handler.java b/src/main/java/org/xbib/netty/http/client/Http2Handler.java new file mode 100644 index 0000000..769d705 --- /dev/null +++ b/src/main/java/org/xbib/netty/http/client/Http2Handler.java @@ -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 { + + private static final Logger logger = Logger.getLogger(Http2Handler.class.getName()); + + private final Map> 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 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>> iterator = streamidPromiseMap.entrySet().iterator(); + while (iterator.hasNext()) { + Entry> 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(); + } + } +} diff --git a/src/main/java/org/xbib/netty/http/client/HttpClient.java b/src/main/java/org/xbib/netty/http/client/HttpClient.java new file mode 100755 index 0000000..3049ed9 --- /dev/null +++ b/src/main/java/org/xbib/netty/http/client/HttpClient.java @@ -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 futureChannel = pool.acquire(); + futureChannel.addListener((FutureListener) 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 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(); + } +} diff --git a/src/main/java/org/xbib/netty/http/client/HttpClientBuilder.java b/src/main/java/org/xbib/netty/http/client/HttpClientBuilder.java new file mode 100644 index 0000000..eb45240 --- /dev/null +++ b/src/main/java/org/xbib/netty/http/client/HttpClientBuilder.java @@ -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 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 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 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 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); + } +} diff --git a/src/main/java/org/xbib/netty/http/client/HttpClientChannelContext.java b/src/main/java/org/xbib/netty/http/client/HttpClientChannelContext.java new file mode 100644 index 0000000..c72d7c6 --- /dev/null +++ b/src/main/java/org/xbib/netty/http/client/HttpClientChannelContext.java @@ -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 CHANNEL_POOL_ATTRIBUTE_KEY = + AttributeKey.valueOf("httpClientChannelPool"); + + static final AttributeKey REQUEST_CONTEXT_ATTRIBUTE_KEY = + AttributeKey.valueOf("httpClientRequestContext"); + + static final AttributeKey RESPONSE_LISTENER_ATTRIBUTE_KEY = + AttributeKey.valueOf("httpClientResponseListener"); + + static final AttributeKey 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 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 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 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; + } +} diff --git a/src/main/java/org/xbib/netty/http/client/HttpClientChannelContextDefaults.java b/src/main/java/org/xbib/netty/http/client/HttpClientChannelContextDefaults.java new file mode 100644 index 0000000..847a881 --- /dev/null +++ b/src/main/java/org/xbib/netty/http/client/HttpClientChannelContextDefaults.java @@ -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 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; +} diff --git a/src/main/java/org/xbib/netty/http/client/HttpClientChannelInitializer.java b/src/main/java/org/xbib/netty/http/client/HttpClientChannelInitializer.java new file mode 100644 index 0000000..5fcb76e --- /dev/null +++ b/src/main/java/org/xbib/netty/http/client/HttpClientChannelInitializer.java @@ -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 { + + 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 { + + 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); + } + } +} diff --git a/src/main/java/org/xbib/netty/http/client/HttpClientChannelPoolHandler.java b/src/main/java/org/xbib/netty/http/client/HttpClientChannelPoolHandler.java new file mode 100644 index 0000000..c226706 --- /dev/null +++ b/src/main/java/org/xbib/netty/http/client/HttpClientChannelPoolHandler.java @@ -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; + } +} diff --git a/src/main/java/org/xbib/netty/http/client/HttpClientChannelPoolMap.java b/src/main/java/org/xbib/netty/http/client/HttpClientChannelPoolMap.java new file mode 100644 index 0000000..c8f07fa --- /dev/null +++ b/src/main/java/org/xbib/netty/http/client/HttpClientChannelPoolMap.java @@ -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 { + + 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; + } +} diff --git a/src/main/java/org/xbib/netty/http/client/HttpClientRequestBuilder.java b/src/main/java/org/xbib/netty/http/client/HttpClientRequestBuilder.java new file mode 100644 index 0000000..d6bb46d --- /dev/null +++ b/src/main/java/org/xbib/netty/http/client/HttpClientRequestBuilder.java @@ -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 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 CompletableFuture execute(Function supplier) { + final CompletableFuture completableFuture = new CompletableFuture<>(); + onResponse(response -> completableFuture.complete(supplier.apply(response))); + onError(completableFuture::completeExceptionally); + execute(); + return completableFuture; + } +} diff --git a/src/main/java/org/xbib/netty/http/client/HttpClientThreadFactory.java b/src/main/java/org/xbib/netty/http/client/HttpClientThreadFactory.java new file mode 100644 index 0000000..4c426a6 --- /dev/null +++ b/src/main/java/org/xbib/netty/http/client/HttpClientThreadFactory.java @@ -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; + } +} diff --git a/src/main/java/org/xbib/netty/http/client/HttpClientUserAgent.java b/src/main/java/org/xbib/netty/http/client/HttpClientUserAgent.java new file mode 100644 index 0000000..003b0ee --- /dev/null +++ b/src/main/java/org/xbib/netty/http/client/HttpClientUserAgent.java @@ -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"); + } +} diff --git a/src/main/java/org/xbib/netty/http/client/HttpRequestBuilder.java b/src/main/java/org/xbib/netty/http/client/HttpRequestBuilder.java new file mode 100644 index 0000000..49ee16a --- /dev/null +++ b/src/main/java/org/xbib/netty/http/client/HttpRequestBuilder.java @@ -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(); + + CompletableFuture execute(Function supplier); +} diff --git a/src/main/java/org/xbib/netty/http/client/HttpRequestContext.java b/src/main/java/org/xbib/netty/http/client/HttpRequestContext.java new file mode 100755 index 0000000..4fc8716 --- /dev/null +++ b/src/main/java/org/xbib/netty/http/client/HttpRequestContext.java @@ -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; + } + +} diff --git a/src/main/java/org/xbib/netty/http/client/HttpRequestDefaults.java b/src/main/java/org/xbib/netty/http/client/HttpRequestDefaults.java new file mode 100644 index 0000000..190e5fd --- /dev/null +++ b/src/main/java/org/xbib/netty/http/client/HttpRequestDefaults.java @@ -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; +} diff --git a/src/main/java/org/xbib/netty/http/client/HttpResponseListener.java b/src/main/java/org/xbib/netty/http/client/HttpResponseListener.java new file mode 100644 index 0000000..b462987 --- /dev/null +++ b/src/main/java/org/xbib/netty/http/client/HttpResponseListener.java @@ -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); +} diff --git a/src/main/java/org/xbib/netty/http/client/InetAddressKey.java b/src/main/java/org/xbib/netty/http/client/InetAddressKey.java new file mode 100644 index 0000000..acb98b1 --- /dev/null +++ b/src/main/java/org/xbib/netty/http/client/InetAddressKey.java @@ -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(); + } +} diff --git a/src/main/java/org/xbib/netty/http/client/SslClientAuthMode.java b/src/main/java/org/xbib/netty/http/client/SslClientAuthMode.java new file mode 100644 index 0000000..0de5849 --- /dev/null +++ b/src/main/java/org/xbib/netty/http/client/SslClientAuthMode.java @@ -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 +} diff --git a/src/main/java/org/xbib/netty/http/client/TrafficLoggingHandler.java b/src/main/java/org/xbib/netty/http/client/TrafficLoggingHandler.java new file mode 100644 index 0000000..1a1940e --- /dev/null +++ b/src/main/java/org/xbib/netty/http/client/TrafficLoggingHandler.java @@ -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); + } + } +} diff --git a/src/main/java/org/xbib/netty/http/client/package-info.java b/src/main/java/org/xbib/netty/http/client/package-info.java new file mode 100644 index 0000000..aef4b83 --- /dev/null +++ b/src/main/java/org/xbib/netty/http/client/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for Netty HTTP client. + */ +package org.xbib.netty.http.client; diff --git a/src/test/java/org/xbib/netty/http/client/test/AkamaiTest.java b/src/test/java/org/xbib/netty/http/client/test/AkamaiTest.java new file mode 100644 index 0000000..baf2292 --- /dev/null +++ b/src/test/java/org/xbib/netty/http/client/test/AkamaiTest.java @@ -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(); + } +} diff --git a/src/test/java/org/xbib/netty/http/client/test/ElasticsearchTest.java b/src/test/java/org/xbib/netty/http/client/test/ElasticsearchTest.java new file mode 100644 index 0000000..9300727 --- /dev/null +++ b/src/test/java/org/xbib/netty/http/client/test/ElasticsearchTest.java @@ -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 queries = new ArrayList<>(); + for (int i = 0; i < max; i++) { + queries.add(createQuery(httpClient)); + } + List contexts = new ArrayList<>(); + for (int i = 0; i < max; i++) { + contexts.add(queries.get(i).execute()); + } + List 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)); + } +} diff --git a/src/test/java/org/xbib/netty/http/client/test/ExceptionTest.java b/src/test/java/org/xbib/netty/http/client/test/ExceptionTest.java new file mode 100644 index 0000000..ddc4aee --- /dev/null +++ b/src/test/java/org/xbib/netty/http/client/test/ExceptionTest.java @@ -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(); + } +} diff --git a/src/test/java/org/xbib/netty/http/client/test/GoogleTest.java b/src/test/java/org/xbib/netty/http/client/test/GoogleTest.java new file mode 100644 index 0000000..9ad7564 --- /dev/null +++ b/src/test/java/org/xbib/netty/http/client/test/GoogleTest.java @@ -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(); + } +} diff --git a/src/test/java/org/xbib/netty/http/client/test/Http2PushioTest.java b/src/test/java/org/xbib/netty/http/client/test/Http2PushioTest.java new file mode 100644 index 0000000..6647873 --- /dev/null +++ b/src/test/java/org/xbib/netty/http/client/test/Http2PushioTest.java @@ -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(); + } +} diff --git a/src/test/java/org/xbib/netty/http/client/test/Http2Test.java b/src/test/java/org/xbib/netty/http/client/test/Http2Test.java new file mode 100644 index 0000000..26b65bc --- /dev/null +++ b/src/test/java/org/xbib/netty/http/client/test/Http2Test.java @@ -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() { + @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() { + @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(); + } + } +} diff --git a/src/test/java/org/xbib/netty/http/client/test/IndexHbzTest.java b/src/test/java/org/xbib/netty/http/client/test/IndexHbzTest.java new file mode 100644 index 0000000..bb93b8d --- /dev/null +++ b/src/test/java/org/xbib/netty/http/client/test/IndexHbzTest.java @@ -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 httpResponseStringFunction = + response -> response.content().toString(StandardCharsets.UTF_8); + + final CompletableFuture 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(); + } +} diff --git a/src/test/java/org/xbib/netty/http/client/test/XbibTest.java b/src/test/java/org/xbib/netty/http/client/test/XbibTest.java new file mode 100644 index 0000000..aab4bb4 --- /dev/null +++ b/src/test/java/org/xbib/netty/http/client/test/XbibTest.java @@ -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 httpResponseStringFunction = + response -> response.content().toString(StandardCharsets.UTF_8); + + final CompletableFuture 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(); + } +} diff --git a/src/test/java/org/xbib/netty/http/client/test/package-info.java b/src/test/java/org/xbib/netty/http/client/test/package-info.java new file mode 100644 index 0000000..6f8b69d --- /dev/null +++ b/src/test/java/org/xbib/netty/http/client/test/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for testing Netty HTTP client. + */ +package org.xbib.netty.http.client.test;